Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UserDetailTelegram support #1038

Merged
merged 1 commit into from
Jun 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion backend/_example/memory_store/accessor/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err erro
// and all site's details listing under the same function (and not to extend engine interface by two separate functions).
func (m *MemData) UserDetail(req engine.UserDetailRequest) ([]engine.UserDetailEntry, error) {
switch req.Detail {
case engine.UserEmail:
case engine.UserEmail, engine.UserTelegram:
if req.UserID == "" {
return nil, errors.New("userid cannot be empty in request for single detail")
}
Expand Down Expand Up @@ -443,6 +443,8 @@ func (m *MemData) getUserDetail(req engine.UserDetailRequest) ([]engine.UserDeta
switch req.Detail {
case engine.UserEmail:
return []engine.UserDetailEntry{{UserID: req.UserID, Email: meta.Details.Email}}, nil
case engine.UserTelegram:
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: meta.Details.Telegram}}, nil
}
}

Expand Down Expand Up @@ -473,6 +475,10 @@ func (m *MemData) setUserDetail(req engine.UserDetailRequest) ([]engine.UserDeta
entry.Details.Email = req.Update
m.metaUsers[req.UserID] = entry
return []engine.UserDetailEntry{{UserID: req.UserID, Email: req.Update}}, nil
case engine.UserTelegram:
entry.Details.Telegram = req.Update
m.metaUsers[req.UserID] = entry
return []engine.UserDetailEntry{{UserID: req.UserID, Telegram: req.Update}}, nil
}

return []engine.UserDetailEntry{}, nil
Expand Down Expand Up @@ -509,6 +515,8 @@ func (m *MemData) deleteUserDetail(locator store.Locator, userID string, userDet
switch userDetail {
case engine.UserEmail:
entry.Details.Email = ""
case engine.UserTelegram:
entry.Details.Telegram = ""
case engine.AllUserDetails:
entry.Details = engine.UserDetailEntry{UserID: userID}
}
Expand Down
52 changes: 43 additions & 9 deletions backend/_example/memory_store/accessor/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,12 +658,40 @@ func TestMemData_DeleteAll(t *testing.T) {
assert.Equal(t, 0, len(comments), "nothing left")
}

func TestMemData_UserDetailAll(t *testing.T) {
b := prepMem(t)

val, err := b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails})
require.NoError(t, err)
require.Nil(t, val)
}

func TestMemData_UserDetailErrors(t *testing.T) {
b := prepMem(t)

val, err := b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.UserEmail, Update: "value1"})
require.EqualError(t, err, "userid cannot be empty in request for single detail")
require.Nil(t, val)

val, err = b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: engine.AllUserDetails, Update: "value1"})
require.EqualError(t, err, "unsupported request with userdetail all")
require.Nil(t, val)

val, err = b.UserDetail(engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, Detail: "bad"})
require.EqualError(t, err, "unsupported detail \"bad\"")
require.Nil(t, val)
}

func TestMemData_DeleteUserDetail(t *testing.T) {
var (
createUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserEmail, Update: "value1"}
readUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserEmail}
emailSet = []engine.UserDetailEntry{{UserID: "user1", Email: "value1"}}
emailUnset = []engine.UserDetailEntry{{UserID: "user1", Email: ""}}
createEmailUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserEmail, Update: "value1"}
readEmailUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserEmail}
createTelegramUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserTelegram, Update: "value1"}
readTelegramUser = engine.UserDetailRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", Detail: engine.UserTelegram}
emailSet = []engine.UserDetailEntry{{UserID: "user1", Email: "value1"}}
emailUnset = []engine.UserDetailEntry{{UserID: "user1", Email: ""}}
telegramSet = []engine.UserDetailEntry{{UserID: "user1", Telegram: "value1"}}
telegramUnset = []engine.UserDetailEntry{{UserID: "user1", Telegram: ""}}
)

b := prepMem(t)
Expand All @@ -674,15 +702,21 @@ func TestMemData_DeleteUserDetail(t *testing.T) {
expected []engine.UserDetailEntry
}{
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserEmail},
detailReq: createUser, expected: emailSet},
detailReq: createEmailUser, expected: emailSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1", UserDetail: engine.UserEmail},
detailReq: readUser, expected: emailSet},
detailReq: readEmailUser, expected: emailSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserEmail},
detailReq: readUser, expected: emailUnset},
detailReq: readEmailUser, expected: emailUnset},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserTelegram},
detailReq: createTelegramUser, expected: telegramSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1", UserDetail: engine.UserTelegram},
detailReq: readTelegramUser, expected: telegramSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.UserTelegram},
detailReq: readTelegramUser, expected: telegramUnset},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.AllUserDetails},
detailReq: createUser, expected: emailSet},
detailReq: createEmailUser, expected: emailSet},
{delReq: engine.DeleteRequest{Locator: store.Locator{SiteID: "test-site"}, UserID: "user1", UserDetail: engine.AllUserDetails},
detailReq: readUser, expected: emailUnset},
detailReq: readEmailUser, expected: emailUnset},
}

for i, x := range testData {
Expand Down
2 changes: 2 additions & 0 deletions backend/_example/memory_store/accessor/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ func TestMemImage_LoadAfterSave(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, gopher, img)

svc.ResetCleanupTimer(id)

err = svc.Commit(id)
assert.NoError(t, err)

Expand Down
39 changes: 26 additions & 13 deletions backend/app/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ type Destination interface {
type Store interface {
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
GetUserEmail(siteID string, userID string) (string, error)
GetUserTelegram(siteID string, userID string) (string, error)
}

// used for email and telegram retrieval from user details
type getUserDetail func(string, string) (string, error)

// Request notification for a Comment
type Request struct {
Comment store.Comment
parent store.Comment
Emails []string
Comment store.Comment
parent store.Comment
Emails []string
Telegrams []string
}

// VerificationRequest notification for user
Expand Down Expand Up @@ -84,7 +89,8 @@ func (s *Service) Submit(req Request) {
if s.dataService != nil && req.Comment.ParentID != "" {
if p, err := s.dataService.Get(req.Comment.Locator, req.Comment.ParentID, store.User{}); err == nil {
req.parent = p
req.Emails = deduplicateStrings(s.getNotificationEmails(req, p))
req.Emails = s.getNotificationTargets(req, p, s.dataService.GetUserEmail)
req.Telegrams = s.getNotificationTargets(req, p, s.dataService.GetUserTelegram)
}
}
select {
Expand All @@ -94,25 +100,32 @@ func (s *Service) Submit(req Request) {
}
}

// getNotificationEmails returns list of emails for notifications for provided comment.
// Emails is not added to the returned list in case original message is from the same user as the notification receiver.
func (s *Service) getNotificationEmails(req Request, notifyComment store.Comment) (result []string) {
// getNotificationTargets returns list of notification targets (like email or telegram username) for users
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comment to reflect the new name, made results deduplicated to prevent the caller from doing it.

// interested in notifications for provided comment.
// Targets are not added to the returned list in case the original message
// is from the same user as the notification receiver.
// Results are deduplicated.
func (s *Service) getNotificationTargets(
req Request,
notifyComment store.Comment,
getUserDetail getUserDetail,
) (result []string) {
paskal marked this conversation as resolved.
Show resolved Hide resolved
// add current user email only if the user is not the one who wrote the original comment
if notifyComment.User.ID != req.Comment.User.ID {
email, err := s.dataService.GetUserEmail(req.Comment.Locator.SiteID, notifyComment.User.ID)
detail, err := getUserDetail(req.Comment.Locator.SiteID, notifyComment.User.ID)
if err != nil {
log.Printf("[WARN] can't read email for %s, %v", notifyComment.User.ID, err)
log.Printf("[WARN] can't read notification detail for %s, %v", notifyComment.User.ID, err)
}
if email != "" {
result = append(result, email)
if detail != "" {
result = append(result, detail)
}
}
if notifyComment.ParentID != "" {
if p, err := s.dataService.Get(req.Comment.Locator, notifyComment.ParentID, store.User{}); err == nil {
result = append(result, s.getNotificationEmails(req, p)...)
result = append(result, s.getNotificationTargets(req, p, getUserDetail)...)
}
}
return result
return deduplicateStrings(result)
}

// SubmitVerification to internal channel if not busy, drop if can't send
Expand Down
32 changes: 20 additions & 12 deletions backend/app/notify/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ func TestService_WithParent(t *testing.T) {

func TestService_EmailRetrieval(t *testing.T) {
dest := &MockDest{id: 1}
dataStore := &mockStore{data: map[string]store.Comment{}, emailData: map[string]string{}}
dataStore := &mockStore{data: map[string]store.Comment{}, userDetails: map[string]string{}}

dataStore.data["p1"] = store.Comment{ID: "p1", User: store.User{ID: "u1"}}
dataStore.data["p2"] = store.Comment{ID: "p2", ParentID: "p1", User: store.User{ID: "u1"}}
dataStore.data["p3"] = store.Comment{ID: "p3", ParentID: "p1", User: store.User{ID: "u2"}}
dataStore.data["p4"] = store.Comment{ID: "p4", ParentID: "p3", User: store.User{ID: "u1"}}
dataStore.emailData["u1"] = "[email protected]"
dataStore.userDetails["u1"] = "[email protected]"

s := NewService(dataStore, 1, dest)
assert.NotNil(t, s)
Expand Down Expand Up @@ -198,16 +198,16 @@ func TestService_EmailRetrieval(t *testing.T) {

func TestService_Recursive(t *testing.T) {
dest := &MockDest{id: 1}
dataStore := &mockStore{data: map[string]store.Comment{}, emailData: map[string]string{}}
dataStore := &mockStore{data: map[string]store.Comment{}, userDetails: map[string]string{}}

dataStore.data["p1"] = store.Comment{ID: "p1", User: store.User{ID: "u1"}}
dataStore.data["p2"] = store.Comment{ID: "p2", ParentID: "p1", User: store.User{ID: "u2"}}
dataStore.data["p3"] = store.Comment{ID: "p3", ParentID: "p2", User: store.User{ID: "u3"}}
dataStore.data["p4"] = store.Comment{ID: "p4", ParentID: "p3", User: store.User{ID: "u1"}}
dataStore.data["p5"] = store.Comment{ID: "p5", ParentID: "p4", User: store.User{ID: "u4"}}
dataStore.emailData["u1"] = "[email protected]"
dataStore.userDetails["u1"] = "[email protected]"
// second comment goes without email address for notification
dataStore.emailData["u3"] = "[email protected]"
dataStore.userDetails["u3"] = "[email protected]"

s := NewService(dataStore, 1, dest)
assert.NotNil(t, s)
Expand Down Expand Up @@ -277,8 +277,16 @@ func TestService_Nop(t *testing.T) {
}

type mockStore struct {
data map[string]store.Comment
emailData map[string]string
data map[string]store.Comment
userDetails map[string]string
}

func (m mockStore) getUserDetail(userID string) (string, error) {
detail, ok := m.userDetails[userID]
if !ok {
return "", errors.New("no such user")
}
return detail, nil
}

func (m mockStore) Get(_ store.Locator, id string, _ store.User) (store.Comment, error) {
Expand All @@ -290,9 +298,9 @@ func (m mockStore) Get(_ store.Locator, id string, _ store.User) (store.Comment,
}

func (m mockStore) GetUserEmail(_, userID string) (string, error) {
email, ok := m.emailData[userID]
if !ok {
return "", errors.New("no such user")
}
return email, nil
return m.getUserDetail(userID)
}

func (m mockStore) GetUserTelegram(_, userID string) (string, error) {
return m.getUserDetail(userID)
}
4 changes: 2 additions & 2 deletions backend/app/rest/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
return
}

if err = a.dataService.DeleteUserDetail(claims.Audience, claims.User.ID, engine.UserEmail); err != nil {
if err = a.dataService.DeleteUserDetail(claims.Audience, claims.User.ID, engine.AllUserDetails); err != nil {
code := parseError(err, rest.ErrInternal)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete email for user", code)
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user details for user", code)
return
}

Expand Down
8 changes: 7 additions & 1 deletion backend/app/store/engine/bolt.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func (b *BoltDB) Flag(req FlagRequest) (val bool, err error) {
// and all site's details listing under the same function (and not to extend interface by two separate functions).
func (b *BoltDB) UserDetail(req UserDetailRequest) ([]UserDetailEntry, error) {
switch req.Detail {
case UserEmail:
case UserEmail, UserTelegram:
if req.UserID == "" {
return nil, errors.New("userid cannot be empty in request for single detail")
}
Expand Down Expand Up @@ -668,6 +668,8 @@ func (b *BoltDB) getUserDetail(req UserDetailRequest) (result []UserDetailEntry,
switch req.Detail {
case UserEmail:
result = []UserDetailEntry{{UserID: req.UserID, Email: entry.Email}}
case UserTelegram:
result = []UserDetailEntry{{UserID: req.UserID, Telegram: entry.Telegram}}
}
}
return nil
Expand Down Expand Up @@ -708,6 +710,8 @@ func (b *BoltDB) setUserDetail(req UserDetailRequest) (result []UserDetailEntry,
switch req.Detail {
case UserEmail:
entry.Email = req.Update
case UserTelegram:
entry.Telegram = req.Update
}

err = bdb.Update(func(tx *bolt.Tx) error {
Expand Down Expand Up @@ -765,6 +769,8 @@ func (b *BoltDB) deleteUserDetail(bdb *bolt.DB, userID string, userDetail UserDe
switch userDetail {
case UserEmail:
entry.Email = ""
case UserTelegram:
entry.Telegram = ""
case AllUserDetails:
entry = UserDetailEntry{UserID: userID}
}
Expand Down
4 changes: 4 additions & 0 deletions backend/app/store/engine/bolt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,10 @@ func TestBoltDB_UserDetail(t *testing.T) {
}{
{req: UserDetailRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "u1", Detail: UserEmail},
expected: []UserDetailEntry{{UserID: "u1", Email: "[email protected]"}}},
{req: UserDetailRequest{Locator: store.Locator{SiteID: "radio-t"}, Detail: UserEmail},
error: "userid cannot be empty in request for single detail"},
{req: UserDetailRequest{Locator: store.Locator{SiteID: "radio-t"}, Detail: AllUserDetails, UserID: "u1"},
error: "unsupported request with userdetail all"},
{req: UserDetailRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "u1", Detail: UserEmail},
error: `site "bad" not found`},
{req: UserDetailRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "u1xyz", Detail: UserEmail}},
Expand Down
7 changes: 5 additions & 2 deletions backend/app/store/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ const (
const (
// UserEmail is a user email
UserEmail = UserDetail("email")
// UserTelegram is a user telegram
UserTelegram = UserDetail("telegram")
// AllUserDetails used for listing and deletion requests
AllUserDetails = UserDetail("all")
)
Expand All @@ -109,8 +111,9 @@ type UserDetail string

// UserDetailEntry contains single user details entry
type UserDetailEntry struct {
UserID string `json:"user_id"` // duplicate user's id to use this structure not only embedded but separately
Email string `json:"email,omitempty"` // UserEmail
UserID string `json:"user_id"` // duplicate user's id to use this structure not only embedded but separately
Email string `json:"email,omitempty"` // UserEmail
Telegram string `json:"telegram,omitempty"` // UserTelegram
}

// UserDetailRequest is the input for both get/set for details, like email
Expand Down
33 changes: 33 additions & 0 deletions backend/app/store/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,39 @@ func (s *DataStore) SetUserEmail(siteID, userID, value string) (string, error) {
return "", nil
}

// GetUserTelegram gets user telegram
func (s *DataStore) GetUserTelegram(siteID, userID string) (string, error) {
res, err := s.Engine.UserDetail(engine.UserDetailRequest{
Detail: engine.UserTelegram,
Locator: store.Locator{SiteID: siteID},
UserID: userID,
})
if err != nil {
return "", err
}
if len(res) == 1 {
return res[0].Telegram, nil
}
return "", nil
}

// SetUserTelegram sets user telegram
func (s *DataStore) SetUserTelegram(siteID, userID, value string) (string, error) {
res, err := s.Engine.UserDetail(engine.UserDetailRequest{
Detail: engine.UserTelegram,
Locator: store.Locator{SiteID: siteID},
UserID: userID,
Update: value,
})
if err != nil {
return "", err
}
if len(res) == 1 {
return res[0].Telegram, nil
}
return "", nil
}

// DeleteUserDetail deletes user detail
func (s *DataStore) DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error {
return s.Engine.Delete(engine.DeleteRequest{
Expand Down
Loading