diff --git a/db/features.go b/db/features.go index 45e8e9e36..a2423632e 100644 --- a/db/features.go +++ b/db/features.go @@ -3,6 +3,8 @@ package db import ( "errors" "fmt" + "github.com/google/uuid" + "gorm.io/gorm" "net/http" "strings" "time" @@ -391,3 +393,36 @@ func (db database) UpdateFeatureStatus(uuid string, status FeatureStatus) (Works db.db.Where("uuid = ?", uuid).First(&feature) return feature, nil } + +func (db database) SaveNotification(pubkey, event, content, status string) error { + notification := Notification{ + UUID: uuid.New().String(), + PubKey: pubkey, + Event: event, + Content: content, + Status: NotificationStatus(status), + } + + result := db.db.Create(¬ification) + if result.Error != nil { + return fmt.Errorf("error saving notification: %v", result.Error) + } + + return nil +} + +func (db database) GetNotificationsByStatus(status string) []Notification { + var notifications []Notification + db.db.Where("status = ?", status).Find(¬ifications) + return notifications +} + +func (db database) IncrementNotificationRetry(notificationUUID string) { + db.db.Model(&Notification{}).Where("uuid = ?", notificationUUID). + Update("retries", gorm.Expr("retries + 1")) +} + +func (db database) UpdateNotificationStatus(notificationUUID string, status string) { + db.db.Model(&Notification{}).Where("uuid = ?", notificationUUID). + Updates(map[string]interface{}{"status": status, "updated_at": time.Now()}) +} diff --git a/db/interface.go b/db/interface.go index fe6b0c41d..c58ca071d 100644 --- a/db/interface.go +++ b/db/interface.go @@ -284,6 +284,10 @@ type Database interface { DeleteTicketGroup(TicketGroupUUID uuid.UUID) error PauseBountyTiming(bountyID uint) error ResumeBountyTiming(bountyID uint) error + SaveNotification(pubkey, event, content, status string) error + GetNotificationsByStatus(status string) []Notification + IncrementNotificationRetry(notificationUUID string) + UpdateNotificationStatus(notificationUUID string, status string) CreateOrEditTicketPlan(plan *TicketPlan) (*TicketPlan, error) GetTicketPlan(uuid string) (*TicketPlan, error) DeleteTicketPlan(uuid string) error diff --git a/handlers/bounty.go b/handlers/bounty.go index 54d405718..d30c13908 100644 --- a/handlers/bounty.go +++ b/handlers/bounty.go @@ -215,6 +215,113 @@ func (h *bountyHandler) GetPersonAssignedBounties(w http.ResponseWriter, r *http } } +func getContactKey(pubkey string) (*string, error) { + url := fmt.Sprintf("%s/contact/%s", config.V2BotUrl, pubkey) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + req.Header.Set("x-admin-token", config.V2BotToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error fetching contact: %v", err) + } + defer resp.Body.Close() + + var contactResp struct { + ContactKey *string `json:"contact_key"` + } + if err := json.NewDecoder(resp.Body).Decode(&contactResp); err != nil { + return nil, fmt.Errorf("error decoding contact response: %v", err) + } + + return contactResp.ContactKey, nil +} + +func ProcessWaitingNotifications() { + notifications := db.DB.GetNotificationsByStatus("WAITING_KEY_EXCHANGE") + + for _, n := range notifications { + contactKey, err := getContactKey(n.PubKey) + if err != nil { + logger.Log.Error("Error checking contact key for pubkey %s: %v", n.PubKey, err) + db.DB.IncrementNotificationRetry(n.UUID) + continue + } + + if contactKey == nil { + db.DB.IncrementNotificationRetry(n.UUID) + continue + } + + // Contact key is available, proceed with sending + sendRespStatus := sendNotification(n.PubKey, n.Content) + db.DB.UpdateNotificationStatus(n.UUID, sendRespStatus) + } +} + +func sendNotification(pubkey, content string) string { + sendURL := fmt.Sprintf("%s/send", config.V2BotUrl) + msgBody, _ := json.Marshal(map[string]interface{}{ + "dest": pubkey, + "amt_msat": 0, + "content": content, + "is_tribe": false, + "wait": true, + }) + + req, err := http.NewRequest(http.MethodPost, sendURL, bytes.NewBuffer(msgBody)) + if err != nil { + logger.Log.Error("Error creating send request: %v", err) + return "FAILED" + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-admin-token", config.V2BotToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + logger.Log.Error("Error sending notification: %v", err) + return "FAILED" + } + defer resp.Body.Close() + + var sendResp struct { + Status string `json:"status"` + } + if err := json.NewDecoder(resp.Body).Decode(&sendResp); err != nil { + logger.Log.Error("Error decoding send response: %v", err) + return "FAILED" + } + + return sendResp.Status +} + +func processNotification(pubkey, event, content, alias string) string { + contactKey, err := getContactKey(pubkey) + if err != nil { + logger.Log.Error("Error checking contact key: %v", err) + return "FAILED" + } + + if contactKey == nil { + addContactURL := fmt.Sprintf("%s/add_contact", config.V2BotUrl) + body, _ := json.Marshal(map[string]string{"contact_info": pubkey, "alias": alias}) + req, _ := http.NewRequest(http.MethodPost, addContactURL, bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-admin-token", config.V2BotToken) + http.DefaultClient.Do(req) + + contactKey, err = getContactKey(pubkey) + if err != nil || contactKey == nil { + db.DB.SaveNotification(pubkey, event, content, "WAITING_KEY_EXCHANGE") + return "FAILED" + } + } + + return sendNotification(pubkey, content) +} + func (h *bountyHandler) CreateOrEditBounty(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) @@ -285,6 +392,9 @@ func (h *bountyHandler) CreateOrEditBounty(w http.ResponseWriter, r *http.Reques handleTimingError(w, "start_timing", err) } } + + msg := fmt.Sprintf("You have been assigned a new ticket: %s.", bounty.Title) + processNotification(bounty.Assignee, "bounty_assigned", msg, user.OwnerAlias) } if bounty.Tribe == "" { diff --git a/main.go b/main.go index 58843ddf5..01e976c06 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,7 @@ func main() { func runCron() { c := cron.New() c.AddFunc("@every 0h30m0s", handlers.InitV2PaymentsCron) + c.AddFunc("@every 0h0m30s", handlers.ProcessWaitingNotifications) c.Start() } diff --git a/mocks/Database.go b/mocks/Database.go index dff6faca0..38adac824 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -14682,4 +14682,168 @@ func (_c *Database_GetBountiesByWorkspaceAndTimeRange_Call) Return(_a0 []db.NewB func (_c *Database_GetBountiesByWorkspaceAndTimeRange_Call) RunAndReturn(run func(string, time.Time, time.Time) ([]db.NewBounty, error)) *Database_GetBountiesByWorkspaceAndTimeRange_Call { _c.Call.Return(run) return _c -} \ No newline at end of file +} + +// SaveNotification provides a mock function with given fields: pubkey, event, content, status +func (_m *Database) SaveNotification(pubkey string, event string, content string, status string) error { + ret := _m.Called(pubkey, event, content, status) + + if len(ret) == 0 { + panic("no return value specified for SaveNotification") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok { + r0 = rf(pubkey, event, content, status) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Database_SaveNotification_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveNotification' +type Database_SaveNotification_Call struct { + *mock.Call +} + +// SaveNotification is a helper method to define mock.On call +// - pubkey string +// - event string +// - content string +// - status string +func (_e *Database_Expecter) SaveNotification(pubkey interface{}, event interface{}, content interface{}, status interface{}) *Database_SaveNotification_Call { + return &Database_SaveNotification_Call{Call: _e.mock.On("SaveNotification", pubkey, event, content, status)} +} + +func (_c *Database_SaveNotification_Call) Run(run func(pubkey string, event string, content string, status string)) *Database_SaveNotification_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *Database_SaveNotification_Call) Return(_a0 error) *Database_SaveNotification_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_SaveNotification_Call) RunAndReturn(run func(string, string, string, string) error) *Database_SaveNotification_Call { + _c.Call.Return(run) + return _c +} + +// GetNotificationsByStatus provides a mock function with given fields: status +func (_m *Database) GetNotificationsByStatus(status string) []db.Notification { + ret := _m.Called(status) + + if len(ret) == 0 { + panic("no return value specified for GetNotificationsByStatus") + } + + var r0 []db.Notification + if rf, ok := ret.Get(0).(func(string) []db.Notification); ok { + r0 = rf(status) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]db.Notification) + } + } + + return r0 +} + +// Database_GetNotificationsByStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNotificationsByStatus' +type Database_GetNotificationsByStatus_Call struct { + *mock.Call +} + +// GetNotificationsByStatus is a helper method to define mock.On call +// - status string +func (_e *Database_Expecter) GetNotificationsByStatus(status interface{}) *Database_GetNotificationsByStatus_Call { + return &Database_GetNotificationsByStatus_Call{Call: _e.mock.On("GetNotificationsByStatus", status)} +} + +func (_c *Database_GetNotificationsByStatus_Call) Run(run func(status string)) *Database_GetNotificationsByStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Database_GetNotificationsByStatus_Call) Return(_a0 []db.Notification) *Database_GetNotificationsByStatus_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_GetNotificationsByStatus_Call) RunAndReturn(run func(string) []db.Notification) *Database_GetNotificationsByStatus_Call { + _c.Call.Return(run) + return _c +} + +// IncrementNotificationRetry provides a mock function with given fields: notificationUUID +func (_m *Database) IncrementNotificationRetry(notificationUUID string) { + _m.Called(notificationUUID) +} + +// Database_IncrementNotificationRetry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncrementNotificationRetry' +type Database_IncrementNotificationRetry_Call struct { + *mock.Call +} + +// IncrementNotificationRetry is a helper method to define mock.On call +// - notificationUUID string +func (_e *Database_Expecter) IncrementNotificationRetry(notificationUUID interface{}) *Database_IncrementNotificationRetry_Call { + return &Database_IncrementNotificationRetry_Call{Call: _e.mock.On("IncrementNotificationRetry", notificationUUID)} +} + +func (_c *Database_IncrementNotificationRetry_Call) Run(run func(notificationUUID string)) *Database_IncrementNotificationRetry_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Database_IncrementNotificationRetry_Call) Return() *Database_IncrementNotificationRetry_Call { + _c.Call.Return() + return _c +} + +func (_c *Database_IncrementNotificationRetry_Call) RunAndReturn(run func(string)) *Database_IncrementNotificationRetry_Call { + _c.Call.Return(run) + return _c +} + +// UpdateNotificationStatus provides a mock function with given fields: notificationUUID, status +func (_m *Database) UpdateNotificationStatus(notificationUUID string, status string) { + _m.Called(notificationUUID, status) +} + +// Database_UpdateNotificationStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateNotificationStatus' +type Database_UpdateNotificationStatus_Call struct { + *mock.Call +} + +// UpdateNotificationStatus is a helper method to define mock.On call +// - notificationUUID string +// - status string +func (_e *Database_Expecter) UpdateNotificationStatus(notificationUUID interface{}, status interface{}) *Database_UpdateNotificationStatus_Call { + return &Database_UpdateNotificationStatus_Call{Call: _e.mock.On("UpdateNotificationStatus", notificationUUID, status)} +} + +func (_c *Database_UpdateNotificationStatus_Call) Run(run func(notificationUUID string, status string)) *Database_UpdateNotificationStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *Database_UpdateNotificationStatus_Call) Return() *Database_UpdateNotificationStatus_Call { + _c.Call.Return() + return _c +} + +func (_c *Database_UpdateNotificationStatus_Call) RunAndReturn(run func(string, string)) *Database_UpdateNotificationStatus_Call { + _c.Call.Return(run) + return _c +}