add UserDetailTelegram support

This commit is contained in:
Dmitry Verkhoturov
2021-06-13 13:18:33 -05:00
committed by Umputun
parent d6167980f4
commit fd4c6ceb18
11 changed files with 170 additions and 42 deletions
@@ -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")
}
@@ -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
}
}
@@ -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
@@ -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}
}
@@ -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)
@@ -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 {
@@ -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)
+26 -13
View File
@@ -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
@@ -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 {
@@ -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
// 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) {
// 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
+20 -12
View File
@@ -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"] = "u1@example.com"
dataStore.userDetails["u1"] = "u1@example.com"
s := NewService(dataStore, 1, dest)
assert.NotNil(t, s)
@@ -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"] = "u1@example.com"
dataStore.userDetails["u1"] = "u1@example.com"
// second comment goes without email address for notification
dataStore.emailData["u3"] = "u3@example.com"
dataStore.userDetails["u3"] = "u3@example.com"
s := NewService(dataStore, 1, dest)
assert.NotNil(t, s)
@@ -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) {
@@ -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)
}
+2 -2
View File
@@ -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
}
+7 -1
View File
@@ -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")
}
@@ -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
@@ -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 {
@@ -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}
}
+4
View File
@@ -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: "test@example.com"}}},
{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}},
+5 -2
View File
@@ -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")
)
@@ -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
+33
View File
@@ -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{
+19 -2
View File
@@ -908,30 +908,47 @@ func TestService_UserDetailsOperations(t *testing.T) {
result, err := b.SetUserEmail("radio-t", "u1", "test@example.com")
assert.NoError(t, err, "No error inserting entry expected")
assert.Equal(t, "test@example.com", result)
result, err = b.SetUserTelegram("radio-t", "u1", "test@example.com")
assert.NoError(t, err, "No error inserting entry expected")
assert.Equal(t, "test@example.com", result)
// read valid entry back
result, err = b.GetUserEmail("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Equal(t, "test@example.com", result)
result, err = b.GetUserTelegram("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Equal(t, "test@example.com", result)
// delete existing entry
err = b.DeleteUserDetail("radio-t", "u1", engine.UserEmail)
assert.NoError(t, err, "No error deleting entry expected")
err = b.DeleteUserDetail("radio-t", "u1", engine.UserTelegram)
assert.NoError(t, err, "No error deleting entry expected")
// read deleted entry
result, err = b.GetUserEmail("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Empty(t, result)
result, err = b.GetUserTelegram("radio-t", "u1")
assert.NoError(t, err, "No error reading entry expected")
assert.Empty(t, result)
// insert entry with invalid site_id
result, err = b.SetUserEmail("bad-site", "u3", "does_not_matter@example.com")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
result, err = b.SetUserTelegram("bad-site", "u3", "does_not_matter@example.com")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
// read entry with invalid site_id
result, err = b.GetUserEmail("bad-site", "u3")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
result, err = b.GetUserTelegram("bad-site", "u3")
assert.Error(t, err, "Site not found")
assert.Empty(t, result)
}
func TestService_IsAdmin(t *testing.T) {
@@ -1370,7 +1387,7 @@ func TestService_submitImages(t *testing.T) {
b.submitImages(c)
mockStore.AssertNumberOfCalls(t, "ResetCleanupTimer", 2)
time.Sleep(b.EditDuration + 100 * time.Millisecond)
time.Sleep(b.EditDuration + 100*time.Millisecond)
mockStore.AssertNumberOfCalls(t, "Commit", 2)
}
@@ -1415,7 +1432,7 @@ func TestService_ResubmitStagingImages(t *testing.T) {
mockStore.On("Commit", "dev_user/bqf122eq9r8ad657n3ng").Once().Return(nil)
mockStore.On("Commit", "dev_user/bqf321eq9r8ad657n3ng").Once().Return(nil)
mockStore.On("Commit", "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e").Once().Return(nil)
time.Sleep(b.EditDuration + time.Millisecond * 100)
time.Sleep(b.EditDuration + time.Millisecond*100)
mockStore.AssertNumberOfCalls(t, "Info", 1)
mockStore.AssertNumberOfCalls(t, "Commit", 3)