From dd757b035e37f159e411632bd9f787b58ef8ece9 Mon Sep 17 00:00:00 2001 From: HF Date: Tue, 13 Sep 2022 03:09:04 +0200 Subject: [PATCH] store country mutes in redis and make them channel specific --- src/core/ChatProvider.js | 177 +++++++++++++---------------- src/data/redis/chat.js | 115 +++++++++++++++++++ src/data/redis/client.js | 7 ++ src/data/redis/lua/allowedChat.lua | 41 +++++++ 4 files changed, 241 insertions(+), 99 deletions(-) create mode 100644 src/data/redis/chat.js create mode 100644 src/data/redis/lua/allowedChat.lua diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js index 1643eb3..8268051 100644 --- a/src/core/ChatProvider.js +++ b/src/core/ChatProvider.js @@ -3,7 +3,6 @@ */ import { Op } from 'sequelize'; import logger from './logger'; -import redis from '../data/redis/client'; import RateLimiter from '../utils/RateLimiter'; import { Channel, RegUser, UserChannel, Message, @@ -11,7 +10,14 @@ import { import { findIdByNameOrId } from '../data/sql/RegUser'; import ChatMessageBuffer from './ChatMessageBuffer'; import socketEvents from '../socket/socketEvents'; -import checkIPAllowed from './isAllowed'; +import isIPAllowed from './isAllowed'; +import { + mutec, unmutec, + unmutecAll, listMutec, + mute, + unmute, + allowedChat, +} from '../data/redis/chat'; import { DailyCron } from '../utils/cron'; import { escapeMd } from './utils'; import ttags from './ttag'; @@ -49,31 +55,12 @@ export class ChatProvider { this.apiSocketUserId = 1; this.caseCheck = /^[A-Z !.]*$/; this.cyrillic = /[\u0436-\u043B]'/; - this.filters = [ - { - regexp: /ADMIN/gi, - matches: 4, - }, - { - regexp: /ADMlN/gi, - matches: 4, - }, - { - regexp: /ADMlN/gi, - matches: 4, - }, - { - regexp: /FUCK/gi, - matches: 4, - }, - ]; this.substitutes = [ { regexp: /http[s]?:\/\/(old.)?pixelplanet\.fun\/#/g, replace: '#', }, ]; - this.mutedCountries = []; this.chatMessageBuffer = new ChatMessageBuffer(socketEvents); this.clearOldMessages = this.clearOldMessages.bind(this); @@ -274,7 +261,7 @@ export class ChatProvider { return this.chatMessageBuffer.getMessages(cid, limit); } - adminCommands(message, channelId, user) { + async adminCommands(message, channelId, user) { // admin commands const cmdArr = message.split(' '); const cmd = cmdArr[0].substring(1); @@ -314,16 +301,21 @@ export class ChatProvider { case 'mutec': { if (args[0]) { const cc = args[0].toLowerCase(); - if (cc.length > 3) { + const ret = await mutec(channelId, cc); + if (ret === null) { return 'No legit country defined'; } - this.mutedCountries.push(cc); - this.broadcastChatMessage( - 'info', - `Country ${cc} has been muted from en by ${initiator}`, - channelId, - this.infoUserId, - ); + if (!ret) { + return `Cuntry ${cc} is already muted`; + } + if (ret) { + this.broadcastChatMessage( + 'info', + `Country ${cc} has been muted from en by ${initiator}`, + channelId, + this.infoUserId, + ); + } return null; } return 'No country defined for mutec'; @@ -332,10 +324,13 @@ export class ChatProvider { case 'unmutec': { if (args[0]) { const cc = args[0].toLowerCase(); - if (!this.mutedCountries.includes(cc)) { - return `Country ${cc} is not muted`; + const ret = await unmutec(channelId, cc); + if (ret === null) { + return 'No legit country defined'; + } + if (!ret) { + return `Cuntry ${cc} is not muted`; } - this.mutedCountries = this.mutedCountries.filter((c) => c !== cc); this.broadcastChatMessage( 'info', `Country ${cc} has been unmuted from en by ${initiator}`, @@ -344,17 +339,25 @@ export class ChatProvider { ); return null; } - if (this.mutedCountries.length) { + const ret = await unmutecAll(channelId); + if (ret) { this.broadcastChatMessage( 'info', - `Countries ${this.mutedCountries} unmuted from en by ${initiator}`, + `All countries unmuted from this channel by ${initiator}`, channelId, this.infoUserId, ); - this.mutedCountries = []; return null; } - return 'No country is currently muted'; + return 'No country is currently muted from this channel'; + } + + case 'listmc': { + const ccArr = await listMutec(channelId); + if (ccArr.length) { + return `Muted countries: ${ccArr}`; + } + return 'No country is currently muted from this channel'; } default: @@ -382,25 +385,44 @@ export class ChatProvider { if (!name || !id) { return null; } + const country = user.regUser.flag || 'xx'; - const allowed = await checkIPAllowed(user.ip); - if (!allowed.allowed) { - logger.info( - `${name} / ${user.ip} tried to send chat message but is not allowed`, + if (!user.userlvl) { + const [allowed, needProxycheck] = await allowedChat( + channelId, + id, + user.ipSub, + country, ); - switch (allowed.status) { - case 1: + console.log(allowed, needProxycheck, name, id, country); + if (allowed) { + logger.info( + `${name} / ${user.ip} tried to send chat message but is not allowed`, + ); + if (allowed === 1) { return t`You can not send chat messages with proxy`; - case 2: + } if (allowed === 100 && user.userlvl === 0) { + return t`Your country is temporary muted from this chat channel`; + } if (allowed === 101) { + // eslint-disable-next-line max-len + return t`You are permanently muted, join our guilded to apppeal the mute`; + } if (allowed === 2) { return t`You are banned`; - case 3: + } if (allowed === 3) { return t`Your Internet Provider is banned`; - default: - return t`You are not allowed to use chat`; + } if (allowed < 0) { + const ttl = -allowed; + if (ttl > 120) { + const timeMin = Math.round(ttl / 60); + return t`You are muted for another ${timeMin} minutes`; + } + return t`You are muted for another ${ttl} seconds`; + } } - } - - if (message.charAt(0) === '/' && user.userlvl) { + if (needProxycheck) { + isIPAllowed(user.ip); + } + } else if (message.charAt(0) === '/') { return this.adminCommands(message, channelId, user); } @@ -418,7 +440,6 @@ export class ChatProvider { return t`You don\'t have access to this channel`; } - const country = user.regUser.flag || 'xx'; let displayCountry = country; if (user.userlvl !== 0) { displayCountry = 'zz'; @@ -436,27 +457,6 @@ export class ChatProvider { return t`Your mail has to be verified in order to chat`; } - const muted = await ChatProvider.checkIfMuted(user.id); - if (muted === -1) { - return t`You are permanently muted, join our guilded to apppeal the mute`; - } - if (muted > 0) { - if (muted > 120) { - const timeMin = Math.round(muted / 60); - return t`You are muted for another ${timeMin} minutes`; - } - return t`You are muted for another ${muted} seconds`; - } - - for (let i = 0; i < this.filters.length; i += 1) { - const filter = this.filters[i]; - const count = (message.match(filter.regexp) || []).length; - if (count >= filter.matches) { - this.mute(name, { duration: 30, printChannel: channelId }); - return t`Ow no! Spam protection decided to mute you`; - } - } - for (let i = 0; i < this.substitutes.length; i += 1) { const subsitute = this.substitutes[i]; message = message.replace(subsitute.regexp, subsitute.replace); @@ -471,13 +471,6 @@ export class ChatProvider { return t`Please use int channel`; } - if (channelId === this.enChannelId - && this.mutedCountries.includes(country) - && user.userlvl === 0 - ) { - return t`Your country is temporary muted from this chat channel`; - } - if (user.last_message && user.last_message === message) { user.message_repeat += 1; if (user.message_repeat >= 4) { @@ -507,12 +500,6 @@ export class ChatProvider { return this.chatMessageBuffer.broadcastChatMessage(...args); } - static async checkIfMuted(uid) { - const key = `mute:${uid}`; - const ttl = await redis.ttl(key); - return ttl; - } - async mute(nameOrId, opts) { const timeMin = opts.duration || null; const initiator = opts.initiator || null; @@ -525,13 +512,9 @@ export class ChatProvider { const { name, id } = searchResult; const userPing = `@[${escapeMd(name)}](${id})`; - const key = `mute:${id}`; - if (timeMin) { - const ttl = timeMin * 60; - await redis.set(key, '', { - EX: ttl, - }); - if (printChannel) { + mute(id, timeMin); + if (printChannel) { + if (timeMin) { this.broadcastChatMessage( 'info', (initiator) @@ -540,10 +523,7 @@ export class ChatProvider { printChannel, this.infoUserId, ); - } - } else { - await redis.set(key, ''); - if (printChannel) { + } else { this.broadcastChatMessage( 'info', (initiator) @@ -569,9 +549,8 @@ export class ChatProvider { const { name, id } = searchResult; const userPing = `@[${escapeMd(name)}](${id})`; - const key = `mute:${id}`; - const delKeys = await redis.del(key); - if (delKeys !== 1) { + const succ = await unmute(id); + if (!succ) { return `User ${userPing} is not muted`; } if (printChannel) { diff --git a/src/data/redis/chat.js b/src/data/redis/chat.js new file mode 100644 index 0000000..92d088c --- /dev/null +++ b/src/data/redis/chat.js @@ -0,0 +1,115 @@ +/* + * chat mutes + */ +import client from './client'; +import { PREFIX as ALLOWED_PREFIX } from './isAllowedCache'; + +const MUTE_PREFIX = 'MUTE_PREFIX'; +const MUTEC_PREFIX = 'MUTE_PREFIXc'; + +/* + * check if user can send chat message in channel + */ +export async function allowedChat( + channelId, + userId, + ip, + cc, +) { + if (!cc || cc.length !== 2) { + return [0, 0]; + } + const mutecKey = `${MUTEC_PREFIX}:${channelId}`; + const muteKey = `${MUTE_PREFIX}:${userId}`; + const isalKey = `${ALLOWED_PREFIX}:${ip}`; + return client.allowedChat( + mutecKey, muteKey, isalKey, + cc, + ); +} + +/* + * check if user is muted + */ +export async function checkIfMuted(userId) { + const key = `${MUTE_PREFIX}:${userId}`; + const ttl = await client.ttl(key); + return ttl; +} + +/* + * mute user + * @param userId + * @param ttl mute time in minutes + */ +export function mute(userId, ttl) { + const key = `${MUTE_PREFIX}:${userId}`; + if (ttl) { + return client.set(key, '', { + EX: ttl * 60, + }); + } + return client.set(key, ''); +} + +/* + * unmute user + * @param userId + * @return boolean for success + */ +export async function unmute(userId) { + const key = `${MUTE_PREFIX}:${userId}`; + const ret = await client.del(key); + return ret !== 0; +} + +/* + * mute country from channel + * @param channelId + * @param cc country code + * @returns 1 if muted, 0 if already was muted, null if invalid + */ +export function mutec(channelId, cc) { + if (!cc || cc.length !== 2) { + return null; + } + const key = `${MUTEC_PREFIX}:${channelId}`; + return client.hSetNX(key, cc, ''); +} + +/* + * unmute country from channel + * @param channelId + * @param cc country code + * @return boolean if unmute successful, null if invalid + */ +export async function unmutec(channelId, cc) { + if (!cc || cc.length !== 2) { + return null; + } + const key = `${MUTEC_PREFIX}:${channelId}`; + const ret = await client.hDel(key, cc, ''); + return ret !== 0; +} + +/* + * unmute all countries from channel + * @param channelId + * @return boolean for success + */ +export async function unmutecAll(channelId) { + const key = `${MUTEC_PREFIX}:${channelId}`; + const ret = await client.del(key); + return ret !== 0; +} + +/* + * get list of muted countries + * @param channelId + * @return array with country codes that are muted + */ +export async function listMutec(channelId) { + const key = `${MUTEC_PREFIX}:${channelId}`; + const ret = await client.hKeys(key); + return ret; +} diff --git a/src/data/redis/client.js b/src/data/redis/client.js index 416ba8d..7591b28 100644 --- a/src/data/redis/client.js +++ b/src/data/redis/client.js @@ -16,6 +16,13 @@ const scripts = { return args.map((a) => ((typeof a === 'string') ? a : a.toString())); }, }), + allowedChat: defineScript({ + NUMBER_OF_KEYS: 3, + SCRIPT: fs.readFileSync('./workers/lua/allowedChat.lua'), + transformArguments(...args) { + return args.map((a) => ((typeof a === 'string') ? a : a.toString())); + }, + }), }; const client = createClient(REDIS_URL.startsWith('redis://') diff --git a/src/data/redis/lua/allowedChat.lua b/src/data/redis/lua/allowedChat.lua new file mode 100644 index 0000000..d437084 --- /dev/null +++ b/src/data/redis/lua/allowedChat.lua @@ -0,0 +1,41 @@ +-- Check if user is allowed to chat +-- Keys: +-- mutecKey: 'mutec:cid' hash of channel for country mutes +-- muteKey: 'mute:uid' key for user mute +-- isalKey: 'isal:ip' (proxycheck, blacklist, whitelist) +-- Args: +-- cc: two letter country code of user +-- Returns: +-- { +-- 1: return status code +-- 100: country muted +-- 101: user permanently muted +-- >0: isAllowed status code (see core/isAllowed) +-- 0: success +-- <0: time left for mute in seconds * -1 +-- 2: if we have to update isAllowed (proxycheck) +-- } +local ret = {0, 0} +-- check country mute +if redis.call('hget', KEYS[1], ARGV[1]) then + ret[1] = 100 + return ret +end +-- check user mute +local ttl = redis.call('ttl', KEYS[2]) +if ttl == -1 then + ret[1] = 101 + return ret +end +if ttl > 0 then + ret[1] = -ttl + return ret +end +-- check if isAllowed +local ia = redis.call('get', KEYS[3]) +if not ia then + ret[2] = 1 +else + ret[1] = tonumber(ia) +end +return ret