dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

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 }