package api import ( "net/http" "sort" chatclasses "git.golem15.com/golem15/sm-chat-plugin/classes" chatmodels "git.golem15.com/golem15/sm-chat-plugin/models" chanclasses "git.golem15.com/golem15/sm-chatchannels-plugin/classes" usermodels "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/jakub/sm-bm-plugin/classes" "git.golem15.com/jakub/sm-bm-plugin/models" "gorm.io/gorm" ) // chatNonMemberJSON is the whole answer of /chat for a user outside the // chat group. type chatNonMemberJSON struct { Member bool `json:"member"` } // chatBootstrapJSON is the answer of /chat for a chat-group member. type chatBootstrapJSON struct { Member bool `json:"member"` Moderator bool `json:"moderator"` Context chatContextJSON `json:"context"` General chatGeneralJSON `json:"general"` Conversations []chatConversationJSON `json:"conversations"` Limits chatLimitsJSON `json:"limits"` } type chatContextJSON struct { Type string `json:"type"` ID uint `json:"id"` } // chatGeneralJSON is ogólny: its conversation, unread messages and unread // mentions of the caller. type chatGeneralJSON struct { ConversationID uint `json:"conversation_id"` Name string `json:"name"` Unread int64 `json:"unread"` Mentions int64 `json:"mentions"` } // chatConversationJSON is one of the caller's DMs. type chatConversationJSON struct { ID uint `json:"id"` Type string `json:"type"` OtherUserID uint `json:"other_user_id"` Unread int64 `json:"unread"` LastMessage *chatLastMessageJSON `json:"last_message"` } type chatLastMessageJSON struct { ID uint `json:"id"` SenderID uint `json:"sender_id"` Content string `json:"content"` CreatedAt string `json:"created_at"` } // chatLimitsJSON are the composer limits: attachments per message, their // total size and embeds per message. type chatLimitsJSON struct { MaxFiles int `json:"max_files"` MaxBytes int `json:"max_bytes"` MaxEmbeds int `json:"max_embeds"` } // chatMemberJSON is one chat-group member as the mention picker and the DM // picker show them. type chatMemberJSON struct { ID uint `json:"id"` Name string `json:"name"` Surname string `json:"surname"` Handle string `json:"handle"` IsBot bool `json:"is_bot"` } // chatServices returns golem15.chat's and golem15.chatchannels' services, // or answers an opaque 500 and false while either is missing. func (h *Handlers) chatServices(w http.ResponseWriter) (*chatclasses.Service, *chanclasses.ChannelService, bool) { var chat *chatclasses.Service var channels *chanclasses.ChannelService if h.ChatService != nil { chat = h.ChatService() } if h.Channels != nil { channels = h.Channels() } if chat == nil || channels == nil { writeOpaque500(w) return nil, nil, false } return chat, channels, true } // Chat answers GET /chat, the chat bootstrap. A user outside the chat group // gets only member: false and loses any participant rows left in the BM // chat. A member is joined to ogólny (created and adopted as a channel on // first use), the participants are synced with the chat group, and the // answer carries ogólny's unread and mention counts, the caller's DMs // (newest message first), the moderator flag and the composer limits. func (h *Handlers) Chat(w http.ResponseWriter, r *http.Request) { userID, ok := h.caller(w, r) if !ok { return } db, ok := h.db(w, r) if !ok { return } chat, channels, ok := h.chatServices(w) if !ok { return } ctx := r.Context() member, err := classes.IsChatMember(ctx, db, userID, h.ChatGroup) if err != nil { writeOpaque500(w) return } if !member { if err := classes.PruneChatParticipant(ctx, db, userID); err != nil { writeOpaque500(w) return } writeData(w, chatNonMemberJSON{Member: false}) return } general, channel, err := classes.EnsureGeneral(ctx, db, chat, channels, userID) if err != nil { writeOpaque500(w) return } if _, _, err := classes.SyncChatParticipants(ctx, db, general.ID, h.ChatGroup); err != nil { writeOpaque500(w) return } views, err := chat.GetConversationsForUser(ctx, userID, classes.ChatContextType, classes.ChatContextID) if err != nil { writeOpaque500(w) return } mentions, err := channels.MentionCountFor(ctx, userID, channel) if err != nil { writeOpaque500(w) return } moderator, err := classes.IsChatModerator(ctx, db, userID, h.ChatModeratorGroup) if err != nil { writeOpaque500(w) return } out := chatBootstrapJSON{ Member: true, Moderator: moderator, Context: chatContextJSON{Type: classes.ChatContextType, ID: classes.ChatContextID}, General: chatGeneralJSON{ConversationID: general.ID, Name: classes.ChatGeneralName, Mentions: mentions}, Limits: chatLimitsJSON{ MaxFiles: chatclasses.MaxAttachments, MaxBytes: h.ChatMaxUploadBytes, MaxEmbeds: h.ChatMaxEmbeds, }, } dms := []chatclasses.ConversationView{} for _, v := range views { switch { case v.Conversation.ID == general.ID: out.General.Unread = v.UnreadCount case v.Conversation.Type == chatmodels.TypeDM: dms = append(dms, v) } } others, err := otherParticipants(db, dms, userID) if err != nil { writeOpaque500(w) return } out.Conversations = newChatConversationsJSON(dms, others) writeData(w, out) } // otherParticipants maps each DM to the participant who is not userID. func otherParticipants(db *gorm.DB, dms []chatclasses.ConversationView, userID uint) (map[uint]uint, error) { others := map[uint]uint{} if len(dms) == 0 { return others, nil } ids := make([]uint, len(dms)) for i, v := range dms { ids[i] = v.Conversation.ID } var rows []chatmodels.Participant err := db.Select("conversation_id", "user_id"). Where("conversation_id IN ? AND user_id <> ?", ids, userID). Order("id").Find(&rows).Error if err != nil { return nil, err } for _, p := range rows { if _, ok := others[p.ConversationID]; !ok { others[p.ConversationID] = p.UserID } } return others, nil } // newChatConversationsJSON lists the DMs newest message first; DMs without // a message come last, newest conversation first. func newChatConversationsJSON(dms []chatclasses.ConversationView, others map[uint]uint) []chatConversationJSON { sort.SliceStable(dms, func(i, j int) bool { a, b := dms[i].LastMessage, dms[j].LastMessage switch { case a == nil && b == nil: return dms[i].Conversation.ID > dms[j].Conversation.ID case a == nil || b == nil: return b == nil case a.CreatedAt == nil || b.CreatedAt == nil: return a.ID > b.ID case !a.CreatedAt.Equal(*b.CreatedAt): return a.CreatedAt.After(*b.CreatedAt) } return a.ID > b.ID }) out := make([]chatConversationJSON, 0, len(dms)) for _, v := range dms { row := chatConversationJSON{ ID: v.Conversation.ID, Type: chatmodels.TypeDM, OtherUserID: others[v.Conversation.ID], Unread: v.UnreadCount, } if m := v.LastMessage; m != nil { last := &chatLastMessageJSON{ID: m.ID, SenderID: m.SenderID} if m.Content != nil { last.Content = *m.Content } if m.CreatedAt != nil { last.CreatedAt = models.FormatDateTime(*m.CreatedAt) } row.LastMessage = last } out = append(out, row) } return out } // ChatMembers answers GET /chat/members: the chat-group members with their // name, surname, @handle and bot flag, ordered by id. Members without a // handle get one first. Anyone outside the chat group gets 404. func (h *Handlers) ChatMembers(w http.ResponseWriter, r *http.Request) { userID, ok := h.caller(w, r) if !ok { return } db, ok := h.db(w, r) if !ok { return } ctx := r.Context() member, err := classes.IsChatMember(ctx, db, userID, h.ChatGroup) if err != nil { writeOpaque500(w) return } if !member { writeNotFound(w) return } if _, err := classes.AssignMissingHandles(ctx, db, h.ChatGroup); err != nil { writeOpaque500(w) return } ids, err := classes.ChatMemberIDs(ctx, db, h.ChatGroup) if err != nil { writeOpaque500(w) return } out := make([]chatMemberJSON, 0, len(ids)) if len(ids) == 0 { writeData(w, out) return } var users []usermodels.User if err := db.Select("id", "name", "surname", "handle").Where("id IN ?", ids).Order("id").Find(&users).Error; err != nil { writeOpaque500(w) return } bots := map[uint]bool{} if h.ChatBots { var botIDs []uint if err := db.Table("golem15_chatbots_bot_flags").Where("is_bot AND user_id IN ?", ids).Pluck("user_id", &botIDs).Error; err != nil { writeOpaque500(w) return } for _, id := range botIDs { bots[id] = true } } for _, u := range users { out = append(out, chatMemberJSON{ ID: u.ID, Name: deref(u.Name), Surname: deref(u.Surname), Handle: deref(u.Handle), IsBot: bots[u.ID], }) } writeData(w, out) } func deref(s *string) string { if s == nil { return "" } return *s }