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>⛶</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 }