dkforest

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

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 }