Fix incorrect snippet queries (and generify some utilities)
This commit is contained in:
parent
06781b3b16
commit
5427092708
7
go.mod
7
go.mod
|
@ -44,7 +44,7 @@ require (
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||||
github.com/evanw/esbuild v0.21.4 // indirect
|
github.com/evanw/esbuild v0.21.4
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
@ -59,11 +59,10 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/teambition/rrule-go v1.7.2 // indirect
|
github.com/teambition/rrule-go v1.7.2 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
golang.org/x/net v0.6.0 // indirect
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
|
||||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.5.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
golang.org/x/text v0.7.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -117,8 +117,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
@ -282,8 +282,6 @@ github.com/teacat/noire v1.1.0/go.mod h1:cetGlnqr+9yKJcFgRgYXOWJY66XIrrjUsGBwNlN
|
||||||
github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
|
github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
|
||||||
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/wellington/go-libsass v0.9.2 h1:6Ims04UDdBs6/CGSVK5JC8FNikR5ssrsMMKE/uaO5Q8=
|
|
||||||
github.com/wellington/go-libsass v0.9.2/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
|
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||||
|
@ -310,6 +308,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||||
|
@ -339,8 +339,6 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -350,8 +348,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -402,8 +400,6 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn
|
||||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
|
|
@ -129,7 +129,7 @@ func FetchEmbed(ctx context.Context, urlStr string, httpClient *http.Client, max
|
||||||
filename := ""
|
filename := ""
|
||||||
u, err := url.Parse(urlStr)
|
u, err := url.Parse(urlStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
lastSlash := utils.IntMax(strings.LastIndex(u.Path, "/"), 0)
|
lastSlash := utils.Max(strings.LastIndex(u.Path, "/"), 0)
|
||||||
filename = u.Path[lastSlash:]
|
filename = u.Path[lastSlash:]
|
||||||
}
|
}
|
||||||
result := Embed{
|
result := Embed{
|
||||||
|
|
|
@ -99,16 +99,19 @@ func FetchSnippets(
|
||||||
TRUE
|
TRUE
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
allIDs := make([]int, 0, len(q.IDs)+len(tagSnippetIDs)+len(projectSnippetIDs))
|
allSnippetIDs := make([]int, 0, len(q.IDs)+len(tagSnippetIDs)+len(projectSnippetIDs))
|
||||||
allIDs = append(allIDs, q.IDs...)
|
allSnippetIDs = append(allSnippetIDs, q.IDs...)
|
||||||
allIDs = append(allIDs, tagSnippetIDs...)
|
allSnippetIDs = append(allSnippetIDs, tagSnippetIDs...)
|
||||||
allIDs = append(allIDs, projectSnippetIDs...)
|
allSnippetIDs = append(allSnippetIDs, projectSnippetIDs...)
|
||||||
if len(allIDs) > 0 && len(q.OwnerIDs) > 0 {
|
if len(allSnippetIDs) == 0 {
|
||||||
qb.Add(`AND (snippet.id = ANY ($?) OR snippet.owner_id = ANY ($?))`, allIDs, q.OwnerIDs)
|
// We already managed to filter out all snippets, and all further
|
||||||
|
// parts of this query are more filters, so we can just fail everything
|
||||||
|
// else from right here.
|
||||||
|
qb.Add(`AND FALSE`)
|
||||||
|
} else if len(q.OwnerIDs) > 0 {
|
||||||
|
qb.Add(`AND (snippet.id = ANY ($?) OR snippet.owner_id = ANY ($?))`, allSnippetIDs, q.OwnerIDs)
|
||||||
} else {
|
} else {
|
||||||
if len(allIDs) > 0 {
|
qb.Add(`AND snippet.id = ANY ($?)`, allSnippetIDs)
|
||||||
qb.Add(`AND snippet.id = ANY ($?)`, allIDs)
|
|
||||||
}
|
|
||||||
if len(q.OwnerIDs) > 0 {
|
if len(q.OwnerIDs) > 0 {
|
||||||
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
|
qb.Add(`AND snippet.owner_id = ANY ($?)`, q.OwnerIDs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,7 +289,7 @@ func seedProject(ctx context.Context, tx pgx.Tx, input models.Project, owners []
|
||||||
input.ForumEnabled, input.BlogEnabled,
|
input.ForumEnabled, input.BlogEnabled,
|
||||||
utils.OrDefault(input.DateCreated, time.Now()),
|
utils.OrDefault(input.DateCreated, time.Now()),
|
||||||
)
|
)
|
||||||
latestProjectId = utils.IntMax(latestProjectId, project.ID)
|
latestProjectId = utils.Max(latestProjectId, project.ID)
|
||||||
|
|
||||||
// Create forum (even if unused)
|
// Create forum (even if unused)
|
||||||
forum := db.MustQueryOne[models.Subforum](ctx, tx,
|
forum := db.MustQueryOne[models.Subforum](ctx, tx,
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
<legend>Owners</legend>
|
<legend>Owners</legend>
|
||||||
<div class="pa3">
|
<div class="pa3">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<input class="flex-grow-1 no-border bl bt bb br-0" id="owner_name" type="text" placeholder="Enter a username" />
|
<input class="flex-grow-1 no-border bl bt bb br-0" id="owner_name" type="text" placeholder="Enter another owner's username" />
|
||||||
<button class="flex no-padding no-border pa3 bt br bb bl-0 also-focus" id="owner_add"><span class="flex w1">{{ svg "add" }}</span></button>
|
<button class="flex no-padding no-border pa3 bt br bb bl-0 also-focus" id="owner_add"><span class="flex w1">{{ svg "add" }}</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="owners_error" class="f6"></div>
|
<div id="owners_error" class="f6"></div>
|
||||||
|
@ -163,11 +163,13 @@
|
||||||
</legend>
|
</legend>
|
||||||
<div class="pa3 input-group">
|
<div class="pa3 input-group">
|
||||||
<div id="links" class="flex flex-column g3 relative">
|
<div id="links" class="flex flex-column g3 relative">
|
||||||
|
<div>Primary Links</div>
|
||||||
|
<div>Secondary Links</div>
|
||||||
</div>
|
</div>
|
||||||
<template id="link_row">
|
<template id="link_row">
|
||||||
<div class="link_row w-100 flex flex-row items-center" data-tmpl="root">
|
<div class="link_row w-100 flex flex-row items-center" data-tmpl="root">
|
||||||
<span class="link_handle svgicon pr3 pointer grab" onmousedown="startLinkDrag(event)">{{ svg "draggable" }}</span>
|
<span class="link_handle svgicon pr3 pointer grab" onmousedown="startLinkDrag(event)">{{ svg "draggable" }}</span>
|
||||||
<input data-tmpl="nameInput" class="link_name mr3 w4" type="text" placeholder="Name" />
|
<input data-tmpl="nameInput" class="link_name mr3 w5" type="text" placeholder="Name" />
|
||||||
<input data-tmpl="urlInput" class="link_url flex-grow-1" type="url" placeholder="Link" />
|
<input data-tmpl="urlInput" class="link_url flex-grow-1" type="url" placeholder="Link" />
|
||||||
<a class="delete_link svgicon link--normal pl3 f3" href="javascript:;" onclick="deleteLink(event)">{{ svg "delete" }}</a>
|
<a class="delete_link svgicon link--normal pl3 f3" href="javascript:;" onclick="deleteLink(event)">{{ svg "delete" }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,7 @@ func getTwitchUsersByLogin(ctx context.Context, logins []string) ([]twitchUser,
|
||||||
for i := 0; i < numChunks; i++ {
|
for i := 0; i < numChunks; i++ {
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
query.Add("first", "100")
|
query.Add("first", "100")
|
||||||
for _, login := range logins[i*100 : utils.IntMin((i+1)*100, len(logins))] {
|
for _, login := range logins[i*100 : utils.Min((i+1)*100, len(logins))] {
|
||||||
query.Add("login", login)
|
query.Add("login", login)
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", buildUrl("/users", query.Encode()), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", buildUrl("/users", query.Encode()), nil)
|
||||||
|
@ -94,7 +94,7 @@ func getStreamStatus(ctx context.Context, twitchIDs []string) ([]streamStatus, e
|
||||||
for i := 0; i < numChunks; i++ {
|
for i := 0; i < numChunks; i++ {
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
query.Add("first", "100")
|
query.Add("first", "100")
|
||||||
for _, tid := range twitchIDs[i*100 : utils.IntMin((i+1)*100, len(twitchIDs))] {
|
for _, tid := range twitchIDs[i*100 : utils.Min((i+1)*100, len(twitchIDs))] {
|
||||||
query.Add("user_id", tid)
|
query.Add("user_id", tid)
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", buildUrl("/streams", query.Encode()), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", buildUrl("/streams", query.Encode()), nil)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the provided value, or a default value if the input was zero.
|
// Returns the provided value, or a default value if the input was zero.
|
||||||
|
@ -37,29 +38,26 @@ func Must1[T any](v T, err error) T {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func IntMin(a, b int) int {
|
func Min[T constraints.Ordered](a, b T) T {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func IntMax(a, b int) int {
|
func Max[T constraints.Ordered](a, b T) T {
|
||||||
if a > b {
|
if a > b {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func IntClamp(min, t, max int) int {
|
func Clamp[T constraints.Ordered](min, t, max T) T {
|
||||||
return IntMax(min, IntMin(t, max))
|
return Max(min, Min(t, max))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Int64Max(a, b int64) int64 {
|
func ClampSlice[T any](s []T, max int) []T {
|
||||||
if a > b {
|
return s[:Min(len(s), max)]
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DurationRoundUp(d time.Duration, interval time.Duration) time.Duration {
|
func DurationRoundUp(d time.Duration, interval time.Duration) time.Duration {
|
||||||
|
@ -67,7 +65,7 @@ func DurationRoundUp(d time.Duration, interval time.Duration) time.Duration {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NumPages(numThings, thingsPerPage int) int {
|
func NumPages(numThings, thingsPerPage int) int {
|
||||||
return IntMax(int(math.Ceil(float64(numThings)/float64(thingsPerPage))), 1)
|
return Max(int(math.Ceil(float64(numThings)/float64(thingsPerPage))), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -102,8 +102,8 @@ func BlogIndex(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
FirstUrl: c.UrlContext.BuildBlog(1),
|
FirstUrl: c.UrlContext.BuildBlog(1),
|
||||||
LastUrl: c.UrlContext.BuildBlog(numPages),
|
LastUrl: c.UrlContext.BuildBlog(numPages),
|
||||||
PreviousUrl: c.UrlContext.BuildBlog(utils.IntClamp(1, page-1, numPages)),
|
PreviousUrl: c.UrlContext.BuildBlog(utils.Clamp(1, page-1, numPages)),
|
||||||
NextUrl: c.UrlContext.BuildBlog(utils.IntClamp(1, page+1, numPages)),
|
NextUrl: c.UrlContext.BuildBlog(utils.Clamp(1, page+1, numPages)),
|
||||||
},
|
},
|
||||||
|
|
||||||
ShowContent: len(entries) <= 5,
|
ShowContent: len(entries) <= 5,
|
||||||
|
|
|
@ -53,8 +53,8 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
FirstUrl: hmnurl.BuildFeed(),
|
FirstUrl: hmnurl.BuildFeed(),
|
||||||
LastUrl: hmnurl.BuildFeedWithPage(numPages),
|
LastUrl: hmnurl.BuildFeedWithPage(numPages),
|
||||||
NextUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page+1, numPages)),
|
NextUrl: hmnurl.BuildFeedWithPage(utils.Clamp(1, page+1, numPages)),
|
||||||
PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
|
PreviousUrl: hmnurl.BuildFeedWithPage(utils.Clamp(1, page-1, numPages)),
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := fetchAllPosts(c, (page-1)*feedPostsPerPage, feedPostsPerPage)
|
posts, err := fetchAllPosts(c, (page-1)*feedPostsPerPage, feedPostsPerPage)
|
||||||
|
|
|
@ -198,8 +198,8 @@ func Forum(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
FirstUrl: c.UrlContext.BuildForum(currentSubforumSlugs, 1),
|
FirstUrl: c.UrlContext.BuildForum(currentSubforumSlugs, 1),
|
||||||
LastUrl: c.UrlContext.BuildForum(currentSubforumSlugs, numPages),
|
LastUrl: c.UrlContext.BuildForum(currentSubforumSlugs, numPages),
|
||||||
NextUrl: c.UrlContext.BuildForum(currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
NextUrl: c.UrlContext.BuildForum(currentSubforumSlugs, utils.Clamp(1, page+1, numPages)),
|
||||||
PreviousUrl: c.UrlContext.BuildForum(currentSubforumSlugs, utils.IntClamp(1, page-1, numPages)),
|
PreviousUrl: c.UrlContext.BuildForum(currentSubforumSlugs, utils.Clamp(1, page-1, numPages)),
|
||||||
},
|
},
|
||||||
Subforums: subforums,
|
Subforums: subforums,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
|
@ -375,8 +375,8 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
FirstUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, 1),
|
FirstUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, 1),
|
||||||
LastUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, numPages),
|
LastUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, numPages),
|
||||||
NextUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, utils.IntClamp(1, page+1, numPages)),
|
NextUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, utils.Clamp(1, page+1, numPages)),
|
||||||
PreviousUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, utils.IntClamp(1, page-1, numPages)),
|
PreviousUrl: c.UrlContext.BuildForumThread(currentSubforumSlugs, thread.ID, thread.Title, utils.Clamp(1, page-1, numPages)),
|
||||||
}
|
}
|
||||||
|
|
||||||
postsAndStuff, err := hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
postsAndStuff, err := hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
|
||||||
|
|
|
@ -117,7 +117,7 @@ func csrfMiddleware(h Handler) Handler {
|
||||||
func securityTimerMiddleware(duration time.Duration, h Handler) Handler {
|
func securityTimerMiddleware(duration time.Duration, h Handler) Handler {
|
||||||
// NOTE(asaf): Will make sure that the request takes at least `duration` to finish. Adds a 10% random duration.
|
// NOTE(asaf): Will make sure that the request takes at least `duration` to finish. Adds a 10% random duration.
|
||||||
return func(c *RequestContext) ResponseData {
|
return func(c *RequestContext) ResponseData {
|
||||||
additionalDuration := time.Duration(rand.Int63n(utils.Int64Max(1, int64(duration)/10)))
|
additionalDuration := time.Duration(rand.Int63n(utils.Max(1, int64(duration)/10)))
|
||||||
timer := time.NewTimer(duration + additionalDuration)
|
timer := time.NewTimer(duration + additionalDuration)
|
||||||
res := h(c)
|
res := h(c)
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -16,7 +16,7 @@ func getPageInfo(
|
||||||
totalPages int,
|
totalPages int,
|
||||||
ok bool,
|
ok bool,
|
||||||
) {
|
) {
|
||||||
totalPages = utils.IntMax(1, int(math.Ceil(float64(totalItems)/float64(itemsPerPage))))
|
totalPages = utils.Max(1, int(math.Ceil(float64(totalItems)/float64(itemsPerPage))))
|
||||||
ok = true
|
ok = true
|
||||||
|
|
||||||
page = 1
|
page = 1
|
||||||
|
|
|
@ -28,7 +28,7 @@ func ParsePageNumber(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if page < 1 || numPages < page {
|
if page < 1 || numPages < page {
|
||||||
return utils.IntClamp(1, page, numPages), false
|
return utils.Clamp(1, page, numPages), false
|
||||||
}
|
}
|
||||||
|
|
||||||
return page, true
|
return page, true
|
||||||
|
|
|
@ -126,12 +126,12 @@ func ProjectIndex(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
AllProjects: true,
|
AllProjects: true,
|
||||||
|
|
||||||
OfficialProjects: officialProjects[:utils.IntMin(len(officialProjects), projectsPerSection)],
|
OfficialProjects: officialProjects[:utils.Min(len(officialProjects), projectsPerSection)],
|
||||||
OfficialProjectsLink: hmnurl.BuildProjectIndex(1, "official"),
|
OfficialProjectsLink: hmnurl.BuildProjectIndex(1, "official"),
|
||||||
PersonalProjects: personalProjects[:utils.IntMin(len(personalProjects), projectsPerSection)],
|
PersonalProjects: personalProjects[:utils.Min(len(personalProjects), projectsPerSection)],
|
||||||
PersonalProjectsLink: hmnurl.BuildProjectIndex(1, "personal"),
|
PersonalProjectsLink: hmnurl.BuildProjectIndex(1, "personal"),
|
||||||
// Current jam stuff set later
|
// Current jam stuff set later
|
||||||
PreviousJamProjects: previousJamProjects[:utils.IntMin(len(previousJamProjects), projectsPerSection)],
|
PreviousJamProjects: previousJamProjects[:utils.Min(len(previousJamProjects), projectsPerSection)],
|
||||||
PreviousJamProjectsLink: hmnurl.BuildProjectIndex(1, previousJam.UrlSlug),
|
PreviousJamProjectsLink: hmnurl.BuildProjectIndex(1, previousJam.UrlSlug),
|
||||||
PreviousJamLink: hmnurl.BuildJamIndexAny(previousJam.UrlSlug),
|
PreviousJamLink: hmnurl.BuildJamIndexAny(previousJam.UrlSlug),
|
||||||
PreviousJamSlug: previousJam.UrlSlug,
|
PreviousJamSlug: previousJam.UrlSlug,
|
||||||
|
@ -140,7 +140,7 @@ func ProjectIndex(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentJam != nil {
|
if currentJam != nil {
|
||||||
tmpl.CurrentJamProjects = currentJamProjects[:utils.IntMin(len(currentJamProjects), projectsPerSection)]
|
tmpl.CurrentJamProjects = currentJamProjects[:utils.Min(len(currentJamProjects), projectsPerSection)]
|
||||||
tmpl.CurrentJamProjectsLink = hmnurl.BuildProjectIndex(1, currentJam.UrlSlug)
|
tmpl.CurrentJamProjectsLink = hmnurl.BuildProjectIndex(1, currentJam.UrlSlug)
|
||||||
tmpl.CurrentJamLink = hmnurl.BuildJamIndexAny(currentJam.UrlSlug)
|
tmpl.CurrentJamLink = hmnurl.BuildJamIndexAny(currentJam.UrlSlug)
|
||||||
tmpl.CurrentJamSlug = currentJam.UrlSlug
|
tmpl.CurrentJamSlug = currentJam.UrlSlug
|
||||||
|
@ -195,12 +195,12 @@ func ProjectIndex(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
FirstUrl: hmnurl.BuildProjectIndex(1, cat),
|
FirstUrl: hmnurl.BuildProjectIndex(1, cat),
|
||||||
LastUrl: hmnurl.BuildProjectIndex(numPages, cat),
|
LastUrl: hmnurl.BuildProjectIndex(numPages, cat),
|
||||||
NextUrl: hmnurl.BuildProjectIndex(utils.IntClamp(1, page+1, numPages), cat),
|
NextUrl: hmnurl.BuildProjectIndex(utils.Clamp(1, page+1, numPages), cat),
|
||||||
PreviousUrl: hmnurl.BuildProjectIndex(utils.IntClamp(1, page-1, numPages), cat),
|
PreviousUrl: hmnurl.BuildProjectIndex(utils.Clamp(1, page-1, numPages), cat),
|
||||||
}
|
}
|
||||||
|
|
||||||
firstProjectIndex := (page - 1) * projectsPerPage
|
firstProjectIndex := (page - 1) * projectsPerPage
|
||||||
endIndex := utils.IntMin(firstProjectIndex+projectsPerPage, len(projects))
|
endIndex := utils.Min(firstProjectIndex+projectsPerPage, len(projects))
|
||||||
pageProjects := projects[firstProjectIndex:endIndex]
|
pageProjects := projects[firstProjectIndex:endIndex]
|
||||||
|
|
||||||
baseData := getBaseData(c, "Projects", []templates.Breadcrumb{
|
baseData := getBaseData(c, "Projects", []templates.Breadcrumb{
|
||||||
|
@ -474,7 +474,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
templateData.RecentActivity = templateData.RecentActivity[:utils.IntMin(len(templateData.RecentActivity)-1, maxRecentActivity)]
|
templateData.RecentActivity = utils.ClampSlice(templateData.RecentActivity, maxRecentActivity)
|
||||||
|
|
||||||
followUrl := ""
|
followUrl := ""
|
||||||
following := false
|
following := false
|
||||||
|
|
|
@ -75,44 +75,50 @@ func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.Us
|
||||||
|
|
||||||
perf.StartBlock("TIMELINE", "Fetch timeline data")
|
perf.StartBlock("TIMELINE", "Fetch timeline data")
|
||||||
if len(q.UserIDs) > 0 || len(q.ProjectIDs) > 0 {
|
if len(q.UserIDs) > 0 || len(q.ProjectIDs) > 0 {
|
||||||
users, err := hmndata.FetchUsers(ctx, conn, currentUser, hmndata.UsersQuery{
|
var err error
|
||||||
UserIDs: q.UserIDs,
|
|
||||||
})
|
if len(q.UserIDs) > 0 {
|
||||||
if err != nil {
|
users, err = hmndata.FetchUsers(ctx, conn, currentUser, hmndata.UsersQuery{
|
||||||
return nil, oops.New(err, "failed to fetch users")
|
UserIDs: q.UserIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to fetch users")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE(asaf): Clear out invalid users in case we banned someone after they got followed
|
// NOTE(asaf): Clear out invalid users in case we banned someone after they got followed
|
||||||
q.UserIDs = q.UserIDs[0:0]
|
validUserIDs := make([]int, 0, len(q.UserIDs))
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
q.UserIDs = append(q.UserIDs, u.ID)
|
validUserIDs = append(validUserIDs, u.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
projects, err = hmndata.FetchProjects(ctx, conn, currentUser, hmndata.ProjectsQuery{
|
if len(q.ProjectIDs) > 0 {
|
||||||
ProjectIDs: q.ProjectIDs,
|
projects, err = hmndata.FetchProjects(ctx, conn, currentUser, hmndata.ProjectsQuery{
|
||||||
})
|
ProjectIDs: q.ProjectIDs,
|
||||||
if err != nil {
|
})
|
||||||
return nil, oops.New(err, "failed to fetch projects")
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to fetch projects")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE(asaf): The original projectIDs might container hidden/abandoned projects,
|
// NOTE(asaf): The original projectIDs might contain hidden/abandoned projects,
|
||||||
// so we recreate it after the projects get filtered by FetchProjects.
|
// so we recreate it after the projects get filtered by FetchProjects.
|
||||||
q.ProjectIDs = q.ProjectIDs[0:0]
|
validProjectIDs := make([]int, 0, len(q.ProjectIDs))
|
||||||
for _, p := range projects {
|
for _, p := range projects {
|
||||||
q.ProjectIDs = append(q.ProjectIDs, p.Project.ID)
|
validProjectIDs = append(validProjectIDs, p.Project.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
snippets, err = hmndata.FetchSnippets(ctx, conn, currentUser, hmndata.SnippetQuery{
|
snippets, err = hmndata.FetchSnippets(ctx, conn, currentUser, hmndata.SnippetQuery{
|
||||||
OwnerIDs: q.UserIDs,
|
OwnerIDs: validUserIDs,
|
||||||
ProjectIDs: q.ProjectIDs,
|
ProjectIDs: validProjectIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, oops.New(err, "failed to fetch user snippets")
|
return nil, oops.New(err, "failed to fetch user snippets")
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err = hmndata.FetchPosts(ctx, conn, currentUser, hmndata.PostsQuery{
|
posts, err = hmndata.FetchPosts(ctx, conn, currentUser, hmndata.PostsQuery{
|
||||||
UserIDs: q.UserIDs,
|
UserIDs: validUserIDs,
|
||||||
ProjectIDs: q.ProjectIDs,
|
ProjectIDs: validProjectIDs,
|
||||||
SortDescending: true,
|
SortDescending: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -120,8 +126,8 @@ func FetchTimeline(ctx context.Context, conn db.ConnOrTx, currentUser *models.Us
|
||||||
}
|
}
|
||||||
|
|
||||||
streamers, err = hmndata.FetchTwitchStreamers(ctx, conn, hmndata.TwitchStreamersQuery{
|
streamers, err = hmndata.FetchTwitchStreamers(ctx, conn, hmndata.TwitchStreamersQuery{
|
||||||
UserIDs: q.UserIDs,
|
UserIDs: validUserIDs,
|
||||||
ProjectIDs: q.ProjectIDs,
|
ProjectIDs: validProjectIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, oops.New(err, "failed to fetch streamers")
|
return nil, oops.New(err, "failed to fetch streamers")
|
||||||
|
|
Loading…
Reference in New Issue