Implement mentions
Extends internal type and outgoing structs
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user