diff --git a/messaging/repository/channel.go b/messaging/repository/channel.go index cd00f1e12..ff0b0c483 100644 --- a/messaging/repository/channel.go +++ b/messaging/repository/channel.go @@ -2,8 +2,6 @@ package repository import ( "context" - "sort" - "strconv" "strings" "time" @@ -57,10 +55,6 @@ func (r channel) table() string { return "messaging_channel" } -func (r channel) tableMember() string { - return "messaging_channel_member" -} - func (r channel) columns() []string { return []string{ "c.id", @@ -91,29 +85,10 @@ func (r channel) FindByID(ID uint64) (*types.Channel, error) { // FindByMemberSet searches for channel (group!) with exactly the same membership structure func (r channel) FindByMemberSet(memberIDs ...uint64) (*types.Channel, error) { - // Make sure members are sorted - sort.Slice(memberIDs, func(i, j int) bool { - return memberIDs[i] < memberIDs[j] - }) - - // Concatentating members fore - membersConcat := "" - for i := range memberIDs { - // Don't panic, we're adding , in the SQL as well - membersConcat += strconv.FormatUint(memberIDs[i], 10) + "," - } - return r.findOneBy( squirrel.And{ squirrel.Eq{"type": types.ChannelTypeGroup}, - squirrel. - Select("rel_channel"). - From(r.tableMember()). - GroupBy("rel_channel"). - Having(squirrel.Eq{ - "COUNT(*)": len(memberIDs), - "CONCAT(GROUP_CONCAT(rel_user ORDER BY 1 ASC SEPARATOR ','),',')": membersConcat, - }), + squirrel.ConcatExpr("c.id IN (", (channelMember{}).queryExactMembers(memberIDs...), ")"), }) } @@ -145,6 +120,8 @@ func (r channel) Find(filter types.ChannelFilter) (set types.ChannelSet, f types query := r.query() + query = query.Where(squirrel.Eq{"c.archived_at": nil}) + if !f.IncludeDeleted { query = query.Where(squirrel.Eq{"c.deleted_at": nil}) } @@ -157,10 +134,7 @@ func (r channel) Find(filter types.ChannelFilter) (set types.ChannelSet, f types if f.CurrentUserID > 0 { query = query.Where(squirrel.Or{ squirrel.Eq{"c.type": types.ChannelTypePublic}, - squirrel.ConcatExpr("c.id IN (", squirrel. - Select("rel_channel"). - From(r.tableMember()). - Where(squirrel.Eq{"rel_user": f.CurrentUserID}), ")"), + squirrel.ConcatExpr("c.id IN (", (channelMember{}).queryAnyMember(f.CurrentUserID), ")"), }) } diff --git a/messaging/repository/channel_member.go b/messaging/repository/channel_member.go index b74be3ab0..5f4cbef06 100644 --- a/messaging/repository/channel_member.go +++ b/messaging/repository/channel_member.go @@ -2,7 +2,10 @@ package repository import ( "context" + "sort" + "strconv" + "github.com/Masterminds/squirrel" "github.com/titpetric/factory" "github.com/cortezaproject/corteza-server/messaging/types" @@ -14,14 +17,11 @@ type ( ChannelMemberRepository interface { With(ctx context.Context, db *factory.DB) ChannelMemberRepository - Find(filter *types.ChannelMemberFilter) (types.ChannelMemberSet, error) + Find(filter types.ChannelMemberFilter) (types.ChannelMemberSet, error) Create(mod *types.ChannelMember) (*types.ChannelMember, error) Update(mod *types.ChannelMember) (*types.ChannelMember, error) Delete(channelID, userID uint64) error - - CountMemberships(userID uint64) (c int, err error) - ChangeMembership(userID, target uint64) error } channelMember struct { @@ -29,28 +29,6 @@ type ( } ) -const ( - // subquery that filters out all channels that current user has access to as a member - // or via channel type (public channels) - sqlChannelAccess = ` ( - SELECT id - FROM messaging_channel c - LEFT OUTER JOIN messaging_channel_member AS m ON (c.id = m.rel_channel) - WHERE rel_user = ? - UNION - SELECT id - FROM messaging_channel c - WHERE c.type = ? - )` - - // Fetching channel members of all channels a specific user has access to - sqlChannelMemberSelect = `SELECT m.* - FROM messaging_channel_member AS m - INNER JOIN messaging_channel AS c ON (m.rel_channel = c.id) - WHERE c.archived_at IS NULL - AND c.deleted_at IS NULL` -) - // ChannelMember creates new instance of channel member repository func ChannelMember(ctx context.Context, db *factory.DB) ChannelMemberRepository { return (&channelMember{}).With(ctx, db) @@ -63,35 +41,81 @@ func (r *channelMember) With(ctx context.Context, db *factory.DB) ChannelMemberR } } -// FindMembers fetches membership info +func (r channelMember) table() string { + return "messaging_channel_member" +} + +func (r channelMember) columns() []string { + return []string{ + "cm.rel_channel", + "cm.rel_user", + "cm.type", + "cm.flag", + "cm.created_at", + "cm.updated_at", + } +} + +func (r channelMember) query() squirrel.SelectBuilder { + return squirrel. + Select(r.columns()...). + From(r.table() + " AS cm") +} + +// Finds channel ID(s) with any of the members // -// If channelID > 0 it returns members of a specific channel -// If userID > 0 it returns members of all channels this user is member of -func (r *channelMember) Find(filter *types.ChannelMemberFilter) (types.ChannelMemberSet, error) { - params := make([]interface{}, 0) - mm := types.ChannelMemberSet{} +// Builds a (sub)query that returns list of channel IDs at least one of the members +// +func (r channelMember) queryAnyMember(memberIDs ...uint64) squirrel.SelectBuilder { + return squirrel. + Select("cm.rel_channel"). + From(r.table() + " AS cm"). + Where(squirrel.Eq{"cm.rel_user": memberIDs}) +} - sql := sqlChannelMemberSelect - - if filter != nil { - if filter.ComembersOf > 0 { - // scope: only channel we have access to - sql += " AND m.rel_channel IN " + sqlChannelAccess - params = append(params, filter.ComembersOf, types.ChannelTypePublic) - } - - if filter.MemberID > 0 { - sql += " AND m.rel_user = ?" - params = append(params, filter.MemberID) - } - - if filter.ChannelID > 0 { - sql += " AND m.rel_channel = ?" - params = append(params, filter.ChannelID) - } +// Finds channel ID(s) with exact membership +// +// Builds a (sub)query that returns list of channel IDs that have this exact membership +// +func (r channelMember) queryExactMembers(memberIDs ...uint64) squirrel.SelectBuilder { + if len(memberIDs) == 0 { + return squirrel. + Select("null") } - return mm, r.db().Select(&mm, sql, params...) + // Make sure members are sorted + sort.Slice(memberIDs, func(i, j int) bool { + return memberIDs[i] < memberIDs[j] + }) + + // Concatentating members fore + membersConcat := "" + for i := range memberIDs { + // Don't panic, we're adding , in the SQL as well + membersConcat += strconv.FormatUint(memberIDs[i], 10) + "," + } + + return r.queryAnyMember(memberIDs...). + GroupBy("cm.rel_channel"). + Having(squirrel.Eq{ + "COUNT(*)": len(memberIDs), + "CONCAT(GROUP_CONCAT(cm.rel_user ORDER BY 1 ASC SEPARATOR ','),',')": membersConcat, + }) +} + +// Find fetches membership info +func (r *channelMember) Find(filter types.ChannelMemberFilter) (set types.ChannelMemberSet, err error) { + query := r.query() + + if len(filter.MemberID) > 0 { + query = query.Where(squirrel.Eq{"cm.rel_user": filter.MemberID}) + } + + if len(filter.ChannelID) > 0 { + query = query.Where(squirrel.Eq{"cm.rel_channel": filter.ChannelID}) + } + + return set, rh.FetchAll(r.db(), query, &set) } // Create adds channel membership record @@ -116,26 +140,3 @@ func (r *channelMember) Delete(channelID, userID uint64) error { sql := `DELETE FROM messaging_channel_member WHERE rel_channel = ? AND rel_user = ?` return exec(r.db().Exec(sql, channelID, userID)) } - -func (r *channelMember) CountMemberships(userID uint64) (c int, err error) { - return c, r.db().Get(&c, - "SELECT COUNT(*) FROM messaging_channel_member WHERE rel_user = ?", - userID) -} - -func (r *channelMember) ChangeMembership(userID, target uint64) (err error) { - // Remove dups - // with an ugly mysql workaround - _, err = r.db().Exec( - "DELETE FROM messaging_channel_member WHERE rel_user = ? "+ - "AND rel_channel IN (SELECT rel_channel FROM (SELECT * FROM messaging_channel_member) AS workaround WHERE rel_user = ?)", - userID, - target) - - if err != nil { - return err - } - - _, err = r.db().Exec("UPDATE messaging_channel_member SET rel_user = ? WHERE rel_user = ?", target, userID) - return err -} diff --git a/messaging/repository/message.go b/messaging/repository/message.go index d2bf7106d..aa2e7e05f 100644 --- a/messaging/repository/message.go +++ b/messaging/repository/message.go @@ -42,6 +42,19 @@ type ( const ( MESSAGES_MAX_LIMIT = 100 + // subquery that filters out all channels that current user has access to as a member + // or via channel type (public channels) + sqlChannelAccess = ` ( + SELECT id + FROM messaging_channel c + LEFT OUTER JOIN messaging_channel_member AS m ON (c.id = m.rel_channel) + WHERE rel_user = ? + UNION + SELECT id + FROM messaging_channel c + WHERE c.type = ? + )` + sqlMessageColumns = "id, " + "COALESCE(type,'') AS type, " + "message, " + diff --git a/messaging/service/channel.go b/messaging/service/channel.go index f3a14fa37..56013bd9a 100644 --- a/messaging/service/channel.go +++ b/messaging/service/channel.go @@ -159,7 +159,8 @@ func (svc *channel) preloadMembers(cc types.ChannelSet) (err error) { mm types.ChannelMemberSet ) - if mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ComembersOf: userID}); err != nil { + // Load membership info of all channels + if mm, err = svc.cmember.Find(types.ChannelMemberFilterChannels(cc.IDs()...)); err != nil { return } else { err = cc.Walk(func(ch *types.Channel) error { @@ -216,7 +217,7 @@ func (svc *channel) FindMembers(channelID uint64) (out types.ChannelMemberSet, e return } - out, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}) + out, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)) if err != nil { return err } @@ -549,7 +550,7 @@ func (svc *channel) SetFlag(ID uint64, flag types.ChannelMembershipFlag) (ch *ty return } - if members, err := svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: ch.ID, MemberID: userID}); err != nil { + if members, err := svc.cmember.Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{userID}}); err != nil { return err } else if len(members) == 1 { membership = members[0] @@ -671,7 +672,7 @@ func (svc *channel) InviteUser(channelID uint64, memberIDs ...uint64) (out types } return out, svc.db.Transaction(func() (err error) { - if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil { + if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil { return } @@ -729,7 +730,7 @@ func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types. } return out, svc.db.Transaction(func() (err error) { - if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil { + if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil { return } @@ -819,7 +820,7 @@ func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err err } return svc.db.Transaction(func() (err error) { - if existing, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: channelID}); err != nil { + if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil { return } @@ -882,7 +883,7 @@ func (svc *channel) sendChannelEvent(ch *types.Channel) (err error) { // Preload members, if needed if len(ch.Members) == 0 || ch.Member == nil { - if mm, err := svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: ch.ID}); err != nil { + if mm, err := svc.cmember.Find(types.ChannelMemberFilterChannels(ch.ID)); err != nil { return err } else { ch.Members = mm.AllMemberIDs() diff --git a/messaging/service/message.go b/messaging/service/message.go index 01d4c6dc1..17fecc654 100644 --- a/messaging/service/message.go +++ b/messaging/service/message.go @@ -247,7 +247,7 @@ func (svc message) Create(in *types.Message) (m *types.Message, err error) { // // reset unreads for all members var mm types.ChannelMemberSet - mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: original.ChannelID}) + mm, err = svc.cmember.Find(types.ChannelMemberFilterChannels(original.ChannelID)) if err != nil { return err } @@ -910,7 +910,7 @@ func (svc message) findChannelByID(channelID uint64) (ch *types.Channel, err err if ch, err = svc.channel.FindByID(channelID); err != nil { return nil, err - } else if mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ChannelID: ch.ID}); err != nil { + } else if mm, err = svc.cmember.Find(types.ChannelMemberFilterChannels(ch.ID)); err != nil { return nil, err } else { ch.Members = mm.AllMemberIDs() diff --git a/messaging/types/channel_member.go b/messaging/types/channel_member.go index 4f2764c6a..23b5827aa 100644 --- a/messaging/types/channel_member.go +++ b/messaging/types/channel_member.go @@ -18,9 +18,8 @@ type ( } ChannelMemberFilter struct { - ComembersOf uint64 - ChannelID uint64 - MemberID uint64 + ChannelID []uint64 + MemberID []uint64 } ChannelMembershipType string @@ -37,3 +36,8 @@ const ( ChannelMembershipFlagIgnored ChannelMembershipFlag = "ignored" ChannelMembershipFlagNone ChannelMembershipFlag = "" ) + +// ChannelMemberFilterChannels helper func for building channel member filter with list of channels +func ChannelMemberFilterChannels(ID ...uint64) ChannelMemberFilter { + return ChannelMemberFilter{ChannelID: ID} +} diff --git a/tests/messaging/channel_flags_test.go b/tests/messaging/channel_flags_test.go index 63a840119..778ced64e 100644 --- a/tests/messaging/channel_flags_test.go +++ b/tests/messaging/channel_flags_test.go @@ -18,7 +18,10 @@ func TestChannelSetFlag(t *testing.T) { h.repoMakeMember(ch, h.cUser) flagCheck := func(ID uint64, flag string) { - mm, err := h.repoChMember().Find(&types.ChannelMemberFilter{ChannelID: ID, MemberID: h.cUser.ID}) + mm, err := h.repoChMember().Find(types.ChannelMemberFilter{ + ChannelID: []uint64{ID}, + MemberID: []uint64{h.cUser.ID}, + }) h.a.NoError(err) h.a.Len(mm, 1, "expecting 1 member") h.a.Equal(flag, string(mm[0].Flag), "expecting flags to match") diff --git a/tests/messaging/channel_test.go b/tests/messaging/channel_test.go index dfaccd706..9c9dfc8c3 100644 --- a/tests/messaging/channel_test.go +++ b/tests/messaging/channel_test.go @@ -47,14 +47,13 @@ func (h helper) repoMakeMember(ch *types.Channel, u *sysTypes.User) *types.Chann } func (h helper) repoChAssertNotMember(ch *types.Channel, u *sysTypes.User) { - mm, err := h.repoChMember().Find(&types.ChannelMemberFilter{ChannelID: ch.ID, MemberID: h.cUser.ID}) + mm, err := h.repoChMember().Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{h.cUser.ID}}) h.a.NoError(err) - h.a.NotNil(mm) h.a.NotContains(mm.AllMemberIDs(), u.ID, "not expecting to find a member") } func (h helper) repoChAssertMember(ch *types.Channel, u *sysTypes.User, typ types.ChannelMembershipType) { - mm, err := h.repoChMember().Find(&types.ChannelMemberFilter{ChannelID: ch.ID, MemberID: h.cUser.ID}) + mm, err := h.repoChMember().Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{h.cUser.ID}}) h.a.NoError(err) h.a.NotNil(mm)