From 1bffd2e64d0172cb2072d09d7121985c8999df8c Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 9 Feb 2022 17:58:33 -0400 Subject: [PATCH] feat: unified chat API pt. 1 --- protocol/communities/community.go | 4 + protocol/message_persistence.go | 14 +- protocol/messenger_communities.go | 4 + services/ext/chat_accesors.go | 328 ++++++++++++++++++++++++++++++ 4 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 services/ext/chat_accesors.go diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 0b9548c0b..64507ae76 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -1221,6 +1221,10 @@ func (o *Community) isMember() bool { return o.hasMember(o.config.MemberIdentity) } +func (o *Community) CanMemberIdentityPost(chatID string) (bool, error) { + return o.CanPost(o.config.MemberIdentity, chatID, nil) +} + // CanJoin returns whether a user can join the community, only if it's func (o *Community) canJoin() bool { if o.config.Joined { diff --git a/protocol/message_persistence.go b/protocol/message_persistence.go index 19e95f065..9c1bc195c 100644 --- a/protocol/message_persistence.go +++ b/protocol/message_persistence.go @@ -784,6 +784,11 @@ func (db sqlitePersistence) PinnedMessageByChatIDs(chatIDs []string, currCursor if currCursor != "" { args = append(args, currCursor) } + + limitStr := "" + if limit > -1 { + args = append(args, limit+1) // take one more to figure our whether a cursor should be returned + } // Build a new column `cursor` at the query time by having a fixed-sized clock value at the beginning // concatenated with message ID. Results are sorted using this new column. // This new column values can also be returned as a cursor for subsequent requests. @@ -812,9 +817,9 @@ func (db sqlitePersistence) PinnedMessageByChatIDs(chatIDs []string, currCursor pm.pinned = 1 AND NOT(m1.hide) AND m1.local_chat_id IN %s %s ORDER BY cursor DESC - LIMIT ? - `, allFields, "(?"+strings.Repeat(",?", len(chatIDs)-1)+")", cursorWhere), - append(args, limit+1)..., // take one more to figure our whether a cursor should be returned + %s + `, allFields, "(?"+strings.Repeat(",?", len(chatIDs)-1)+")", cursorWhere, limitStr), + args..., // take one more to figure our whether a cursor should be returned ) if err != nil { return nil, "", err @@ -845,7 +850,8 @@ func (db sqlitePersistence) PinnedMessageByChatIDs(chatIDs []string, currCursor } var newCursor string - if len(result) > limit && cursors != nil { + + if limit > -1 && len(result) > limit && cursors != nil { newCursor = cursors[limit] result = result[:limit] } diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index e2a333775..ba19c8623 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -665,6 +665,10 @@ func (m *Messenger) InviteUsersToCommunity(request *requests.InviteUsersToCommun return response, nil } +func (m *Messenger) GetCommunityByID(communityID types.HexBytes) (*communities.Community, error) { + return m.communitiesManager.GetByID(communityID) +} + func (m *Messenger) ShareCommunity(request *requests.ShareCommunity) (*MessengerResponse, error) { if err := request.Validate(); err != nil { return nil, err diff --git a/services/ext/chat_accesors.go b/services/ext/chat_accesors.go new file mode 100644 index 000000000..30f806b5f --- /dev/null +++ b/services/ext/chat_accesors.go @@ -0,0 +1,328 @@ +package ext + +import ( + "context" + "errors" + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/images" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/communities" + "github.com/status-im/status-go/protocol/protobuf" + v1protocol "github.com/status-im/status-go/protocol/v1" +) + +var ( + ErrChatNotFound = errors.New("can't find chat") +) + +type ChannelGroupType string + +const Personal ChannelGroupType = "personal" +const Community ChannelGroupType = "community" + +type ChatPinnedMessages struct { + Cursor string + PinnedMessages []*common.PinnedMessage +} + +type ChatMember struct { + // Community Roles + Roles []protobuf.CommunityMember_Roles `json:"roles,omitempty"` + // Admin indicates if the member is an admin of the group chat + Admin bool `json:"admin"` + // Joined indicates if the member has joined the group chat + Joined bool `json:"joined"` +} + +type Chat struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Color string `json:"color"` + Emoji string `json:"emoji"` + Active bool `json:"active"` + ChatType protocol.ChatType `json:"chatType"` + Timestamp int64 `json:"timestamp"` + LastClockValue uint64 `json:"lastClockValue"` + DeletedAtClockValue uint64 `json:"deletedAtClockValue"` + ReadMessagesAtClockValue uint64 `json:"readMessagesAtClockValue"` + UnviewedMessagesCount uint `json:"unviewedMessagesCount"` + UnviewedMentionsCount uint `json:"unviewedMentionsCount"` + LastMessage *common.Message `json:"lastMessage"` + Members map[string]ChatMember `json:"members,omitempty"` + MembershipUpdates []v1protocol.MembershipUpdateEvent `json:"membershipUpdateEvents"` + Alias string `json:"alias,omitempty"` + Identicon string `json:"identicon"` + Muted bool `json:"muted"` + InvitationAdmin string `json:"invitationAdmin,omitempty"` + ReceivedInvitationAdmin string `json:"receivedInvitationAdmin,omitempty"` + Profile string `json:"profile,omitempty"` + CommunityID string `json:"communityId,omitempty"` + CategoryID string `json:"categoryId"` + Position int32 `json:"position,omitempty"` + Permissions *protobuf.CommunityPermissions `json:"permissions,omitempty"` + Joined int64 `json:"joined,omitempty"` + SyncedTo uint32 `json:"syncedTo,omitempty"` + SyncedFrom uint32 `json:"syncedFrom,omitempty"` + Highlight bool `json:"highlight,omitempty"` + PinnedMessages *ChatPinnedMessages `json:"pinnedMessages,omitempty"` + CanPost bool `json:"canPost"` +} + +type ChannelGroup struct { + Type ChannelGroupType `json:"channelGroupType"` + Name string `json:"name"` + Images map[string]images.IdentityImage `json:"images"` + Color string `json:"color"` + Chats map[string]*Chat `json:"chats"` + Categories map[string]communities.CommunityCategory `json:"categories"` + EnsName string `json:"ensName"` +} + +func (api *PublicAPI) GetChats(parent context.Context) (map[string]ChannelGroup, error) { + joinedCommunities, err := api.service.messenger.JoinedCommunities() + if err != nil { + return nil, err + } + + channels := api.service.messenger.Chats() + + pubKey, err := api.service.accountsDB.GetPublicKey() + if err != nil { + return nil, err + } + + result := make(map[string]ChannelGroup) + + result[pubKey] = ChannelGroup{ + Type: Personal, + Name: "", + Images: make(map[string]images.IdentityImage), + Color: "", + Chats: make(map[string]*Chat), + Categories: make(map[string]communities.CommunityCategory), + EnsName: "", // Not implemented yet in communities + } + + for _, chat := range channels { + if !chat.Active || (!chat.OneToOne() && !chat.PrivateGroupChat() && !chat.Public()) || chat.CommunityID != "" { + continue + } + + pinnedMessages, cursor, err := api.service.messenger.PinnedMessageByChatID(chat.ID, "", -1) + if err != nil { + return nil, err + } + + c, err := toAPIChat(chat, nil, pubKey, pinnedMessages, cursor) + if err != nil { + return nil, err + } + result[pubKey].Chats[chat.ID] = c + } + + for _, community := range joinedCommunities { + chGrp := ChannelGroup{ + Type: Community, + Name: community.Name(), + Color: community.Description().Identity.Color, + Images: make(map[string]images.IdentityImage), + Chats: make(map[string]*Chat), + Categories: make(map[string]communities.CommunityCategory), + } + + for t, i := range community.Description().Identity.Images { + chGrp.Images[t] = images.IdentityImage{Name: t, Payload: i.Payload} + } + + for _, cat := range community.Description().Categories { + chGrp.Categories[cat.CategoryId] = communities.CommunityCategory{ + ID: cat.CategoryId, + Name: cat.Name, + Position: int(cat.Position), + } + } + + for _, chat := range channels { + if chat.CommunityID == community.IDString() { + pinnedMessages, cursor, err := api.service.messenger.PinnedMessageByChatID(chat.ID, "", -1) + if err != nil { + return nil, err + } + + c, err := toAPIChat(chat, community, pubKey, pinnedMessages, cursor) + if err != nil { + return nil, err + } + + chGrp.Chats[c.ID] = c + } + } + + result[community.IDString()] = chGrp + } + + return result, nil +} + +func (api *PublicAPI) GetChat(parent context.Context, communityID types.HexBytes, chatID string) (*Chat, error) { + fullChatID := chatID + + if len(communityID) != 0 { + fullChatID = string(communityID.Bytes()) + chatID + } + + messengerChat := api.service.messenger.Chat(fullChatID) + if messengerChat == nil { + return nil, ErrChatNotFound + } + + pubKey, err := api.service.accountsDB.GetPublicKey() + if err != nil { + return nil, err + } + + var community *communities.Community + if messengerChat.CommunityID != "" { + communityID, err := hexutil.Decode(messengerChat.CommunityID) + if err != nil { + return nil, err + } + + community, err = api.service.messenger.GetCommunityByID(communityID) + if err != nil { + return nil, err + } + } + + pinnedMessages, cursor, err := api.service.messenger.PinnedMessageByChatID(messengerChat.ID, "", -1) + if err != nil { + return nil, err + } + + result, err := toAPIChat(messengerChat, community, pubKey, pinnedMessages, cursor) + if err != nil { + return nil, err + } + + return result, nil +} + +func toAPIChat(protocolChat *protocol.Chat, community *communities.Community, pubKey string, pinnedMessages []*common.PinnedMessage, cursor string) (*Chat, error) { + chat := &Chat{ + ID: strings.TrimPrefix(protocolChat.ID, protocolChat.CommunityID), + Name: protocolChat.Name, + Description: protocolChat.Description, + Color: protocolChat.Color, + Emoji: protocolChat.Emoji, + Active: protocolChat.Active, + ChatType: protocolChat.ChatType, + Timestamp: protocolChat.Timestamp, + LastClockValue: protocolChat.LastClockValue, + DeletedAtClockValue: protocolChat.DeletedAtClockValue, + ReadMessagesAtClockValue: protocolChat.ReadMessagesAtClockValue, + UnviewedMessagesCount: protocolChat.UnviewedMessagesCount, + UnviewedMentionsCount: protocolChat.UnviewedMentionsCount, + LastMessage: protocolChat.LastMessage, + Members: make(map[string]ChatMember), + MembershipUpdates: protocolChat.MembershipUpdates, + Alias: protocolChat.Alias, + Identicon: protocolChat.Identicon, + Muted: protocolChat.Muted, + InvitationAdmin: protocolChat.InvitationAdmin, + ReceivedInvitationAdmin: protocolChat.ReceivedInvitationAdmin, + Profile: protocolChat.Profile, + CommunityID: protocolChat.CommunityID, + CategoryID: protocolChat.CategoryID, + Joined: protocolChat.Joined, + SyncedTo: protocolChat.SyncedTo, + SyncedFrom: protocolChat.SyncedFrom, + Highlight: protocolChat.Highlight, + } + + if protocolChat.OneToOne() { + chat.Name = "" // Emptying since it contains non useful data + } + + if len(pinnedMessages) != 0 { + chat.PinnedMessages = &ChatPinnedMessages{ + Cursor: cursor, + PinnedMessages: pinnedMessages, + } + } + + err := chat.populateCommunityFields(community) + if err != nil { + return nil, err + } + + chat.setChatMembers(protocolChat, community, pubKey) + + return chat, nil +} + +func (chat *Chat) setChatMembers(sourceChat *protocol.Chat, community *communities.Community, userPubKey string) { + if sourceChat.ChatType == protocol.ChatTypePrivateGroupChat && len(sourceChat.Members) > 0 { + for _, m := range sourceChat.Members { + chat.Members[m.ID] = ChatMember{ + Admin: m.Admin, + Joined: m.Joined, + } + } + return + } + + if sourceChat.ChatType == protocol.ChatTypeOneToOne { + chat.Members[sourceChat.ID] = ChatMember{ + Joined: true, + } + chat.Members[userPubKey] = ChatMember{ + Joined: true, + } + return + } + + if community != nil { + for pubKey, m := range community.Description().Members { + if pubKey == userPubKey { + chat.Members[pubKey] = ChatMember{ + Roles: m.Roles, + Joined: true, + } + } else { + chat.Members[pubKey] = ChatMember{ + Roles: m.Roles, + Joined: community.Joined(), + } + } + } + return + } +} + +func (chat *Chat) populateCommunityFields(community *communities.Community) error { + commChat, exists := community.Chats()[chat.ID] + if !exists { + return ErrChatNotFound + } + + canPost, err := community.CanMemberIdentityPost(chat.ID) + if err != nil { + return err + } + + chat.CategoryID = commChat.CategoryId + chat.Position = commChat.Position + chat.Permissions = commChat.Permissions + chat.Emoji = commChat.Identity.Emoji + chat.Name = commChat.Identity.DisplayName + chat.Color = commChat.Identity.Color + chat.Description = commChat.Identity.Description + chat.CanPost = canPost + + return nil +}