msgInterceptor.go (10691B)
1 package interceptors 2 3 import ( 4 "dkforest/pkg/config" 5 "dkforest/pkg/database" 6 dutils "dkforest/pkg/database/utils" 7 "dkforest/pkg/managers" 8 "dkforest/pkg/utils" 9 "dkforest/pkg/web/handlers/interceptors/command" 10 "errors" 11 "fmt" 12 "regexp" 13 "strings" 14 ) 15 16 const minMsgLen = 1 17 const maxMsgLen = 10000 18 19 var usernameF = `\w{3,20}` // username (regex Fragment) 20 var userOr0 = usernameF + `|0` 21 var groupName = `\w{3,20}` 22 var roomNameF = `\w{3,50}` 23 var chatTs = `\d{2}:\d{2}:\d{2}` 24 var optAtGUser = `@?(` + usernameF + `)` // Optional @, Grouped, Username 25 var optAtGUserOr0 = `@?(` + userOr0 + `)` // Optional @, Grouped, Username or 0 26 var onionV2Rgx = regexp.MustCompile(`[a-z2-7]{16}\.onion`) 27 var onionV3Rgx = regexp.MustCompile(`[a-z2-7]{56}\.onion`) 28 var deleteMsgRgx = regexp.MustCompile(`^/d (\d{2}:\d{2}:\d{2})(?:\s` + optAtGUserOr0 + `)?$`) 29 var ignoreRgx = regexp.MustCompile(`^/(?:ignore|i) ` + optAtGUser) 30 var pmToggleWhitelistUserRgx = regexp.MustCompile(`^/pmw ` + optAtGUser) 31 var pmToggleBlacklistUserRgx = regexp.MustCompile(`^/pmb ` + optAtGUser) 32 var whitelistUserRgx = regexp.MustCompile(`^/(?:whitelist|wl) ` + optAtGUser) 33 var unIgnoreRgx = regexp.MustCompile(`^/(?:unignore|ui) ` + optAtGUser) 34 var groupRgx = regexp.MustCompile(`^/g (` + groupName + `)\s(?s:(.*))`) 35 var pmRgx = regexp.MustCompile(`^/pm ` + optAtGUserOr0 + `(?:\s(?s:(.*)))?`) 36 var editRgx = regexp.MustCompile(`^/e (` + chatTs + `)\s(?s:(.*))`) 37 var hbmtRgx = regexp.MustCompile(`^/hbmt (` + chatTs + `)$`) 38 var chessRgx = regexp.MustCompile(`^/chess ` + optAtGUser + `(?:\s(w|b|r))?`) 39 var inboxRgx = regexp.MustCompile(`^/inbox ` + optAtGUser + `(\s-e)?\s(?s:(.*))`) 40 var purgeRgx = regexp.MustCompile(`^/purge(\s-hb)? ` + optAtGUserOr0) 41 var renameRgx = regexp.MustCompile(`^/rename ` + optAtGUser + ` ` + optAtGUser) 42 var profileRgx = regexp.MustCompile(`^/p ` + optAtGUserOr0) 43 var kickRgx = regexp.MustCompile(`^/(?:kick|k) ` + optAtGUser) 44 var setUrlRgx = regexp.MustCompile(`^/seturl (.+)`) 45 var kickKeepRgx = regexp.MustCompile(`^/(?:kk) ` + optAtGUser) 46 var kickSilentRgx = regexp.MustCompile(`^/(?:ks) ` + optAtGUser) 47 var kickKeepSilentRgx = regexp.MustCompile(`^/(?:kks) ` + optAtGUser) 48 var rtutoRgx = regexp.MustCompile(`^/(?:rtuto) ` + optAtGUser) 49 var logoutRgx = regexp.MustCompile(`^/(?:logout) ` + optAtGUser) 50 var wizzRgx = regexp.MustCompile(`^/(?:wizz) ` + optAtGUser) 51 var forceCaptchaRgx = regexp.MustCompile(`^/(?:captcha) ` + optAtGUser) 52 var unkickRgx = regexp.MustCompile(`^/(?:unkick|uk) ` + optAtGUser) 53 var hellbanRgx = regexp.MustCompile(`^/(?:hellban|hb) ` + optAtGUser) 54 var unhellbanRgx = regexp.MustCompile(`^/(?:unhellban|uhb) ` + optAtGUser) 55 var inviteRgx = regexp.MustCompile(`^/invite ` + optAtGUser + ` (\d+)$`) 56 var randRgx = regexp.MustCompile(`^/rand (-?\d+) (-?\d+)$`) 57 var tokenRgx = regexp.MustCompile(`^/token (\d{1,2})$`) 58 var snippetRgx = regexp.MustCompile(`!\w{1,20}`) 59 var tagRgx = regexp.MustCompile(`@(` + userOr0 + `)`) 60 var autoTagRgx = regexp.MustCompile(`(?:\\?)@(\w+)\*`) 61 var roomTagRgx = regexp.MustCompile(`#(` + roomNameF + `)`) 62 var tzRgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} at \d{1,2}\.\d{1,2}\.\d{1,2} (?i)[A|P]M)`) // Screen Shot 2022-02-04 at 11.58.58 PM 63 var tz1Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} \d{1,2}-\d{1,2}-\d{1,2})`) // Screenshot from 2022-02-04 11-58-58.png 64 var tz3Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2} \d{1,6})`) // Screenshot 2023-05-20 202351.png 65 var tz4Rgx = regexp.MustCompile(`(\d{4}-\d{1,2}-\d{1,2}_\d{1,2}_\d{1,2}_\d{1,2})`) // Screenshot_2023-05-20_11_13_14.png 66 var addGroupRgx = regexp.MustCompile(`^/addgroup (` + groupName + `)$`) 67 var rmGroupRgx = regexp.MustCompile(`^/rmgroup (` + groupName + `)$`) 68 var lockGroupRgx = regexp.MustCompile(`^/glock (` + groupName + `)$`) 69 var unlockGroupRgx = regexp.MustCompile(`^/gunlock (` + groupName + `)$`) 70 var groupUsersRgx = regexp.MustCompile(`^/gusers (` + groupName + `)$`) 71 var groupAddUserRgx = regexp.MustCompile(`^/gadduser (` + groupName + `) ` + optAtGUser + `$`) 72 var groupRmUserRgx = regexp.MustCompile(`^/grmuser (` + groupName + `) ` + optAtGUser + `$`) 73 var unsubscribeRgx = regexp.MustCompile(`^/unsubscribe (` + roomNameF + `)$`) 74 var bsRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /bs\s?([A-J]\d)?$`) 75 var cRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /c\s?(move)?$`) 76 var hideRgx = regexp.MustCompile(`^/hide (?:“\[)?(\d{2}:\d{2}:\d{2})`) 77 var unhideRgx = regexp.MustCompile(`^/unhide (\d{2}:\d{2}:\d{2})$`) 78 var memeRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50})$`) 79 var memeRenameRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50}) ([a-zA-Z0-9_-]{3,50})$`) 80 var memeRemoveRgx = regexp.MustCompile(`^/memerm ([a-zA-Z0-9_-]{3,50})$`) 81 var memesRgx = regexp.MustCompile(`^/memes$`) 82 var locateRgx = regexp.MustCompile(`^/locate ` + optAtGUser) 83 var chipsRgx = regexp.MustCompile(`^/chips ` + optAtGUser + ` (\d+)`) 84 var chipsSendRgx = regexp.MustCompile(`^/chips-send ` + optAtGUser + ` (\d+)`) 85 var betRgx = regexp.MustCompile(`^/bet (\d+)$`) 86 var distRgx = regexp.MustCompile(`^/dist ` + optAtGUser + ` ` + optAtGUser + `$`) 87 88 type MsgInterceptor struct{} 89 90 func (i MsgInterceptor) InterceptMsg(cmd *command.Command) { 91 if cmd.Room.ReadOnly { 92 if !cmd.Room.IsRoomOwner(cmd.AuthUser.ID) { 93 cmd.Err = fmt.Errorf("room is read-only") 94 return 95 } 96 } 97 98 // Only check maximum length of message if we are uploading a file 99 // Trim whitespaces and ensure minimum length 100 minLen := utils.Ternary(cmd.Upload != nil, 0, minMsgLen) 101 if !utils.ValidateRuneLength(strings.TrimSpace(cmd.Message), minLen, maxMsgLen) { 102 cmd.DataMessage = cmd.OrigMessage 103 cmd.Err = fmt.Errorf("%d - %d characters", minLen, maxMsgLen) 104 return 105 } 106 107 var html string 108 var err error 109 taggedUsersIDsMap := make(map[database.UserID]database.User) 110 if !cmd.Raw { 111 html, taggedUsersIDsMap, err = dutils.ProcessRawMessage(cmd.DB, cmd.Message, cmd.RoomKey, cmd.AuthUser.ID, cmd.Room.ID, cmd.Upload, cmd.AuthUser.IsModerator(), cmd.AuthUser.CanUseMultiline, cmd.AuthUser.ManualMultiline) 112 if err != nil { 113 cmd.DataMessage = cmd.OrigMessage 114 cmd.Err = err 115 return 116 } 117 } else { 118 html = cmd.Message 119 } 120 121 if len(strings.TrimSpace(html)) <= len("<p></p>") { 122 cmd.DataMessage = cmd.OrigMessage 123 cmd.Err = errors.New("empty message") 124 return 125 } 126 127 pmUsername := dutils.DoParseUsernamePtr(cmd.C.QueryParam(command.RedirectPmUsernameQP)) 128 if pmUsername != nil { 129 if err := cmd.SetToUser(*pmUsername); err != nil { 130 cmd.Err = command.ErrRedirect 131 return 132 } 133 cmd.HellbanMsg = false 134 cmd.ModMsg = false 135 cmd.SystemMsg = false 136 cmd.GroupID = nil 137 } 138 139 toUserID := database.UserPtrID(cmd.ToUser) 140 141 msgID, _ := cmd.DB.CreateOrEditMessage(cmd.EditMsg, html, cmd.OrigMessage, cmd.RoomKey, cmd.Room.ID, cmd.AuthUser.ID, toUserID, cmd.Upload, cmd.GroupID, cmd.HellbanMsg, cmd.ModMsg, cmd.SystemMsg) 142 143 if !cmd.SkipInboxes { 144 sendInboxes(cmd.DB, cmd.Room, cmd.AuthUser, cmd.ToUser, msgID, cmd.GroupID, html, cmd.ModMsg, taggedUsersIDsMap) 145 } 146 147 // Count public messages in #general room 148 if cmd.Room.ID == config.GeneralRoomID && cmd.ToUser == nil { 149 cmd.AuthUser.GeneralMessagesCount++ 150 generalRoomKarma(cmd.DB, cmd.AuthUser) 151 cmd.AuthUser.DoSave(cmd.DB) 152 } 153 154 // Update chat read marker 155 if cmd.EditMsg == nil { 156 cmd.DB.UpdateChatReadMarker(cmd.AuthUser.ID, cmd.Room.ID) 157 } 158 159 // Update user activity 160 isPM := cmd.ToUser != nil 161 updateUserActivity(isPM, cmd.ModMsg, cmd.Room, cmd.AuthUser) 162 } 163 164 func generalRoomKarma(db *database.DkfDB, authUser *database.User) { 165 // Hellban users ain't getting karma 166 if authUser.IsHellbanned { 167 return 168 } 169 messagesCount := authUser.GeneralMessagesCount 170 if messagesCount%100 == 0 { 171 description := fmt.Sprintf("sent %d messages", messagesCount) 172 authUser.IncrKarma(db, 1, description) 173 } else if messagesCount == 20 { 174 authUser.IncrKarma(db, 1, "first 20 messages sent") 175 } 176 } 177 178 func sendInboxes(db *database.DkfDB, room database.ChatRoom, authUser, toUser *database.User, msgID int64, groupID *database.GroupID, html string, modMsg bool, 179 taggedUsersIDsMap map[database.UserID]database.User) { 180 // Only have chat inbox for unencrypted messages 181 if room.IsProtected() { 182 return 183 } 184 // If user is hellbanned, do not send inboxes 185 if authUser.IsHellbanned { 186 return 187 } 188 // Early return if we don't need to send inboxes 189 if toUser == nil && len(taggedUsersIDsMap) == 0 { 190 return 191 } 192 193 blacklistedBy, _ := db.GetPmBlacklistedByUsers(authUser.ID) 194 blacklistedBySet := utils.Slice2Set(blacklistedBy, func(el database.PmBlacklistedUsers) database.UserID { return el.UserID }) 195 196 ignoredBy, _ := db.GetIgnoredByUsers(authUser.ID) 197 ignoredBySet := utils.Slice2Set(ignoredBy, func(el database.IgnoredUser) database.UserID { return el.UserID }) 198 199 sendInbox := func(user database.User, isPM, modCh bool) { 200 if !managers.ActiveUsers.IsUserActiveInRoom(user.ID, room) || user.AFK { 201 // Do not send notification if receiver is blacklisting you 202 if blacklistedBySet.Contains(user.ID) { 203 return 204 } 205 // Do not send notification if receiver is ignoring you 206 if ignoredBySet.Contains(user.ID) { 207 return 208 } 209 db.CreateInboxMessage(html, room.ID, authUser.ID, user.ID, isPM, modCh, &msgID) 210 } 211 } 212 213 // If the message is a PM, only notify the receiver, not the tagged people in it. 214 if toUser != nil { 215 sendInbox(*toUser, true, false) 216 } else if room.Name == "moderators" { // Only tags other moderators on "moderators" room 217 for _, user := range taggedUsersIDsMap { 218 if user.IsModerator() { 219 sendInbox(user, false, false) 220 } 221 } 222 } else if modMsg { // Only tags other moderators on /m messages 223 for _, user := range taggedUsersIDsMap { 224 if user.IsModerator() { 225 sendInbox(user, false, true) 226 } 227 } 228 } else if groupID != nil { // Only tags other people in the group 229 for _, user := range taggedUsersIDsMap { 230 if db.IsUserInGroupByID(user.ID, *groupID) { 231 sendInbox(user, false, false) 232 } 233 } 234 } else { // Otherwise, notify tagged people 235 for _, user := range taggedUsersIDsMap { 236 sendInbox(user, false, false) 237 } 238 } 239 } 240 241 func updateUserActivity(isPM, modMsg bool, room database.ChatRoom, authUser *database.User) { 242 // We do not update user presence when they send private messages or moderators group message 243 if isPM || modMsg { 244 return 245 } 246 managers.ActiveUsers.UpdateUserInRoom(room, managers.NewUserInfoUpdateActivity(authUser)) 247 } 248 249 func checkCPLinks(db *database.DkfDB, html string) bool { 250 m1 := onionV3Rgx.FindAllStringSubmatch(html, -1) 251 m2 := onionV2Rgx.FindAllStringSubmatch(html, -1) 252 for _, m := range append(m1, m2...) { 253 hash := utils.MD5([]byte(m[0])) 254 if _, err := db.GetOnionBlacklist(hash); err == nil { 255 return true 256 } 257 } 258 return false 259 }