@@ -16,6 +16,7 @@ import (
16
16
"log/slog"
17
17
"net"
18
18
"net/http"
19
+ "net/url"
19
20
"os"
20
21
"runtime"
21
22
"strings"
@@ -130,7 +131,7 @@ func xaddUserLogf(tx *bstore.Tx, userID int64, format string, args ...any) {
130
131
type API struct {}
131
132
132
133
func xrandomID (n int ) string {
133
- return base64 .RawURLEncoding .EncodeToString (xrandom (16 ))
134
+ return base64 .RawURLEncoding .EncodeToString (xrandom (n ))
134
135
}
135
136
136
137
func xrandom (n int ) []byte {
@@ -446,6 +447,12 @@ func (API) UserRemove(ctx context.Context) {
446
447
_ , err = bstore.QueryTx [Message ](tx ).FilterNonzero (Message {UserID : user .ID }).Delete ()
447
448
xcheckf (err , "removing user messages" )
448
449
450
+ _ , err = bstore.QueryTx [Hook ](tx ).FilterNonzero (Hook {UserID : user .ID }).Delete ()
451
+ xcheckf (err , "removing user webhook calls" )
452
+
453
+ _ , err = bstore.QueryTx [HookConfig ](tx ).FilterNonzero (HookConfig {UserID : user .ID }).Delete ()
454
+ xcheckf (err , "removing user webhook configs" )
455
+
449
456
err = tx .Delete (& user )
450
457
xcheckf (err , "removing user" )
451
458
@@ -809,9 +816,16 @@ type Overview struct {
809
816
810
817
Subscriptions []Subscription
811
818
ModuleUpdates []ModuleUpdateURLs
819
+ HookConfigs []HookConfig
820
+ RecentHooks []UpdateHook
812
821
UserLogs []UserLog
813
822
}
814
823
824
+ type UpdateHook struct {
825
+ Update ModuleUpdate
826
+ Hook Hook
827
+ }
828
+
815
829
type ModuleUpdateURLs struct {
816
830
ModuleUpdate
817
831
RepoURL string
@@ -836,18 +850,28 @@ func (API) Overview(ctx context.Context) (overview Overview) {
836
850
overview .MetaUnsubscribed = u .MetaUnsubscribed
837
851
overview .UpdatesUnsubscribed = u .UpdatesUnsubscribed
838
852
839
- overview .Subscriptions , err = bstore .QueryDB [Subscription ](ctx , database ).FilterNonzero (Subscription {UserID : reqInfo .UserID }).SortAsc ("ID" ).List ()
853
+ overview .Subscriptions , err = bstore.QueryTx [Subscription ](tx ).FilterNonzero (Subscription {UserID : reqInfo .UserID }).SortAsc ("ID" ).List ()
840
854
xcheckf (err , "listing subscriptions" )
841
855
842
- modups , err := bstore .QueryDB [ModuleUpdate ](ctx , database ).FilterNonzero (ModuleUpdate {UserID : reqInfo .UserID }).SortDesc ("ID" ).Limit (50 ).List ()
856
+ modups , err := bstore.QueryTx [ModuleUpdate ](tx ).FilterNonzero (ModuleUpdate {UserID : reqInfo .UserID }).SortDesc ("ID" ).Limit (50 ).List ()
843
857
xcheckf (err , "listing module updates" )
844
858
overview .ModuleUpdates = make ([]ModuleUpdateURLs , len (modups ))
845
859
for i , modup := range modups {
846
860
repoURL , tagURL , docURL := guessURLs (modup .Module , modup .Version )
847
861
overview .ModuleUpdates [i ] = ModuleUpdateURLs {modup , repoURL , tagURL , docURL }
848
862
}
849
863
850
- overview .UserLogs , err = bstore .QueryDB [UserLog ](ctx , database ).FilterNonzero (UserLog {UserID : reqInfo .UserID }).SortDesc ("ID" ).Limit (50 ).List ()
864
+ overview .HookConfigs , err = bstore.QueryTx [HookConfig ](tx ).FilterNonzero (HookConfig {UserID : reqInfo .UserID }).SortAsc ("ID" ).List ()
865
+ xcheckf (err , "listing hook configs" )
866
+
867
+ err = bstore.QueryTx [Hook ](tx ).FilterNonzero (Hook {UserID : reqInfo .UserID }).SortDesc ("NextAttempt" ).Limit (100 ).ForEach (func (h Hook ) error {
868
+ mu , err := bstore.QueryTx [ModuleUpdate ](tx ).FilterNonzero (ModuleUpdate {HookID : h .ID }).Get ()
869
+ overview .RecentHooks = append (overview .RecentHooks , UpdateHook {mu , h })
870
+ return err
871
+ })
872
+ xcheckf (err , "listing recent hooks" )
873
+
874
+ overview .UserLogs , err = bstore.QueryTx [UserLog ](tx ).FilterNonzero (UserLog {UserID : reqInfo .UserID }).SortDesc ("ID" ).Limit (50 ).List ()
851
875
xcheckf (err , "listing userlogs" )
852
876
853
877
return nil
@@ -930,6 +954,16 @@ func xcheckModule(m string) {
930
954
}
931
955
}
932
956
957
+ // Check whether hookConfigID is ok for user.
958
+ func xcheckhookconfig (tx * bstore.Tx , userID , hookConfigID int64 ) {
959
+ // Verify it's the user's.
960
+ exists , err := bstore.QueryTx [HookConfig ](tx ).FilterNonzero (HookConfig {UserID : userID , ID : hookConfigID }).Exists ()
961
+ xcheckf (err , "looking up hook config" )
962
+ if ! exists {
963
+ xusererrorf ("no such webhook config" )
964
+ }
965
+ }
966
+
933
967
// SubscriptionCreate adds a new subscription to a module.
934
968
func (API ) SubscriptionCreate (ctx context.Context , sub Subscription ) Subscription {
935
969
reqInfo := ctx .Value (requestInfoCtxKey ).(requestInfo )
@@ -938,8 +972,13 @@ func (API) SubscriptionCreate(ctx context.Context, sub Subscription) Subscriptio
938
972
939
973
sub .ID = 0
940
974
sub .UserID = reqInfo .UserID
941
- err := database .Insert (ctx , & sub )
942
- xusercheckf (err , "inserting new subscription" )
975
+ err := database .Write (ctx , func (tx * bstore.Tx ) error {
976
+ if sub .HookConfigID != 0 {
977
+ xcheckhookconfig (tx , reqInfo .UserID , sub .HookConfigID )
978
+ }
979
+ return tx .Insert (& sub )
980
+ })
981
+ xcheckf (err , "inserting new subscription" )
943
982
return sub
944
983
}
945
984
@@ -950,6 +989,7 @@ type SubscriptionImport struct {
950
989
Prerelease bool
951
990
Pseudo bool
952
991
Comment string
992
+ HookConfigID int64
953
993
Indirect bool
954
994
}
955
995
@@ -962,6 +1002,10 @@ func (API) SubscriptionImport(ctx context.Context, imp SubscriptionImport) (subs
962
1002
xusercheckf (err , "parsing go.mod" )
963
1003
964
1004
err = database .Write (ctx , func (tx * bstore.Tx ) error {
1005
+ if imp .HookConfigID != 0 {
1006
+ xcheckhookconfig (tx , reqInfo .UserID , imp .HookConfigID )
1007
+ }
1008
+
965
1009
for _ , r := range f .Require {
966
1010
if r .Indirect && ! imp .Indirect {
967
1011
continue
@@ -976,12 +1020,13 @@ func (API) SubscriptionImport(ctx context.Context, imp SubscriptionImport) (subs
976
1020
}
977
1021
978
1022
sub := Subscription {
979
- UserID : reqInfo .UserID ,
980
- Module : r .Mod .Path ,
981
- BelowModule : imp .BelowModule ,
982
- Prerelease : imp .Prerelease ,
983
- Pseudo : imp .Pseudo ,
984
- Comment : imp .Comment ,
1023
+ UserID : reqInfo .UserID ,
1024
+ Module : r .Mod .Path ,
1025
+ BelowModule : imp .BelowModule ,
1026
+ Prerelease : imp .Prerelease ,
1027
+ Pseudo : imp .Pseudo ,
1028
+ Comment : imp .Comment ,
1029
+ HookConfigID : imp .HookConfigID ,
985
1030
}
986
1031
err = tx .Insert (& sub )
987
1032
xusercheckf (err , "inserting new subscription" )
@@ -1005,12 +1050,16 @@ func (API) SubscriptionSave(ctx context.Context, sub Subscription) {
1005
1050
xcheckModule (sub .Module )
1006
1051
1007
1052
err := database .Write (ctx , func (tx * bstore.Tx ) error {
1008
- exists , err := bstore.QueryTx [Subscription ](tx ).FilterNonzero (Subscription {ID : sub .ID , UserID : reqInfo .UserID }).Limit ( 1 ). Exists ()
1053
+ exists , err := bstore.QueryTx [Subscription ](tx ).FilterNonzero (Subscription {ID : sub .ID , UserID : reqInfo .UserID }).Exists ()
1009
1054
xcheckf (err , "get subscription" )
1010
1055
if ! exists {
1011
1056
xusererrorf ("no such subscription" )
1012
1057
}
1013
1058
1059
+ if sub .HookConfigID != 0 {
1060
+ xcheckhookconfig (tx , reqInfo .UserID , sub .HookConfigID )
1061
+ }
1062
+
1014
1063
sub .UserID = reqInfo .UserID
1015
1064
err = tx .Update (& sub )
1016
1065
xcheckf (err , "updating subscription" )
@@ -1219,3 +1268,154 @@ func (API) TestSend(ctx context.Context, secret, kind, email string) {
1219
1268
err = smtpSubmit (ctx , smtpconn , false , mailFrom , u .Email , msg , eightbit , smtputf8 )
1220
1269
xcheckf (err , "submit message" )
1221
1270
}
1271
+
1272
+ func xcheckhookurl (s string ) {
1273
+ u , err := url .Parse (s )
1274
+ xusercheckf (err , "parsing url" )
1275
+ if u .Scheme != "http" && u .Scheme != "https" {
1276
+ xusererrorf ("scheme %q not allowed, use https or http" , u .Scheme )
1277
+ }
1278
+ }
1279
+
1280
+ // todo: should we require an opt-in before we start making requests? e.g. require that an endpoint returns certain data we specify.
1281
+
1282
+ func (API ) HookConfigAdd (ctx context.Context , hc HookConfig ) (nhc HookConfig ) {
1283
+ reqInfo := ctx .Value (requestInfoCtxKey ).(requestInfo )
1284
+
1285
+ err := database .Write (ctx , func (tx * bstore.Tx ) error {
1286
+ hc .ID = 0
1287
+ hc .UserID = reqInfo .UserID
1288
+ xcheckhookurl (hc .URL )
1289
+ if err := tx .Insert (& hc ); err != nil {
1290
+ return err
1291
+ }
1292
+ nhc = hc
1293
+ return nil
1294
+ })
1295
+ if err != nil && errors .Is (err , bstore .ErrUnique ) {
1296
+ xusererrorf ("config not unique" )
1297
+ }
1298
+ xcheckf (err , "add hook config" )
1299
+ return
1300
+ }
1301
+
1302
+ func (API ) HookConfigSave (ctx context.Context , hc HookConfig ) {
1303
+ reqInfo := ctx .Value (requestInfoCtxKey ).(requestInfo )
1304
+
1305
+ err := database .Write (ctx , func (tx * bstore.Tx ) error {
1306
+ if hc .ID == 0 {
1307
+ xusererrorf ("missing hook config id" )
1308
+ }
1309
+ xcheckhookurl (hc .URL )
1310
+
1311
+ ohc , err := bstore.QueryTx [HookConfig ](tx ).FilterNonzero (HookConfig {ID : hc .ID , UserID : reqInfo .UserID }).Get ()
1312
+ if err == bstore .ErrAbsent {
1313
+ xusererrorf ("no such hook config" )
1314
+ }
1315
+ xcheckf (err , "get current hook config" )
1316
+ ohc .Name = hc .Name
1317
+ ohc .URL = hc .URL
1318
+ ohc .Headers = hc .Headers
1319
+ ohc .Disabled = hc .Disabled
1320
+ if err := tx .Update (& ohc ); err != nil {
1321
+ return err
1322
+ }
1323
+ return nil
1324
+ })
1325
+ if err != nil && errors .Is (err , bstore .ErrUnique ) {
1326
+ xusererrorf ("config not unique" )
1327
+ }
1328
+ xcheckf (err , "save hook config" )
1329
+ }
1330
+
1331
+ func (API ) HookConfigRemove (ctx context.Context , hcID int64 ) {
1332
+ reqInfo := ctx .Value (requestInfoCtxKey ).(requestInfo )
1333
+
1334
+ err := database .Write (ctx , func (tx * bstore.Tx ) error {
1335
+ if hcID == 0 {
1336
+ xusererrorf ("missing hook config id" )
1337
+ }
1338
+
1339
+ ohc , err := bstore.QueryTx [HookConfig ](tx ).FilterNonzero (HookConfig {ID : hcID , UserID : reqInfo .UserID }).Get ()
1340
+ if err == bstore .ErrAbsent {
1341
+ xusererrorf ("no such hook config" )
1342
+ }
1343
+ xcheckf (err , "get current hook config" )
1344
+
1345
+ // First remove hooks referencing config.
1346
+ _ , err = bstore.QueryTx [Hook ](tx ).FilterNonzero (Hook {HookConfigID : ohc .ID }).Delete ()
1347
+ xcheckf (err , "removing hooks for config" )
1348
+
1349
+ if err := tx .Delete (& ohc ); err != nil {
1350
+ if errors .Is (err , bstore .ErrReference ) {
1351
+ xusererrorf ("webhook config still in use with subscription" )
1352
+ }
1353
+ return err
1354
+ }
1355
+
1356
+ return nil
1357
+ })
1358
+ xcheckf (err , "remove hook config" )
1359
+ }
1360
+
1361
+ func (API ) HookCancel (ctx context.Context , hID int64 ) (nh Hook ) {
1362
+ reqInfo := ctx .Value (requestInfoCtxKey ).(requestInfo )
1363
+
1364
+ err := database .Write (ctx , func (tx * bstore.Tx ) error {
1365
+ if hID == 0 {
1366
+ xusererrorf ("missing hook id" )
1367
+ }
1368
+
1369
+ oh , err := bstore.QueryTx [Hook ](tx ).FilterNonzero (Hook {ID : hID , UserID : reqInfo .UserID }).Get ()
1370
+ if err == bstore .ErrAbsent {
1371
+ xusererrorf ("no such hook" )
1372
+ }
1373
+ xcheckf (err , "get current hook" )
1374
+
1375
+ if oh .Done {
1376
+ xusererrorf ("hook already done" )
1377
+ }
1378
+
1379
+ oh .Done = true
1380
+ oh .Results = append (oh .Results , HookResult {Error : "Canceled by user" , Start : time .Now ()})
1381
+ oh .NextAttempt = time .Now ()
1382
+ if err := tx .Update (& oh ); err != nil {
1383
+ return err
1384
+ }
1385
+ nh = oh
1386
+ return nil
1387
+ })
1388
+ xcheckf (err , "remove hook" )
1389
+ return
1390
+ }
1391
+
1392
+ func (API ) HookKick (ctx context.Context , hID int64 ) (nh Hook ) {
1393
+ reqInfo := ctx .Value (requestInfoCtxKey ).(requestInfo )
1394
+
1395
+ err := database .Write (ctx , func (tx * bstore.Tx ) error {
1396
+ if hID == 0 {
1397
+ xusererrorf ("missing hook id" )
1398
+ }
1399
+
1400
+ h , err := bstore.QueryTx [Hook ](tx ).FilterNonzero (Hook {ID : hID , UserID : reqInfo .UserID }).Get ()
1401
+ if err == bstore .ErrAbsent {
1402
+ xusererrorf ("no such hook" )
1403
+ }
1404
+ xcheckf (err , "get current hook" )
1405
+
1406
+ if h .Done {
1407
+ xusererrorf ("hook already done" )
1408
+ }
1409
+
1410
+ h .NextAttempt = time .Now ()
1411
+ if err := tx .Update (& h ); err != nil {
1412
+ return err
1413
+ }
1414
+
1415
+ nh = h
1416
+ return nil
1417
+ })
1418
+ xcheckf (err , "update hook" )
1419
+ kickHooksQueue ()
1420
+ return
1421
+ }
0 commit comments