diff --git a/backend/app/notify/notify.go b/backend/app/notify/notify.go index 7157bdb6..8766089e 100644 --- a/backend/app/notify/notify.go +++ b/backend/app/notify/notify.go @@ -16,7 +16,7 @@ import ( type Service struct { dataService Store destinations []Destination - queue chan request + queue chan Request closed uint32 // non-zero means closed. uses uint instead of bool for atomic ctx context.Context @@ -26,7 +26,7 @@ type Service struct { // Destination defines interface for a given destination service, like telegram, email and so on type Destination interface { fmt.Stringer - Send(ctx context.Context, req request) error + Send(ctx context.Context, req Request) error } // Store defines the minimal interface accessing stored comments used by notifier @@ -34,8 +34,8 @@ type Store interface { Get(locator store.Locator, id string, user store.User) (store.Comment, error) } -type request struct { - comment store.Comment +type Request struct { + Comment store.Comment parent store.Comment } @@ -50,7 +50,7 @@ func NewService(dataService Store, size int, destinations ...Destination) *Servi ctx, cancel := context.WithCancel(context.Background()) res := Service{ dataService: dataService, - queue: make(chan request, size), + queue: make(chan Request, size), destinations: destinations, ctx: ctx, cancel: cancel, @@ -62,21 +62,20 @@ func NewService(dataService Store, size int, destinations ...Destination) *Servi return &res } -// Submit comment to internal channel if not busy, drop if can't send -func (s *Service) Submit(comment store.Comment) { +// Submit Request to internal channel if not busy, drop if can't send +func (s *Service) Submit(req Request) { if len(s.destinations) == 0 || atomic.LoadUint32(&s.closed) != 0 { return } - parentComment := store.Comment{} if s.dataService != nil { - if p, err := s.dataService.Get(comment.Locator, comment.ParentID, store.User{}); err == nil { - parentComment = p + if p, err := s.dataService.Get(req.Comment.Locator, req.Comment.ParentID, store.User{}); err == nil { + req.parent = p } } select { - case s.queue <- request{comment: comment, parent: parentComment}: + case s.queue <- req: default: - log.Printf("[WARN] can't send comment notification to queue, %+v", comment) + log.Printf("[WARN] can't send comment notification to queue, %+v", req.Comment) } } diff --git a/backend/app/notify/notify_test.go b/backend/app/notify/notify_test.go index 2dfbc632..bddc625b 100644 --- a/backend/app/notify/notify_test.go +++ b/backend/app/notify/notify_test.go @@ -19,9 +19,9 @@ import ( func TestService_NoDestinations(t *testing.T) { s := NewService(nil, 1) assert.NotNil(t, s) - s.Submit(store.Comment{ID: "123"}) - s.Submit(store.Comment{ID: "123"}) - s.Submit(store.Comment{ID: "123"}) + s.Submit(Request{Comment: store.Comment{ID: "123"}}) + s.Submit(Request{Comment: store.Comment{ID: "123"}}) + s.Submit(Request{Comment: store.Comment{ID: "123"}}) s.Close() } @@ -30,20 +30,20 @@ func TestService_WithDestinations(t *testing.T) { s := NewService(nil, 1, d1, d2) assert.NotNil(t, s) - s.Submit(store.Comment{ID: "100"}) + s.Submit(Request{Comment: store.Comment{ID: "100"}}) time.Sleep(time.Millisecond * 110) - s.Submit(store.Comment{ID: "101"}) + s.Submit(Request{Comment: store.Comment{ID: "101"}}) time.Sleep(time.Millisecond * 110) - s.Submit(store.Comment{ID: "102"}) + s.Submit(Request{Comment: store.Comment{ID: "102"}}) time.Sleep(time.Millisecond * 110) s.Close() assert.Equal(t, 3, len(d1.get()), "got all comments to d1") assert.Equal(t, 3, len(d2.get()), "got all comments to d2") - assert.Equal(t, "100", d1.get()[0].comment.ID) - assert.Equal(t, "101", d1.get()[1].comment.ID) - assert.Equal(t, "102", d1.get()[2].comment.ID) + assert.Equal(t, "100", d1.get()[0].Comment.ID) + assert.Equal(t, "101", d1.get()[1].Comment.ID) + assert.Equal(t, "102", d1.get()[2].Comment.ID) } func TestService_WithDrops(t *testing.T) { @@ -51,14 +51,14 @@ func TestService_WithDrops(t *testing.T) { s := NewService(nil, 1, d1, d2) assert.NotNil(t, s) - s.Submit(store.Comment{ID: "100"}) - s.Submit(store.Comment{ID: "101"}) + s.Submit(Request{Comment: store.Comment{ID: "100"}}) + s.Submit(Request{Comment: store.Comment{ID: "101"}}) time.Sleep(time.Millisecond * 110) - s.Submit(store.Comment{ID: "102"}) + s.Submit(Request{Comment: store.Comment{ID: "102"}}) time.Sleep(time.Millisecond * 110) s.Close() - s.Submit(store.Comment{ID: "111"}) // safe to send after close + s.Submit(Request{Comment: store.Comment{ID: "111"}}) // safe to send after close assert.Equal(t, 2, len(d1.get()), "one comment dropped from d1") assert.Equal(t, 2, len(d2.get()), "one comment dropped from d2") @@ -70,7 +70,7 @@ func TestService_Many(t *testing.T) { assert.NotNil(t, s) for i := 0; i < 10; i++ { - s.Submit(store.Comment{ID: fmt.Sprintf("%d", 100+i)}) + s.Submit(Request{Comment: store.Comment{ID: fmt.Sprintf("%d", 100+i)}}) time.Sleep(time.Millisecond * time.Duration(rand.Int31n(200))) } s.Close() @@ -93,41 +93,41 @@ func TestService_WithParent(t *testing.T) { s := NewService(dataStore, 1, dest) assert.NotNil(t, s) - s.Submit(store.Comment{ID: "c1", ParentID: "p1"}) + s.Submit(Request{Comment: store.Comment{ID: "c1", ParentID: "p1"}}) time.Sleep(time.Millisecond * 110) - s.Submit(store.Comment{ID: "c11", ParentID: "p11"}) + s.Submit(Request{Comment: store.Comment{ID: "c11", ParentID: "p11"}}) time.Sleep(time.Millisecond * 110) s.Close() destRes := dest.get() assert.Equal(t, 2, len(destRes), "two comment notified") - assert.Equal(t, "p1", destRes[0].comment.ParentID) + assert.Equal(t, "p1", destRes[0].Comment.ParentID) assert.Equal(t, "p1", destRes[0].parent.ID) - assert.Equal(t, "p11", destRes[1].comment.ParentID) + assert.Equal(t, "p11", destRes[1].Comment.ParentID) assert.Equal(t, "", destRes[1].parent.ID) } func TestService_Nop(t *testing.T) { s := NopService - s.Submit(store.Comment{}) + s.Submit(Request{Comment: store.Comment{}}) s.Close() assert.Equal(t, uint32(1), atomic.LoadUint32(&s.closed)) } type mockDest struct { - data []request + data []Request id int closed bool lock sync.Mutex } -func (m *mockDest) Send(ctx context.Context, r request) error { +func (m *mockDest) Send(ctx context.Context, r Request) error { m.lock.Lock() defer m.lock.Unlock() select { case <-time.After(100 * time.Millisecond): m.data = append(m.data, r) - log.Printf("sent %s -> %d", r.comment.ID, m.id) + log.Printf("sent %s -> %d", r.Comment.ID, m.id) case <-ctx.Done(): log.Printf("ctx closed %d", m.id) m.closed = true @@ -135,10 +135,10 @@ func (m *mockDest) Send(ctx context.Context, r request) error { return nil } -func (m *mockDest) get() []request { +func (m *mockDest) get() []Request { m.lock.Lock() defer m.lock.Unlock() - res := make([]request, len(m.data)) + res := make([]Request, len(m.data)) copy(res, m.data) return res } diff --git a/backend/app/notify/telegram.go b/backend/app/notify/telegram.go index 20f9bee3..83bf532a 100644 --- a/backend/app/notify/telegram.go +++ b/backend/app/notify/telegram.go @@ -85,23 +85,23 @@ func NewTelegram(token string, channelID string, timeout time.Duration, api stri } // Send to telegram channel -func (t *Telegram) Send(ctx context.Context, req request) error { +func (t *Telegram) Send(ctx context.Context, req Request) error { client := http.Client{Timeout: telegramTimeOut} - log.Printf("[DEBUG] send telegram notification to %s, comment id %s", t.channelID, req.comment.ID) + log.Printf("[DEBUG] send telegram notification to %s, comment id %s", t.channelID, req.Comment.ID) - from := req.comment.User.Name - if req.comment.ParentID != "" { + from := req.Comment.User.Name + if req.Comment.ParentID != "" { from += " → " + req.parent.User.Name } from = "*" + from + "*" - link := fmt.Sprintf("↦ [original comment](%s)", req.comment.Locator.URL+uiNav+req.comment.ID) - if req.comment.PostTitle != "" { - link = fmt.Sprintf("↦ [%s](%s)", req.comment.PostTitle, req.comment.Locator.URL+uiNav+req.comment.ID) + link := fmt.Sprintf("↦ [original comment](%s)", req.Comment.Locator.URL+uiNav+req.Comment.ID) + if req.Comment.PostTitle != "" { + link = fmt.Sprintf("↦ [%s](%s)", req.Comment.PostTitle, req.Comment.Locator.URL+uiNav+req.Comment.ID) } u := fmt.Sprintf("%s%s/sendMessage?chat_id=%s&parse_mode=Markdown&disable_web_page_preview=true", t.apiPrefix, t.token, t.channelID) - msg := fmt.Sprintf("%s\n\n%s\n\n%s", from, req.comment.Orig, link) + msg := fmt.Sprintf("%s\n\n%s\n\n%s", from, req.Comment.Orig, link) msg = html.UnescapeString(msg) body := struct { Text string `json:"text"` diff --git a/backend/app/notify/telegram_test.go b/backend/app/notify/telegram_test.go index 35dd2a21..2dd3fdd6 100644 --- a/backend/app/notify/telegram_test.go +++ b/backend/app/notify/telegram_test.go @@ -63,15 +63,15 @@ func TestTelegram_Send(t *testing.T) { cp := store.Comment{Text: "some parent text"} cp.User.Name = "to" - err = tb.Send(context.TODO(), request{comment: c, parent: cp}) + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) assert.NoError(t, err) c.PostTitle = "test title" - err = tb.Send(context.TODO(), request{comment: c, parent: cp}) + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) assert.NoError(t, err) tb, err = NewTelegram("non-json-resp", "remark_test", 2*time.Second, ts.URL+"/") assert.NotNil(t, err, "should failed") - err = tb.Send(context.TODO(), request{comment: c, parent: cp}) + err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) require.NotNil(t, err) assert.Contains(t, err.Error(), "unexpected telegram status code 404", "send on broken tg") diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index f8945e9b..2362139a 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -109,7 +109,7 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) { Scopes(comment.Locator.URL, lastCommentsScope, comment.User.ID, comment.Locator.SiteID)) if s.notifyService != nil { - s.notifyService.Submit(finalComment) + s.notifyService.Submit(notify.Request{Comment: finalComment}) } log.Printf("[DEBUG] created commend %+v", finalComment)