dkforest

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

tableChatMessages.go (19829B)


      1 package database
      2 
      3 import (
      4 	"crypto/cipher"
      5 	"crypto/rand"
      6 	"dkforest/pkg/config"
      7 	"dkforest/pkg/pubsub"
      8 	"dkforest/pkg/utils"
      9 	"encoding/json"
     10 	"errors"
     11 	"fmt"
     12 	"gorm.io/gorm"
     13 	"io"
     14 	"math"
     15 	"net/url"
     16 	"regexp"
     17 	"strings"
     18 	"time"
     19 
     20 	"github.com/google/uuid"
     21 	"github.com/sirupsen/logrus"
     22 )
     23 
     24 type ChatMessages []ChatMessage
     25 
     26 func (m *ChatMessage) Decrypt(key string) error {
     27 	aesgcm, _, err := utils.GetGCM(key)
     28 	if err != nil {
     29 		return err
     30 	}
     31 	m.Message = decrypt(m.Message, aesgcm)
     32 	return nil
     33 }
     34 
     35 func (m ChatMessages) DecryptAll(key string) error {
     36 	aesgcm, _, err := utils.GetGCM(key)
     37 	if err != nil {
     38 		return err
     39 	}
     40 	for i := 0; i < len(m); i++ {
     41 		m[i].Message = decrypt(m[i].Message, aesgcm)
     42 	}
     43 	return nil
     44 }
     45 
     46 func (m ChatMessages) DecryptAllRaw(key string) error {
     47 	aesgcm, _, err := utils.GetGCM(key)
     48 	if err != nil {
     49 		return err
     50 	}
     51 	for i := 0; i < len(m); i++ {
     52 		m[i].RawMessage = decrypt(m[i].RawMessage, aesgcm)
     53 	}
     54 	return nil
     55 }
     56 
     57 func decrypt(msg string, aesgcm cipher.AEAD) string {
     58 	nonceSize := aesgcm.NonceSize()
     59 	msgBytes := []byte(msg)
     60 	if len(msgBytes) < nonceSize {
     61 		msg = "<Failed to decrypt message>"
     62 		return msg
     63 	}
     64 	nonce, ciphertext := msgBytes[:nonceSize], msgBytes[nonceSize:]
     65 	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
     66 	if err != nil {
     67 		msg = "<Failed to decrypt message>"
     68 	} else {
     69 		msg = string(plaintext)
     70 	}
     71 	return msg
     72 }
     73 
     74 type ChatMessage struct {
     75 	ID           int64
     76 	UUID         string
     77 	Message      string
     78 	RawMessage   string
     79 	RoomID       RoomID
     80 	UserID       UserID
     81 	ToUserID     *UserID
     82 	GroupID      *GroupID
     83 	UploadID     *UploadID
     84 	CreatedAt    time.Time
     85 	User         User
     86 	Room         ChatRoom
     87 	ToUser       *User
     88 	Group        *ChatRoomGroup
     89 	System       bool
     90 	Moderators   bool
     91 	IsHellbanned bool
     92 	Rev          int64 // Revision, is incr every time a message is edited
     93 	SkipNotify   bool  `gorm:"-"`
     94 }
     95 
     96 func (m *ChatMessage) GetProfile(authUserID UserID) Username {
     97 	if m.ToUserID != nil && *m.ToUserID != authUserID {
     98 		return m.ToUser.Username
     99 	}
    100 	return m.User.Username
    101 }
    102 
    103 // GetRawMessage get RawMessage value, decrypt it if needed
    104 func (m *ChatMessage) GetRawMessage(key string) (string, error) {
    105 	if !m.Room.IsProtected() {
    106 		return m.RawMessage, nil
    107 	}
    108 	if key == "" {
    109 		return "", errors.New("room key not provided")
    110 	}
    111 	decrypted, err := decryptMessageWithKey(key, m.RawMessage)
    112 	if err != nil {
    113 		return "", err
    114 	}
    115 	return decrypted, nil
    116 }
    117 
    118 func decryptMessageWithKey(key, msg string) (string, error) {
    119 	aesgcm, nonceSize, err := utils.GetGCM(key)
    120 	if err != nil {
    121 		return "", err
    122 	}
    123 
    124 	msgBytes := []byte(msg)
    125 	nonce, ciphertext := msgBytes[:nonceSize], msgBytes[nonceSize:]
    126 	plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
    127 	var out string
    128 	if err != nil {
    129 		out = "<Failed to decrypt message>"
    130 	} else {
    131 		out = string(plaintext)
    132 	}
    133 	return out, nil
    134 }
    135 
    136 func (m *ChatMessage) MarshalJSON() ([]byte, error) {
    137 	var out struct {
    138 		UUID         string
    139 		Message      string
    140 		RawMessage   string
    141 		Username     Username
    142 		ToUsername   *Username `json:"ToUsername,omitempty"`
    143 		CreatedAt    string
    144 		IsHellbanned bool
    145 	}
    146 	out.UUID = m.UUID
    147 	out.Message = m.Message
    148 	out.RawMessage = m.RawMessage
    149 	out.Username = m.User.Username
    150 	out.IsHellbanned = m.IsHellbanned
    151 	if m.ToUser != nil {
    152 		out.ToUsername = &m.ToUser.Username
    153 	}
    154 	out.CreatedAt = m.CreatedAt.Format("2006-01-02T15:04:05")
    155 	return json.Marshal(out)
    156 }
    157 
    158 func (m *ChatMessage) UserCanSee(user IUserRenderMessage) bool {
    159 	// If user is not moderator, cannot see "moderators only" messages
    160 	if m.Moderators && !user.IsModerator() {
    161 		return false
    162 	}
    163 	// msg is HB and user is not hb
    164 	if m.IsHellbanned && !user.GetIsHellbanned() {
    165 		// Cannot see hb if you're not a mod or CanSeeHellbanned is disabled
    166 		cannotSeeHB := !(user.IsModerator() || user.GetCanSeeHellbanned())
    167 		// user cannot see hb OR user disabled hb display
    168 		if cannotSeeHB || !user.GetDisplayHellbanned() {
    169 			return false
    170 		}
    171 	}
    172 	// msg user is not hb || own msg || msg user is hb & user is also hb || user can see and wish to see hb
    173 	return !m.User.IsHellbanned || m.OwnMessage(user.GetID()) || (m.User.IsHellbanned && user.GetIsHellbanned()) || (user.CanSeeHB() && user.GetDisplayHellbanned())
    174 }
    175 
    176 func (m *ChatMessage) DeleteSecondsRemaining() int64 {
    177 	return int64(math.Max((config.EditMessageTimeLimit - time.Since(m.CreatedAt)).Seconds(), 0))
    178 }
    179 
    180 func (m *ChatMessage) CanBeEdited() bool {
    181 	return time.Since(m.CreatedAt) <= config.EditMessageTimeLimit
    182 }
    183 
    184 func (m *ChatMessage) UserCanDelete(user IUserRenderMessage) bool {
    185 	return m.UserCanDeleteErr(user) == nil
    186 }
    187 
    188 // UserCanDeleteErr returns either or not "user" can delete the messages "m"
    189 func (m *ChatMessage) UserCanDeleteErr(user IUserRenderMessage) error {
    190 	// Admin can delete everything
    191 	if user.GetIsAdmin() {
    192 		return nil
    193 	}
    194 	// room owner can delete any messages in their room
    195 	if m.IsRoomOwner(user.GetID()) {
    196 		return nil
    197 	}
    198 	// User can delete PMs from user 0
    199 	if m.IsPmRecipient(user.GetID()) && m.User.Username == config.NullUsername {
    200 		return nil
    201 	}
    202 	// Own messages can be deleted if not too old
    203 	if m.UserID == user.GetID() {
    204 		if m.TooOldToDelete() {
    205 			return errors.New("message is too old to be deleted")
    206 		}
    207 		return nil
    208 	}
    209 	// Moderators cannot delete vetted user messages
    210 	if user.IsModerator() && m.User.Vetted {
    211 		return errors.New("cannot delete message of vetted user")
    212 	}
    213 	// Mod cannot delete admin
    214 	if user.IsModerator() && m.User.IsAdmin {
    215 		return errors.New("cannot delete message of admin user")
    216 	}
    217 	// Mod cannot delete mod
    218 	if user.IsModerator() && m.User.IsModerator() {
    219 		return errors.New("cannot delete message of moderator user")
    220 	}
    221 	// Mod can delete messages they don't own
    222 	if user.IsModerator() {
    223 		return nil
    224 	}
    225 	// Cannot delete message you don't own
    226 	return errors.New("cannot delete this message")
    227 }
    228 
    229 func (m *ChatMessage) TooOldToDelete() bool {
    230 	// PM sent by "0" can always be deleted
    231 	if m.ToUserID != nil && m.User.Username == config.NullUsername {
    232 		return false
    233 	}
    234 	return time.Since(m.CreatedAt) > config.EditMessageTimeLimit
    235 }
    236 
    237 func (m *ChatMessage) OwnMessage(userID UserID) bool {
    238 	return m.UserID == userID
    239 }
    240 
    241 func (m *ChatMessage) IsPm() bool {
    242 	return m.ToUserID != nil
    243 }
    244 
    245 func (m *ChatMessage) IsPmRecipient(userID UserID) bool {
    246 	return m.ToUserID != nil && *m.ToUserID == userID
    247 }
    248 
    249 func (m *ChatMessage) IsRoomOwner(userID UserID) bool {
    250 	return m.Room.IsRoomOwner(userID)
    251 }
    252 
    253 func (m *ChatMessage) IsMe() bool {
    254 	return strings.HasPrefix(m.Message, "<p>/me ")
    255 }
    256 
    257 func (m *ChatMessage) TrimMe() string {
    258 	return "<p>" + strings.TrimPrefix(m.Message, "<p>/me ")
    259 }
    260 
    261 var externalLinkRgx = regexp.MustCompile(`<a href="([^"]+)" rel="noopener noreferrer" target="_blank">`)
    262 
    263 func (m *ChatMessage) MsgToDisplay(authUser IUserRenderMessage) string {
    264 	var msg string
    265 	if m.IsMe() {
    266 		msg = m.TrimMe()
    267 	} else {
    268 		msg = m.Message
    269 	}
    270 	if authUser.GetConfirmExternalLinks() {
    271 		msg = externalLinkRgx.ReplaceAllStringFunc(msg, func(s string) string {
    272 			original := externalLinkRgx.FindStringSubmatch(s)[1]
    273 			if strings.HasPrefix(original, "/") || strings.HasPrefix(original, "?") {
    274 				return s
    275 			}
    276 			return `<a href="/external-link/` + url.PathEscape(original) + `" rel="noopener noreferrer" target="_blank">`
    277 		})
    278 	}
    279 	return msg
    280 }
    281 
    282 func (m *ChatMessage) Delete(db *DkfDB) error {
    283 	// If we delete message manually, also delete linked inbox if any
    284 	_ = db.DeleteChatInboxMessageByChatMessageID(m.ID)
    285 	err := db.DeleteChatMessageByUUID(m.UUID)
    286 	MsgPubSub.Pub("room_"+m.RoomID.String(), ChatMessageType{Typ: DeleteMsg, Msg: *m})
    287 	return err
    288 }
    289 
    290 func (m *ChatMessage) DoSave(db *DkfDB) {
    291 	if err := db.db.Save(m).Error; err != nil {
    292 		logrus.Error(err)
    293 	}
    294 }
    295 
    296 func (d *DkfDB) GetUserLastChatMessageInRoom(userID UserID, roomID RoomID) (out ChatMessage, err error) {
    297 	err = d.db.
    298 		Where("user_id = ? AND room_id = ?", userID, roomID).
    299 		Order("id DESC").
    300 		Preload("User").
    301 		Preload("ToUser").
    302 		Preload("Room").
    303 		Preload("Group").
    304 		First(&out).Error
    305 	return
    306 }
    307 
    308 // RoomChatMessagesGeIncrRev increments revision counter of all messages newer than chatMessageID
    309 func (d *DkfDB) RoomChatMessagesGeIncrRev(roomID RoomID, chatMessageID int64) (err error) {
    310 	err = d.db.
    311 		Exec(`UPDATE chat_messages SET rev = rev + 1  WHERE room_id = ? AND id > ?`, roomID, chatMessageID).
    312 		Error
    313 	return
    314 }
    315 
    316 func (d *DkfDB) GetRoomChatMessages(roomID RoomID) (out ChatMessages, err error) {
    317 	err = d.db.
    318 		Where("room_id = ?", roomID).
    319 		Preload("User").
    320 		Preload("ToUser").
    321 		Preload("Room").
    322 		Preload("Group").
    323 		Find(&out).Error
    324 	return
    325 }
    326 
    327 func (d *DkfDB) GetChatMessageByUUID(msgUUID string) (out ChatMessage, err error) {
    328 	err = d.db.
    329 		Where("uuid = ?", msgUUID).
    330 		Preload("User").
    331 		Preload("ToUser").
    332 		Preload("Room").
    333 		Preload("Group").
    334 		First(&out).Error
    335 	return
    336 }
    337 
    338 func (d *DkfDB) GetRoomChatMessageByUUID(roomID RoomID, msgUUID string) (out ChatMessage, err error) {
    339 	err = d.db.
    340 		Where("room_id = ? AND uuid = ?", roomID, msgUUID).
    341 		Preload("User").
    342 		Preload("ToUser").
    343 		Preload("Room").
    344 		Preload("Group").
    345 		First(&out).Error
    346 	return
    347 }
    348 
    349 func (d *DkfDB) GetRoomChatMessageByDate(roomID RoomID, userID UserID, dt time.Time) (out ChatMessage, err error) {
    350 	err = d.db.
    351 		Select("*, strftime('%Y-%m-%d %H:%M:%S', created_at) as created_at1").
    352 		Where("room_id = ? AND user_id = ? AND created_at1 = ?", roomID, userID, dt.Format("2006-01-02 15:04:05")).
    353 		Preload("User").
    354 		Preload("ToUser").
    355 		Preload("Room").
    356 		First(&out).Error
    357 	return
    358 }
    359 
    360 func (d *DkfDB) GetRoomChatMessagesByDate(roomID RoomID, dt time.Time) (out []ChatMessage, err error) {
    361 	err = d.db.
    362 		Select("*, strftime('%m-%d %H:%M:%S', created_at) as created_at1").
    363 		Where("room_id = ? AND created_at1 = ?", roomID, dt.Format("01-02 15:04:05")).
    364 		Preload("User").
    365 		Preload("ToUser").
    366 		Preload("Room").
    367 		Order("id DESC").
    368 		Find(&out).Error
    369 	return
    370 }
    371 
    372 type PmDisplayMode int64
    373 
    374 const (
    375 	PmNoFilter PmDisplayMode = iota
    376 	PmOnly
    377 	PmNone
    378 )
    379 
    380 func (d *DkfDB) GetChatMessages(roomID RoomID, roomKey string, username Username, userID UserID,
    381 	pmUserID *UserID, displayPms PmDisplayMode, mentionsOnly, displayHellbanned, displayIgnored, displayModerators,
    382 	displayIgnoredMessages bool, msgsLimit, minID1 int64) (out ChatMessages, err error) {
    383 
    384 	cmp := func(t, t2 ChatMessage) bool { return t.ID > t2.ID }
    385 
    386 	q := d.db.
    387 		Preload("User").
    388 		Preload("ToUser").
    389 		Preload("Room").
    390 		Preload("Group").
    391 		Limit(int(msgsLimit)).
    392 		Where(`room_id = ?`, roomID)
    393 	if minID1 > 0 {
    394 		q = q.Where("id >= ?", minID1)
    395 		q = q.Order("id ASC")
    396 	} else {
    397 		q = q.Order("id DESC")
    398 	}
    399 	q = q.Where(`group_id IS NULL OR group_id IN (SELECT group_id FROM chat_room_user_groups g WHERE g.room_id = ? AND g.user_id = ?)`, roomID, userID)
    400 	if !displayIgnoredMessages {
    401 		q = q.Where(`id NOT IN (SELECT message_id FROM ignored_messages WHERE user_id = ?)`, userID)
    402 	}
    403 	if !displayIgnored {
    404 		q = q.Where(`user_id NOT IN (SELECT ignored_user_id FROM ignored_users WHERE user_id = ?)`, userID)
    405 	}
    406 	if !displayModerators {
    407 		q = q.Where(`moderators = 0`)
    408 	}
    409 	if mentionsOnly {
    410 		q = q.Where(`raw_message LIKE ?`, "%@"+username+"%")
    411 	}
    412 	if pmUserID != nil {
    413 		q = q.Where(`(to_user_id = ? AND user_id = ?) OR (user_id = ? AND to_user_id = ?)`, userID, pmUserID, userID, pmUserID)
    414 	}
    415 	switch displayPms {
    416 	case PmNoFilter: // Display all messages
    417 		q = q.Where(`to_user_id is null OR to_user_id = ? OR user_id = ?`, userID, userID)
    418 	case PmOnly: // Display PMs only
    419 		q = q.Where(`to_user_id = ? OR (user_id = ? AND to_user_id IS NOT NULL)`, userID, userID)
    420 	case PmNone: // No PMs displayed
    421 		q = q.Where(`to_user_id is null`)
    422 	}
    423 
    424 	//-----------
    425 
    426 	q1 := q.Session(&gorm.Session{})
    427 	q1 = q1.Where("is_hellbanned = 0")
    428 	var out1 []ChatMessage
    429 	if err = q1.Find(&out1).Error; err != nil {
    430 		return out, err
    431 	}
    432 
    433 	var minID int64
    434 	if len(out1) > 0 {
    435 		minID = out1[len(out1)-1].ID
    436 	}
    437 
    438 	//-----------
    439 
    440 	// Get all the HB messages that are more recent than the oldest non-HB message.
    441 	// We do this in case someone in HB keep spamming the room.
    442 	// So we still have 150 non-HB messages for normal folks and we get all the spam for the people in HB.
    443 
    444 	var out2 []ChatMessage
    445 	if displayHellbanned {
    446 		q2 := q.Session(&gorm.Session{})
    447 		q2 = q2.Where("is_hellbanned = 1 AND id > ?", minID)
    448 		if minID1 > 0 {
    449 			q2 = q2.Where("is_hellbanned = 1")
    450 		}
    451 		if err = q2.Find(&out2).Error; err != nil {
    452 			return out, err
    453 		}
    454 	}
    455 
    456 	out = sortedMerge(out1, out2, cmp)
    457 
    458 	if roomKey != "" {
    459 		if err := out.DecryptAll(roomKey); err != nil {
    460 			return out, err
    461 		}
    462 	}
    463 
    464 	return out, nil
    465 }
    466 
    467 // merge two sorted slices. The output will also be sorted.
    468 func sortedMerge[T any](a, b []T, less func(T, T) bool) []T {
    469 	out := make([]T, len(a)+len(b))
    470 	// "i" is a pointer for slice "a"
    471 	// "j" is a pointer for slice "b"
    472 	// "k" is a pointer for the output slice
    473 	var i, j, k int
    474 	// Loop until we reach the end of either "a" or "b"
    475 	for i < len(a) && j < len(b) {
    476 		if less(a[i], b[j]) {
    477 			out[k] = a[i]
    478 			i++
    479 		} else {
    480 			out[k] = b[j]
    481 			j++
    482 		}
    483 		k++
    484 	}
    485 	// At this point only "a" or "b" will have remaining items.
    486 	// If "a" still have items, finish it.
    487 	for i < len(a) {
    488 		out[k] = a[i]
    489 		k++
    490 		i++
    491 	}
    492 	// Otherwise, if "b" still have items, finish it.
    493 	for j < len(b) {
    494 		out[k] = b[j]
    495 		k++
    496 		j++
    497 	}
    498 	return out
    499 }
    500 
    501 func (d *DkfDB) DeleteChatRoomMessages(roomID RoomID) error {
    502 	return d.db.Delete(&ChatMessage{}, "room_id = ?", roomID).Error
    503 }
    504 
    505 func (d *DkfDB) DeleteChatMessageByUUID(messageUUID string) error {
    506 	return d.db.Where("uuid = ?", messageUUID).Delete(&ChatMessage{}).Error
    507 }
    508 
    509 func (d *DkfDB) DeleteUserChatMessages(userID UserID) error {
    510 	return d.db.Where("user_id = ?", userID).Delete(&ChatMessage{}).Error
    511 }
    512 
    513 func (d *DkfDB) DeleteUserHbChatMessages(userID UserID) error {
    514 	return d.db.Where("user_id = ? AND is_hellbanned = 1", userID).Delete(&ChatMessage{}).Error
    515 }
    516 
    517 func (d *DkfDB) DeleteUserChatMessagesOpt(userID UserID, hbOnly bool, secs int64) error {
    518 	q := d.db.Where("user_id = ?", userID)
    519 	if secs > 0 {
    520 		secsStr := "-" + utils.FormatInt64(secs) + " Second"
    521 		q = q.Where("created_at > datetime('now', ?, 'localtime')", secsStr)
    522 	}
    523 	if hbOnly {
    524 		q = q.Where("is_hellbanned = 1")
    525 	}
    526 	err := q.Delete(&ChatMessage{}).Error
    527 	return err
    528 }
    529 
    530 func (d *DkfDB) DeleteOldChatMessages() {
    531 	rooms, _ := d.GetOfficialChatRooms()
    532 	for _, room := range rooms {
    533 		d.db.Exec(`
    534 DELETE FROM chat_messages
    535 -- Don't delete the last 500 "non PM" and "not hellbanned" messages
    536 WHERE id NOT IN (
    537 	SELECT id FROM chat_messages
    538 	WHERE room_id = ? AND is_hellbanned = 0
    539 	ORDER BY id DESC
    540 	LIMIT 500
    541 )
    542 -- Don't delete messages that were created in the past 24h
    543 AND id NOT IN (
    544 	SELECT id FROM chat_messages
    545 	WHERE room_id = ?
    546 		AND created_at >= date('now', '-1 Day')
    547  		AND is_hellbanned = 0
    548 )
    549 -- Don't delete the last 500 hellbanned messages
    550 AND id NOT IN (
    551 	SELECT id FROM chat_messages
    552 	WHERE room_id = ? AND is_hellbanned = 1
    553 	ORDER BY id DESC
    554 	LIMIT 500
    555 )
    556 AND room_id = ?
    557 `, room.ID, room.ID, room.ID, room.ID)
    558 	}
    559 }
    560 
    561 func makeMsg(raw, txt string, roomID RoomID, userID UserID) ChatMessage {
    562 	return ChatMessage{
    563 		UUID:       uuid.New().String(),
    564 		Message:    txt,
    565 		RawMessage: raw,
    566 		RoomID:     roomID,
    567 		UserID:     userID,
    568 	}
    569 }
    570 
    571 func (d *DkfDB) CreateMsg(raw, txt, roomKey string, roomID RoomID, userID UserID, toUserID *UserID, hellbanMsg bool) (out ChatMessage, err error) {
    572 	return d.createMsg(raw, txt, roomKey, roomID, userID, toUserID, hellbanMsg, false, false)
    573 }
    574 
    575 func (d *DkfDB) CreateSysMsg(raw, txt, roomKey string, roomID RoomID, userID UserID) error {
    576 	return d.CreateSysMsgPM(raw, txt, roomKey, roomID, userID, nil, false)
    577 }
    578 
    579 func (d *DkfDB) CreateSysMsgPM(raw, txt, roomKey string, roomID RoomID, userID UserID, toUserID *UserID, skipNotify bool) error {
    580 	return utils.Second(d.createMsg(raw, txt, roomKey, roomID, userID, toUserID, false, true, skipNotify))
    581 }
    582 
    583 func (d *DkfDB) CreateKickMsg(kickedUser, kickedByUser User) {
    584 	d.createKickMsg(kickedUser, kickedByUser, "%s has been kicked. (%s)")
    585 }
    586 
    587 func (d *DkfDB) CreateUnkickMsg(kickedUser, kickedByUser User) {
    588 	d.createKickMsg(kickedUser, kickedByUser, "%s has been unkicked. (%s)")
    589 }
    590 
    591 func (d *DkfDB) createKickMsg(kickedUser, kickedByUser User, format string) {
    592 	styledUsername := fmt.Sprintf(`<span %s>%s</span>`, kickedUser.GenerateChatStyle(), kickedUser.Username)
    593 	rawTxt := fmt.Sprintf(format, kickedUser.Username, kickedByUser.Username)
    594 	txt := fmt.Sprintf(format, styledUsername, kickedByUser.Username)
    595 	utils.LogErr(d.CreateSysMsg(rawTxt, txt, "", config.GeneralRoomID, kickedByUser.ID))
    596 }
    597 
    598 func (d *DkfDB) createMsg(raw, txt, roomKey string, roomID RoomID, userID UserID, toUserID *UserID, hellbanMsg, system, skipNotify bool) (out ChatMessage, err error) {
    599 	txt, raw, err = encryptWithRoomKey(txt, raw, roomKey)
    600 	if err != nil {
    601 		return
    602 	}
    603 	out = makeMsg(raw, txt, roomID, userID)
    604 	out.SkipNotify = skipNotify
    605 	out.ToUserID = toUserID
    606 	out.IsHellbanned = hellbanMsg
    607 	out.System = system
    608 	err = d.db.Create(&out).Error
    609 	MsgPubSub.Pub("room_"+roomID.String(), ChatMessageType{Typ: CreateMsg, Msg: out})
    610 	return
    611 }
    612 
    613 func (d *DkfDB) CreateOrEditMessage(
    614 	editMsg *ChatMessage,
    615 	message, raw, roomKey string,
    616 	roomID RoomID,
    617 	fromUserID UserID,
    618 	toUserID *UserID,
    619 	upload *Upload,
    620 	groupID *GroupID,
    621 	hellbanMsg, modMsg, systemMsg bool) (int64, error) {
    622 
    623 	var err error
    624 	message, raw, err = encryptWithRoomKey(message, raw, roomKey)
    625 	if err != nil {
    626 		return 0, err
    627 	}
    628 
    629 	typ := CreateMsg
    630 	if editMsg != nil {
    631 		typ = EditMsg
    632 		_ = d.RoomChatMessagesGeIncrRev(roomID, editMsg.ID)
    633 		editMsg.Message = message
    634 		editMsg.RawMessage = raw
    635 		editMsg.Rev++
    636 		// Delete inboxes, we'll create new ones bellow
    637 		_ = d.DeleteChatInboxMessageByChatMessageID(editMsg.ID)
    638 	} else {
    639 		msg := makeMsg(raw, message, roomID, fromUserID)
    640 		editMsg = &msg
    641 		editMsg.IsHellbanned = hellbanMsg
    642 		editMsg.System = systemMsg
    643 		editMsg.Moderators = modMsg
    644 		editMsg.GroupID = groupID
    645 		editMsg.ToUserID = toUserID
    646 		if upload != nil {
    647 			editMsg.UploadID = &upload.ID
    648 		}
    649 	}
    650 
    651 	addFullscreenLinkToCodeBlocks(editMsg)
    652 
    653 	editMsg.DoSave(d)
    654 
    655 	MsgPubSub.Pub("room_"+roomID.String(), ChatMessageType{Typ: typ, Msg: *editMsg})
    656 	return editMsg.ID, nil
    657 }
    658 
    659 func addFullscreenLinkToCodeBlocks(editMsg *ChatMessage) {
    660 	i := 0
    661 	rgx := regexp.MustCompile(`</pre>`)
    662 	editMsg.Message = rgx.ReplaceAllStringFunc(editMsg.Message, func(s string) string {
    663 		i++
    664 		return fmt.Sprintf(`</pre><a href="/chat-code/%s/%d" title="Open in fullscreen" rel="noopener noreferrer" target="_blank" class=fullscreen>&#9974;</a>`,
    665 			editMsg.UUID, i-1)
    666 	})
    667 }
    668 
    669 func encryptWithRoomKey(txt, raw string, roomKey string) (string, string, error) {
    670 	if roomKey == "" {
    671 		return txt, raw, nil
    672 	}
    673 	return encryptMessages(txt, raw, roomKey)
    674 }
    675 
    676 type PubSubMessageType int
    677 
    678 const (
    679 	CreateMsg PubSubMessageType = iota
    680 	EditMsg
    681 	ForceRefresh
    682 	DeleteMsg
    683 	Wizz
    684 	Redirect
    685 	Close
    686 	CloseMenu
    687 
    688 	RefreshTopic string = "refresh"
    689 )
    690 
    691 type ChatMessageType struct {
    692 	Typ            PubSubMessageType
    693 	Msg            ChatMessage
    694 	IsMod          bool
    695 	ToUserUsername *Username
    696 	NewURL         string
    697 }
    698 
    699 var MsgPubSub = pubsub.NewPubSub[ChatMessageType]()
    700 
    701 func encryptMessages(html, origMessage, roomKey string) (string, string, error) {
    702 	var err error
    703 	// Encrypt html message (for displaying)
    704 	html, err = encryptMessage(roomKey, html)
    705 	if err != nil {
    706 		return "", "", err
    707 	}
    708 	// Encrypt original message (for /e command)
    709 	origMessage, err = encryptMessage(roomKey, origMessage)
    710 	if err != nil {
    711 		return "", "", err
    712 	}
    713 	return html, origMessage, nil
    714 }
    715 
    716 func encryptMessage(roomKey, msg string) (string, error) {
    717 	aesgcm, nonceSize, err := utils.GetGCM(roomKey)
    718 	if err != nil {
    719 		return "", err
    720 	}
    721 	nonce := make([]byte, nonceSize)
    722 	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
    723 		return "", err
    724 	}
    725 	return string(aesgcm.Seal(nonce, nonce, []byte(msg), nil)), nil
    726 }