tableChatRooms.go (7043B)
1 package database 2 3 import ( 4 "dkforest/pkg/config" 5 "dkforest/pkg/utils" 6 "time" 7 8 hutils "dkforest/pkg/web/handlers/utils" 9 "github.com/labstack/echo" 10 "github.com/sirupsen/logrus" 11 ) 12 13 type RoomID int64 14 15 func (r RoomID) String() string { 16 return utils.FormatInt64(int64(r)) 17 } 18 19 type ChatRoom struct { 20 ID RoomID 21 Name string 22 ExternalLink string 23 OwnerUserID *UserID 24 Password string // Hashed password (sha512) 25 IsListed bool 26 IsEphemeral bool 27 ReadOnly bool 28 CreatedAt time.Time 29 OwnerUser *User 30 Mode int64 31 } 32 33 const ( 34 NormalRoomMode = 0 35 UserWhitelistRoomMode = 1 36 ) 37 38 func (d *DkfDB) CreateRoom(name string, passwordHash string, ownerID UserID, isListed bool) (out ChatRoom, err error) { 39 out = ChatRoom{ 40 Name: name, 41 Password: passwordHash, 42 OwnerUserID: &ownerID, 43 IsListed: isListed, 44 IsEphemeral: true, 45 } 46 err = d.db.Create(&out).Error 47 return 48 } 49 50 func GetRoomPasswordHash(password string) string { 51 return utils.Sha512(getRoomSaltedPasswordBytes(password)) 52 } 53 54 func GetRoomDecryptionKey(password string) string { 55 return utils.Sha256(getRoomSaltedPasswordBytes(password))[:32] 56 } 57 58 func getRoomSaltedPasswordBytes(password string) []byte { 59 return getSaltedPasswordBytes(config.RoomPasswordSalt, password) 60 } 61 62 func getSaltedPasswordBytes(salt, password string) []byte { 63 return []byte(salt + password) 64 } 65 66 // IsOwned returns either or not a user created the room 67 func (r *ChatRoom) IsOwned() bool { 68 return r.OwnerUserID != nil 69 } 70 71 func (r *ChatRoom) IsRoomOwner(userID UserID) bool { 72 return r.OwnerUserID != nil && *r.OwnerUserID == userID 73 } 74 75 func (r *ChatRoom) VerifyPasswordHash(passwordHash string) bool { 76 return r.Password == passwordHash 77 } 78 79 func (r *ChatRoom) IsProtected() bool { 80 return r.Password != "" 81 } 82 83 func (r *ChatRoom) DoSave(db *DkfDB) { 84 if err := db.db.Save(r).Error; err != nil { 85 logrus.Error(err) 86 } 87 } 88 89 func (r *ChatRoom) IsOfficialRoom() bool { 90 return r.Name == "general" || 91 r.Name == "announcements" || 92 r.Name == "suggestions" || 93 r.Name == "moderators" || 94 r.Name == "club" 95 } 96 97 func (r *ChatRoom) HasAccess(c echo.Context) (bool, string) { 98 authUser := c.Get("authUser").(*User) 99 db := c.Get("database").(*DkfDB) 100 if authUser == nil { 101 return false, "" 102 } 103 if r.Name == "club" && !authUser.IsClubMember { 104 return false, "" 105 } 106 if r.Name == "moderators" && !authUser.IsModerator() { 107 return false, "" 108 } 109 if r.Mode == UserWhitelistRoomMode { 110 if !r.IsRoomOwner(authUser.ID) { 111 if !db.IsUserWhitelistedInRoom(authUser.ID, r.ID) { 112 return false, "" 113 } 114 } 115 } 116 if !r.IsProtected() { 117 return true, "" 118 } 119 cookie, err := hutils.GetRoomCookie(c, int64(r.ID)) 120 if err != nil { 121 return false, "" 122 } 123 if !r.VerifyPasswordHash(cookie.Value) { 124 hutils.DeleteRoomCookie(c, int64(r.ID)) 125 return false, "" 126 } 127 cookie, err = hutils.GetRoomKeyCookie(c, int64(r.ID)) 128 if err != nil { 129 return false, "" 130 } 131 return true, cookie.Value 132 } 133 134 func (d *DkfDB) GetChatRoomsByID(roomIDs []RoomID) (out []ChatRoom, err error) { 135 err = d.db.Where("id IN (?)", roomIDs).Find(&out).Error 136 return 137 } 138 139 func (d *DkfDB) GetChatRoomByID(roomID RoomID) (out ChatRoom, err error) { 140 err = d.db.Where("id = ?", roomID).First(&out).Error 141 return 142 } 143 144 func (d *DkfDB) GetChatRoomByName(roomName string) (out ChatRoom, err error) { 145 err = d.db.Where("name = ?", roomName).First(&out).Error 146 return 147 } 148 149 func (d *DkfDB) DeleteChatRoomByID(id RoomID) { 150 if err := d.db.Delete(ChatRoom{}, "id = ?", id).Error; err != nil { 151 logrus.Error(err) 152 } 153 } 154 155 type ChatRoomAug struct { 156 ChatRoom 157 OwnerUser *User `gorm:"embedded"` // https://gorm.io/docs/models.html#Embedded-Struct 158 IsUnread bool 159 } 160 161 type ChatRoomAug1 struct { 162 Name string 163 IsUnread bool 164 } 165 166 // GetOfficialChatRooms1 returns official chat rooms with additional information such as "IsUnread" 167 func (d *DkfDB) GetOfficialChatRooms1(userID UserID) (out []ChatRoomAug1, err error) { 168 err = d.db.Raw(`SELECT r.name, 169 COALESCE((rr.read_at < m.created_at), 1) as is_unread 170 FROM chat_rooms r 171 -- Find last message for room 172 LEFT JOIN chat_messages m ON m.id = (SELECT max(id) FROM chat_messages WHERE room_id = r.id AND (to_user_id IS NULL OR to_user_id = ?)) 173 -- Get read record for the authUser & room 174 LEFT JOIN chat_read_records rr ON rr.user_id = ? AND rr.room_id = r.id 175 WHERE r.name IN ('general', 'programming', 'hacking', 'suggestions', 'club', 'moderators', 'announcements') 176 ORDER BY r.id ASC`, userID, userID).Scan(&out).Error 177 return 178 } 179 180 func (d *DkfDB) GetUserRoomSubscriptions(userID UserID) (out []ChatRoomAug1, err error) { 181 err = d.db.Raw(`SELECT r.name, 182 COALESCE((rr.read_at < m.created_at), 1) as is_unread 183 FROM user_room_subscriptions s 184 INNER JOIN chat_rooms r ON r.id = s.room_id 185 -- Find last message for room 186 LEFT JOIN chat_messages m ON m.id = (SELECT max(id) FROM chat_messages WHERE room_id = r.id AND (to_user_id IS NULL OR to_user_id = ?)) 187 -- Get read record for the authUser & room 188 LEFT JOIN chat_read_records rr ON rr.user_id = ? AND rr.room_id = r.id 189 WHERE s.user_id = ? 190 ORDER BY r.id ASC`, userID, userID, userID).Scan(&out).Error 191 return 192 } 193 194 func (d *DkfDB) GetListedChatRooms(userID UserID) (out []ChatRoomAug, err error) { 195 err = d.db.Raw(`SELECT r.*, 196 u.*, 197 COALESCE((rr.read_at < m.created_at), 1) as is_unread 198 FROM chat_rooms r 199 -- Join OwnerUser 200 INNER JOIN users u ON r.owner_user_id = u.id 201 -- Find last message for room 202 LEFT JOIN chat_messages m ON m.id = (SELECT max(id) FROM chat_messages WHERE room_id = r.id AND (to_user_id IS NULL OR to_user_id = ?)) 203 -- Get read record for the authUser & room 204 LEFT JOIN chat_read_records rr ON rr.user_id = ? AND rr.room_id = r.id 205 WHERE r.is_listed = 1 206 ORDER BY r.id ASC`, userID, userID).Scan(&out).Error 207 return 208 } 209 210 func (d *DkfDB) GetOfficialChatRooms() (out []ChatRoom, err error) { 211 rooms := []string{"general", "club", "moderators"} 212 err = d.db.Where("name IN (?)", rooms).Find(&out).Error 213 return 214 } 215 216 func (d *DkfDB) DeleteOldPrivateChatRooms() { 217 d.db.Exec(`DELETE FROM chat_rooms 218 WHERE owner_user_id IS NOT NULL 219 AND is_ephemeral = 1 220 AND ((SELECT chat_messages.created_at FROM chat_messages WHERE chat_messages.room_id = chat_rooms.id ORDER BY chat_messages.ID DESC) < date('now', '-1 Day') 221 OR (SELECT COUNT(*) FROM chat_messages WHERE chat_messages.room_id = chat_rooms.id) == 0) 222 AND chat_rooms.created_at < date('now', '-1 Day');`) 223 } 224 225 // ChatReadRecord use to keep track of last message read (loaded) in a room, for rooms new message indicator. 226 // ie: a room you're not currently in, changes color if new messages are posted in it. 227 type ChatReadRecord struct { 228 UserID UserID 229 RoomID RoomID 230 ReadAt time.Time 231 } 232 233 func (r *ChatReadRecord) DoSave(db *DkfDB) { 234 if err := db.db.Save(r).Error; err != nil { 235 logrus.Error(err) 236 } 237 } 238 239 func (d *DkfDB) UpdateChatReadRecord(userID UserID, roomID RoomID) { 240 now := time.Now() 241 res := d.db.Exec(`UPDATE chat_read_records SET read_at = ? WHERE user_id = ? AND room_id = ?`, now, userID, roomID) 242 if res.RowsAffected == 0 { 243 d.db.Create(ChatReadRecord{UserID: userID, RoomID: roomID, ReadAt: now}) 244 } 245 }