mirror of
https://github.com/status-im/status-go.git
synced 2025-01-09 06:12:55 +00:00
b8b402da8d
This commit adds new tables to the database and APIs in `Messenger` and communities `Manager` to store `CheckChannelPermissionsResponse`s. The responses are stored whenever channel permissions have been checked. The reason we're doing this is so that clients can retrieve the last known channel permission state before waiting for onchain checks to finish.
537 lines
20 KiB
Go
537 lines
20 KiB
Go
package chat
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/status-im/status-go/eth-node/crypto"
|
|
"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"
|
|
"github.com/status-im/status-go/protocol/requests"
|
|
v1protocol "github.com/status-im/status-go/protocol/v1"
|
|
)
|
|
|
|
var (
|
|
ErrChatNotFound = errors.New("can't find chat")
|
|
ErrCommunityNotFound = errors.New("can't find community")
|
|
ErrCommunitiesNotSupported = errors.New("communities are not supported")
|
|
ErrChatTypeNotSupported = errors.New("chat type not supported")
|
|
)
|
|
|
|
type ChannelGroupType string
|
|
|
|
const Personal ChannelGroupType = "personal"
|
|
const Community ChannelGroupType = "community"
|
|
|
|
type PinnedMessages struct {
|
|
Cursor string
|
|
PinnedMessages []*common.PinnedMessage
|
|
}
|
|
|
|
type Member struct {
|
|
// Community Role
|
|
Role protobuf.CommunityMember_Roles `json:"role,omitempty"`
|
|
// 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]Member `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"`
|
|
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"`
|
|
FirstMessageTimestamp uint32 `json:"firstMessageTimestamp,omitempty"`
|
|
Highlight bool `json:"highlight,omitempty"`
|
|
PinnedMessages *PinnedMessages `json:"pinnedMessages,omitempty"`
|
|
CanPost bool `json:"canPost"`
|
|
Base64Image string `json:"image,omitempty"`
|
|
}
|
|
|
|
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"`
|
|
MemberRole protobuf.CommunityMember_Roles `json:"memberRole"`
|
|
Verified bool `json:"verified"`
|
|
Description string `json:"description"`
|
|
IntroMessage string `json:"introMessage"`
|
|
OutroMessage string `json:"outroMessage"`
|
|
Tags []communities.CommunityTag `json:"tags"`
|
|
Permissions *protobuf.CommunityPermissions `json:"permissions"`
|
|
Members map[string]*protobuf.CommunityMember `json:"members"`
|
|
CanManageUsers bool `json:"canManageUsers"`
|
|
Muted bool `json:"muted"`
|
|
BanList []string `json:"banList"`
|
|
Encrypted bool `json:"encrypted"`
|
|
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
|
|
UnviewedMessagesCount int `json:"unviewedMessagesCount"`
|
|
UnviewedMentionsCount int `json:"unviewedMentionsCount"`
|
|
CheckChannelPermissionResponses map[string]*communities.CheckChannelPermissionsResponse `json:"checkChannelPermissionResponses"`
|
|
}
|
|
|
|
func NewAPI(service *Service) *API {
|
|
return &API{
|
|
s: service,
|
|
}
|
|
}
|
|
|
|
type API struct {
|
|
s *Service
|
|
}
|
|
|
|
func unique(communities []*communities.Community) (result []*communities.Community) {
|
|
inResult := make(map[string]bool)
|
|
for _, community := range communities {
|
|
if _, ok := inResult[community.IDString()]; !ok {
|
|
inResult[community.IDString()] = true
|
|
result = append(result, community)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (api *API) getChannelGroups(ctx context.Context, channelGroupID string) (map[string]ChannelGroup, error) {
|
|
joinedCommunities, err := api.s.messenger.JoinedCommunities()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
spectatedCommunities, err := api.s.messenger.SpectatedCommunities()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
|
|
|
|
result := make(map[string]ChannelGroup)
|
|
|
|
// Get chats from cache to get unviewed messages counts
|
|
channels := api.s.messenger.Chats()
|
|
totalUnviewedMessageCount := 0
|
|
totalUnviewedMentionsCount := 0
|
|
|
|
if channelGroupID == "" || channelGroupID == pubKey {
|
|
chats := make(map[string]*Chat)
|
|
for _, chat := range channels {
|
|
if !chat.IsActivePersonalChat() {
|
|
continue
|
|
}
|
|
if !chat.Muted || chat.UnviewedMentionsCount > 0 {
|
|
totalUnviewedMessageCount += int(chat.UnviewedMessagesCount)
|
|
}
|
|
totalUnviewedMentionsCount += int(chat.UnviewedMentionsCount)
|
|
|
|
c, err := api.toAPIChat(chat, nil, pubKey, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chats[chat.ID] = c
|
|
}
|
|
|
|
result[pubKey] = ChannelGroup{
|
|
Type: Personal,
|
|
Name: "",
|
|
Images: make(map[string]images.IdentityImage),
|
|
Color: "",
|
|
Chats: chats,
|
|
Categories: make(map[string]communities.CommunityCategory),
|
|
EnsName: "", // Not implemented yet in communities
|
|
MemberRole: protobuf.CommunityMember_ROLE_OWNER,
|
|
Verified: true,
|
|
Description: "",
|
|
IntroMessage: "",
|
|
OutroMessage: "",
|
|
Tags: []communities.CommunityTag{},
|
|
Permissions: &protobuf.CommunityPermissions{},
|
|
Muted: false,
|
|
CommunityTokensMetadata: []*protobuf.CommunityTokenMetadata{},
|
|
UnviewedMessagesCount: totalUnviewedMessageCount,
|
|
UnviewedMentionsCount: totalUnviewedMentionsCount,
|
|
CheckChannelPermissionResponses: make(map[string]*communities.CheckChannelPermissionsResponse),
|
|
}
|
|
}
|
|
|
|
if channelGroupID == pubKey {
|
|
// They asked for the personal channel group only, so we return now
|
|
return result, nil
|
|
}
|
|
|
|
for _, community := range unique(append(joinedCommunities, spectatedCommunities...)) {
|
|
if channelGroupID != "" && channelGroupID != community.IDString() {
|
|
continue
|
|
}
|
|
totalUnviewedMessageCount = 0
|
|
totalUnviewedMentionsCount = 0
|
|
|
|
for _, chat := range channels {
|
|
if chat.CommunityID != community.IDString() || !chat.Active {
|
|
continue
|
|
}
|
|
if !chat.Muted || chat.UnviewedMentionsCount > 0 {
|
|
totalUnviewedMessageCount += int(chat.UnviewedMessagesCount)
|
|
}
|
|
totalUnviewedMentionsCount += int(chat.UnviewedMentionsCount)
|
|
}
|
|
|
|
chGrp := ChannelGroup{
|
|
Type: Community,
|
|
Name: community.Name(),
|
|
Color: community.Color(),
|
|
Images: make(map[string]images.IdentityImage),
|
|
Chats: make(map[string]*Chat),
|
|
Categories: make(map[string]communities.CommunityCategory),
|
|
MemberRole: community.MemberRole(community.MemberIdentity()),
|
|
Verified: community.Verified(),
|
|
Description: community.DescriptionText(),
|
|
IntroMessage: community.IntroMessage(),
|
|
OutroMessage: community.OutroMessage(),
|
|
Tags: community.Tags(),
|
|
Permissions: community.Description().Permissions,
|
|
Members: community.Description().Members,
|
|
CanManageUsers: community.CanManageUsers(community.MemberIdentity()),
|
|
Muted: community.Muted(),
|
|
BanList: community.Description().BanList,
|
|
Encrypted: community.Encrypted(),
|
|
CommunityTokensMetadata: community.Description().CommunityTokensMetadata,
|
|
UnviewedMessagesCount: totalUnviewedMessageCount,
|
|
UnviewedMentionsCount: totalUnviewedMentionsCount,
|
|
CheckChannelPermissionResponses: make(map[string]*communities.CheckChannelPermissionsResponse),
|
|
}
|
|
|
|
for t, i := range community.Images() {
|
|
chGrp.Images[t] = images.IdentityImage{Name: t, Payload: i.Payload}
|
|
}
|
|
|
|
for _, cat := range community.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() && chat.Active {
|
|
c, err := api.toAPIChat(chat, community, pubKey, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chGrp.Chats[c.ID] = c
|
|
}
|
|
}
|
|
|
|
response, err := api.s.messenger.GetCommunityCheckChannelPermissionResponses(community.ID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chGrp.CheckChannelPermissionResponses = response.Channels
|
|
|
|
result[community.IDString()] = chGrp
|
|
|
|
if channelGroupID == community.IDString() {
|
|
// We asked for this particular community, so we return now
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (api *API) GetChannelGroups(ctx context.Context) (map[string]ChannelGroup, error) {
|
|
return api.getChannelGroups(ctx, "")
|
|
}
|
|
|
|
func (api *API) GetChannelGroupByID(ctx context.Context, channelGroupID string) (map[string]ChannelGroup, error) {
|
|
return api.getChannelGroups(ctx, channelGroupID)
|
|
}
|
|
|
|
func (api *API) GetChat(ctx context.Context, communityID types.HexBytes, chatID string) (*Chat, error) {
|
|
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
|
|
messengerChat, community, err := api.getChatAndCommunity(pubKey, communityID, chatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if messengerChat == nil {
|
|
return nil, ErrChatNotFound
|
|
}
|
|
|
|
result, err := api.toAPIChat(messengerChat, community, pubKey, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (api *API) GetMembers(ctx context.Context, communityID types.HexBytes, chatID string) (map[string]Member, error) {
|
|
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
|
|
messengerChat, community, err := api.getChatAndCommunity(pubKey, communityID, chatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getChatMembers(messengerChat, community, pubKey)
|
|
}
|
|
|
|
func (api *API) JoinChat(ctx context.Context, communityID types.HexBytes, chatID string) (*Chat, error) {
|
|
if len(communityID) != 0 {
|
|
return nil, ErrCommunitiesNotSupported
|
|
}
|
|
|
|
response, err := api.s.messenger.CreatePublicChat(&requests.CreatePublicChat{ID: chatID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
|
|
|
|
return api.toAPIChat(response.Chats()[0], nil, pubKey, false)
|
|
}
|
|
|
|
func (api *API) toAPIChat(protocolChat *protocol.Chat, community *communities.Community, pubKey string, skipPinnedMessages bool) (*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,
|
|
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,
|
|
FirstMessageTimestamp: protocolChat.FirstMessageTimestamp,
|
|
Highlight: protocolChat.Highlight,
|
|
Base64Image: protocolChat.Base64Image,
|
|
}
|
|
|
|
if protocolChat.OneToOne() {
|
|
chat.Name = "" // Emptying since it contains non useful data
|
|
}
|
|
|
|
if !skipPinnedMessages {
|
|
pinnedMessages, cursor, err := api.s.messenger.PinnedMessageByChatID(protocolChat.ID, "", -1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(pinnedMessages) != 0 {
|
|
chat.PinnedMessages = &PinnedMessages{
|
|
Cursor: cursor,
|
|
PinnedMessages: pinnedMessages,
|
|
}
|
|
}
|
|
}
|
|
|
|
err := chat.populateCommunityFields(community)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chatMembers, err := getChatMembers(protocolChat, community, pubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chat.Members = chatMembers
|
|
|
|
return chat, nil
|
|
}
|
|
|
|
func getChatMembers(sourceChat *protocol.Chat, community *communities.Community, userPubKey string) (map[string]Member, error) {
|
|
result := make(map[string]Member)
|
|
if sourceChat != nil {
|
|
if sourceChat.ChatType == protocol.ChatTypePrivateGroupChat && len(sourceChat.Members) > 0 {
|
|
for _, m := range sourceChat.Members {
|
|
result[m.ID] = Member{
|
|
Role: func() protobuf.CommunityMember_Roles {
|
|
if m.Admin {
|
|
return protobuf.CommunityMember_ROLE_OWNER
|
|
}
|
|
return protobuf.CommunityMember_ROLE_NONE
|
|
}(),
|
|
Joined: true,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
if sourceChat.ChatType == protocol.ChatTypeOneToOne {
|
|
result[sourceChat.ID] = Member{
|
|
Joined: true,
|
|
}
|
|
result[userPubKey] = Member{
|
|
Joined: true,
|
|
}
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
if community != nil {
|
|
for member := range community.Description().Members {
|
|
pubKey, err := common.HexToPubkey(member)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[member] = Member{
|
|
Role: community.MemberRole(pubKey),
|
|
Joined: community.Joined(),
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (api *API) getCommunityByID(id string) (*communities.Community, error) {
|
|
communityID, err := hexutil.Decode(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
community, err := api.s.messenger.GetCommunityByID(communityID)
|
|
if community == nil && err == nil {
|
|
return nil, ErrCommunityNotFound
|
|
}
|
|
|
|
return community, err
|
|
}
|
|
|
|
func (chat *Chat) populateCommunityFields(community *communities.Community) error {
|
|
if community == nil {
|
|
return nil
|
|
}
|
|
|
|
commChat, exists := community.Chats()[chat.ID]
|
|
if !exists {
|
|
// Skip unknown community chats. They might be channels that were deleted
|
|
return nil
|
|
}
|
|
|
|
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.Description = commChat.Identity.Description
|
|
chat.CanPost = canPost
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) getChatAndCommunity(pubKey string, communityID types.HexBytes, chatID string) (*protocol.Chat, *communities.Community, error) {
|
|
fullChatID := chatID
|
|
|
|
if string(communityID.Bytes()) == pubKey { // Obtaining chats from personal
|
|
communityID = []byte{}
|
|
}
|
|
|
|
if len(communityID) != 0 {
|
|
id := string(communityID.Bytes())
|
|
|
|
if chatID == "" {
|
|
community, err := api.getCommunityByID(id)
|
|
return nil, community, err
|
|
}
|
|
|
|
fullChatID = id + chatID
|
|
}
|
|
|
|
messengerChat := api.s.messenger.Chat(fullChatID)
|
|
if messengerChat == nil {
|
|
return nil, nil, ErrChatNotFound
|
|
}
|
|
|
|
var community *communities.Community
|
|
if messengerChat.CommunityID != "" {
|
|
var err error
|
|
community, err = api.getCommunityByID(messengerChat.CommunityID)
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
return messengerChat, community, nil
|
|
}
|
|
|
|
func (api *API) EditChat(ctx context.Context, communityID types.HexBytes, chatID string, name string, color string, image images.CroppedImage) (*Chat, error) {
|
|
if len(communityID) != 0 {
|
|
return nil, ErrCommunitiesNotSupported
|
|
}
|
|
|
|
chatToEdit := api.s.messenger.Chat(chatID)
|
|
if chatToEdit == nil {
|
|
return nil, ErrChatNotFound
|
|
}
|
|
|
|
if chatToEdit.ChatType != protocol.ChatTypePrivateGroupChat {
|
|
return nil, ErrChatTypeNotSupported
|
|
}
|
|
|
|
response, err := api.s.messenger.EditGroupChat(ctx, chatID, name, color, image)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pubKey := types.EncodeHex(crypto.FromECDSAPub(api.s.messenger.IdentityPublicKey()))
|
|
return api.toAPIChat(response.Chats()[0], nil, pubKey, false)
|
|
}
|