Implement mentions

Extends internal type and outgoing structs
This commit is contained in:
Denis Arh
2018-11-07 11:21:11 +01:00
parent db5f7623e5
commit 5b832bbea1
11 changed files with 391 additions and 2 deletions
+7
View File
@@ -31,6 +31,7 @@ func Message(ctx context.Context, msg *samTypes.Message) *outgoing.Message {
User: User(msg.User),
Attachment: Attachment(msg.Attachment),
Mentions: messageMentionSet(msg.Mentions),
Reactions: messageReactionSumSet(msg.Flags),
IsPinned: msg.Flags.IsPinned(),
IsBookmarked: msg.Flags.IsBookmarked(currentUserID),
@@ -81,6 +82,12 @@ func messageReactionSumSet(flags samTypes.MessageFlagSet) outgoing.MessageReacti
return rr
}
// Converts slice of mentions into slice of strings containing all user IDs
// These are IDs of users mentioned in the message
func messageMentionSet(mm samTypes.MentionSet) outgoing.MessageMentionSet {
return Uint64stoa(mm.UserIDs())
}
func MessageReaction(f *samTypes.MessageFlag) *outgoing.MessageReaction {
return &outgoing.MessageReaction{
UserID: f.UserID,
+3
View File
@@ -16,6 +16,7 @@ type (
User *User `json:"user"`
Attachment *Attachment `json:"att,omitempty"`
Mentions MessageMentionSet `json:"mentions,omitempty"`
Reactions MessageReactionSumSet `json:"reactions,omitempty"`
IsBookmarked bool `json:"isBookmarked"`
IsPinned bool `json:"isPinned"`
@@ -31,6 +32,8 @@ type (
MessageSet []*Message
MessageMentionSet []string
// Used for single reaction event notification
MessageReactionSum struct {
UserIDs []string `json:"userIDs"`
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
CREATE TABLE mentions (
id BIGINT UNSIGNED NOT NULL,
rel_channel BIGINT UNSIGNED NOT NULL,
rel_message BIGINT UNSIGNED NOT NULL,
rel_user BIGINT UNSIGNED NOT NULL,
rel_mentioned_by BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE INDEX lookup_mentions ON mentions (rel_mentioned_by)
+80
View File
@@ -0,0 +1,80 @@
package repository
import (
"context"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/titpetric/factory"
"github.com/crusttech/crust/sam/types"
)
type (
MentionRepository interface {
With(ctx context.Context, db *factory.DB) MentionRepository
FindByUserIDs(IDs ...uint64) (mm types.MentionSet, err error)
FindByMessageIDs(IDs ...uint64) (mm types.MentionSet, err error)
Create(m *types.Mention) (*types.Mention, error)
DeleteByMessageID(ID uint64) error
DeleteByID(ID uint64) error
}
mention struct {
*repository
}
)
var (
ErrMentionNotFound = repositoryError("MentionNotFound")
)
func Mention(ctx context.Context, db *factory.DB) MentionRepository {
return (&mention{}).With(ctx, db)
}
func (r *mention) With(ctx context.Context, db *factory.DB) MentionRepository {
return &mention{
repository: r.repository.With(ctx, db),
}
}
func (r *mention) FindByUserIDs(IDs ...uint64) (types.MentionSet, error) {
return r.findByIDs("rel_user", IDs...)
}
func (r *mention) FindByMessageIDs(IDs ...uint64) (types.MentionSet, error) {
return r.findByIDs("rel_message", IDs...)
}
func (r *mention) findByIDs(col string, IDs ...uint64) (mm types.MentionSet, err error) {
mm = types.MentionSet{}
if len(IDs) == 0 {
return
}
sql := fmt.Sprintf(`SELECT * FROM mentions WHERE %s IN (?)`, col)
if sql, args, err := sqlx.In(sql, IDs); err != nil {
return nil, err
} else {
return mm, r.db().Select(&mm, sql, args...)
}
}
func (r *mention) Create(m *types.Mention) (*types.Mention, error) {
m.ID = factory.Sonyflake.NextID()
m.CreatedAt = time.Now()
return m, r.db().Insert("mentions", m)
}
func (r *mention) DeleteByMessageID(ID uint64) error {
return exec(r.db().Exec("DELETE FROM mentions WHERE rel_message = ?", ID))
}
func (r *mention) DeleteByID(ID uint64) error {
return exec(r.db().Exec("DELETE FROM mentions WHERE id = ?", ID))
}
+95
View File
@@ -2,10 +2,12 @@ package service
import (
"context"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/payload"
"github.com/crusttech/crust/sam/repository"
"github.com/crusttech/crust/sam/types"
systemService "github.com/crusttech/crust/system/service"
@@ -23,6 +25,7 @@ type (
cview repository.ChannelViewRepository
message repository.MessageRepository
mflag repository.MessageFlagRepository
mentions repository.MentionRepository
usr systemService.UserService
evl EventService
@@ -52,6 +55,11 @@ type (
const (
settingsMessageBodyLength = 0
mentionRE = `<([@#])(\d+)((?:\s)([^>]+))?>`
)
var (
mentionsFinder = regexp.MustCompile(mentionRE)
)
func Message() MessageService {
@@ -76,6 +84,7 @@ func (svc *message) With(ctx context.Context) MessageService {
cview: repository.ChannelView(ctx, db),
message: repository.Message(ctx, db),
mflag: repository.MessageFlag(ctx, db),
mentions: repository.Mention(ctx, db),
}
}
@@ -176,6 +185,10 @@ func (svc *message) Create(in *types.Message) (message *types.Message, err error
return
}
if err = svc.updateMentions(message.ID, svc.extractMentions(message)); err != nil {
return
}
if err = svc.cview.Inc(message.ChannelID, message.UserID); err != nil {
return
}
@@ -226,6 +239,10 @@ func (svc *message) Update(in *types.Message) (message *types.Message, err error
return err
}
if err = svc.updateMentions(message.ID, svc.extractMentions(message)); err != nil {
return
}
return svc.sendEvent(message)
})
}
@@ -281,6 +298,10 @@ func (svc *message) Delete(ID uint64) error {
deletedMsg.DeletedAt = timeNowPtr()
}
if err = svc.updateMentions(ID, nil); err != nil {
return
}
return svc.sendEvent(append(bq, deletedMsg)...)
})
}
@@ -397,6 +418,10 @@ func (svc *message) preload(mm types.MessageSet) (err error) {
return
}
if err = svc.preloadMentions(mm); err != nil {
return
}
return
}
@@ -437,6 +462,21 @@ func (svc *message) preloadFlags(mm types.MessageSet) (err error) {
})
}
// Preload for all messages
func (svc *message) preloadMentions(mm types.MessageSet) (err error) {
var mentions types.MentionSet
mentions, err = svc.mentions.FindByMessageIDs(mm.IDs()...)
if err != nil {
return
}
return mm.Walk(func(m *types.Message) error {
m.Mentions = mentions.FindByMessageID(m.ID)
return nil
})
}
func (svc *message) preloadAttachments(mm types.MessageSet) (err error) {
var (
ids []uint64
@@ -500,4 +540,59 @@ func (svc *message) sendFlagEvent(ff ...*types.MessageFlag) (err error) {
return
}
func (svc *message) extractMentions(m *types.Message) (mm types.MentionSet) {
const reSubID = 2
mm = types.MentionSet{}
match := mentionsFinder.FindAllStringSubmatch(m.Message, -1)
// Prepopulated with all we know from message
tpl := types.Mention{
ChannelID: m.ChannelID,
MessageID: m.ID,
MentionedByID: m.UserID,
}
for m := 0; m < len(match); m++ {
uid := payload.ParseUInt64(match[m][reSubID])
if len(mm.FindByUserID(uid)) == 0 {
// Copy template & assign user id
mnt := tpl
mnt.UserID = uid
mm = append(mm, &mnt)
}
}
return
}
func (svc *message) updateMentions(messageID uint64, mm types.MentionSet) error {
if existing, err := svc.mentions.FindByMessageIDs(messageID); err != nil {
return errors.Wrap(err, "Could not update mentions")
} else if len(mm) > 0 {
add, _, del := existing.Diff(mm)
err = add.Walk(func(m *types.Mention) error {
m, err = svc.mentions.Create(m)
return err
})
if err != nil {
return errors.Wrap(err, "Could not create mentions")
}
err = del.Walk(func(m *types.Mention) error {
return svc.mentions.DeleteByID(m.ID)
})
if err != nil {
return errors.Wrap(err, "Could not delete mentions")
}
} else {
return svc.mentions.DeleteByMessageID(messageID)
}
return nil
}
var _ MessageService = &message{}
+37 -1
View File
@@ -28,7 +28,7 @@ import (
// svc.Create()
// }
func TesMessageLength(t *testing.T) {
func TestMessageLength(t *testing.T) {
// mockCtrl := gomock.NewController(t)
// defer mockCtrl.Finish()
@@ -43,3 +43,39 @@ func TesMessageLength(t *testing.T) {
assert(t, e(svc.Create(&types.Message{})) != nil, "Should not allow to create unnamed channels")
assert(t, e(svc.Create(&types.Message{Message: longText})) != nil, "Should not allow to create channel with really long name")
}
func TestMentionsExtraction(t *testing.T) {
var (
svc = message{}
mm types.MentionSet
cases = []struct {
text string
ids []uint64
}{
{"abcde",
[]uint64{}},
{"<@4095834095>",
[]uint64{4095834095}},
{"<@4095834095> <@4095834095>",
[]uint64{4095834095}},
{"<@4095834095> <@4095834097>",
[]uint64{4095834095, 4095834097}},
{"dfsf<@4095834095>dsfsd<@4095834097>sdfs",
[]uint64{4095834095, 4095834097}},
{"dfsf<@4095834095>dsfsd<@40958340dfsZ",
[]uint64{4095834095}},
{"<@4095834095 label> <@4095834097>",
[]uint64{4095834095, 4095834097}},
}
)
for _, c := range cases {
mm = svc.extractMentions(&types.Message{Message: c.text})
assert(t, len(mm) == len(c.ids), "Number of extracted (%d) and expected (%d) user IDs do not match (%s)", len(mm), len(c.ids), c.text)
for _, id := range c.ids {
assert(t, len(mm.FindByUserID(id)) == 1, "User ID (%d) was not extracted (%s)", id, c.text)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
package types
import (
"fmt"
"runtime"
"testing"
)
func assert(t *testing.T, ok bool, format string, args ...interface{}) bool {
if !ok {
_, file, line, _ := runtime.Caller(1)
caller := fmt.Sprintf("\nAsserted at:%s:%d", file, line)
t.Fatalf(format+caller, args...)
}
return ok
}
+123
View File
@@ -0,0 +1,123 @@
package types
import (
"time"
)
type (
Mention struct {
ID uint64 `db:"id"`
MessageID uint64 `db:"rel_message"`
ChannelID uint64 `db:"rel_channel"`
UserID uint64 `db:"rel_user"`
MentionedByID uint64 `db:"rel_mentioned_by"`
CreatedAt time.Time `db:"created_at"`
}
MentionSet []*Mention
MentionFilter struct {
// All mentions by this user
MentionedByID uint64
// All mentions of this user
UserID uint64
// How many entries
Limit uint
}
)
func (mm MentionSet) Walk(w func(*Mention) error) (err error) {
for i := range mm {
if err = w(mm[i]); err != nil {
return
}
}
return
}
func (mm MentionSet) FindByID(ID uint64) (out *Mention) {
out = &Mention{}
for i := range mm {
if mm[i].ID == ID {
return
}
}
return nil
}
func (mm MentionSet) FindByUserID(ID uint64) (out MentionSet) {
out = MentionSet{}
for i := range mm {
if mm[i].UserID == ID {
out = append(out, mm[i])
}
}
return
}
func (mm MentionSet) FindByMessageID(ID uint64) (out MentionSet) {
out = MentionSet{}
for i := range mm {
if mm[i].MessageID == ID {
out = append(out, mm[i])
}
}
return
}
func (mm MentionSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(mm))
for i := range mm {
IDs[i] = mm[i].ID
}
return
}
func (mm MentionSet) UserIDs() (IDs []uint64) {
IDs = make([]uint64, len(mm))
for i := range mm {
IDs[i] = mm[i].UserID
}
return
}
func (mm MentionSet) Diff(in MentionSet) (add, upd, del MentionSet) {
add, upd, del = MentionSet{}, MentionSet{}, MentionSet{}
for _, m := range in {
if m.ID == 0 {
// Mark for adding all new
add = append(add, m)
}
}
for _, m := range mm {
if m.ID == 0 {
// Ignore all unsaved
continue
}
if in.FindByID(m.ID) == nil {
// Mark for removal all that are not added
del = append(del, m)
} else {
// Mark for update all that are still there
upd = append(upd, m)
}
}
return
}
+14
View File
@@ -0,0 +1,14 @@
package types
import (
"testing"
)
func TestMentionSet_Diff(t *testing.T) {
ex := MentionSet{&Mention{ID: 1000}, &Mention{ID: 1001}}
add, upd, del := ex.Diff(MentionSet{&Mention{ID: 1001}, &Mention{UserID: 1}})
assert(t, len(add) == 1 && len(add.FindByUserID(1)) == 1, "Did not find expected mention (UserID:1) for creation")
assert(t, len(upd) == 1 && upd.FindByID(1001) != nil, "Did not find expected mention (id:1001) for update")
assert(t, len(del) == 1 && del.FindByID(1000) != nil, "Did not find expected mention (id:1000) for removal")
}
+1
View File
@@ -22,6 +22,7 @@ type (
Attachment *Attachment `json:"attachment,omitempty"`
User *systemTypes.User `json:"user,omitempty"`
Flags MessageFlagSet `json:"flags,omitempty"`
Mentions MentionSet
}
MessageSet []*Message