store country mutes in redis and make them channel specific

This commit is contained in:
HF 2022-09-13 03:09:04 +02:00
parent ecbcbe43fd
commit dd757b035e
4 changed files with 241 additions and 99 deletions

View File

@ -3,7 +3,6 @@
*/ */
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import logger from './logger'; import logger from './logger';
import redis from '../data/redis/client';
import RateLimiter from '../utils/RateLimiter'; import RateLimiter from '../utils/RateLimiter';
import { import {
Channel, RegUser, UserChannel, Message, Channel, RegUser, UserChannel, Message,
@ -11,7 +10,14 @@ import {
import { findIdByNameOrId } from '../data/sql/RegUser'; import { findIdByNameOrId } from '../data/sql/RegUser';
import ChatMessageBuffer from './ChatMessageBuffer'; import ChatMessageBuffer from './ChatMessageBuffer';
import socketEvents from '../socket/socketEvents'; 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 { DailyCron } from '../utils/cron';
import { escapeMd } from './utils'; import { escapeMd } from './utils';
import ttags from './ttag'; import ttags from './ttag';
@ -49,31 +55,12 @@ export class ChatProvider {
this.apiSocketUserId = 1; this.apiSocketUserId = 1;
this.caseCheck = /^[A-Z !.]*$/; this.caseCheck = /^[A-Z !.]*$/;
this.cyrillic = /[\u0436-\u043B]'/; 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 = [ this.substitutes = [
{ {
regexp: /http[s]?:\/\/(old.)?pixelplanet\.fun\/#/g, regexp: /http[s]?:\/\/(old.)?pixelplanet\.fun\/#/g,
replace: '#', replace: '#',
}, },
]; ];
this.mutedCountries = [];
this.chatMessageBuffer = new ChatMessageBuffer(socketEvents); this.chatMessageBuffer = new ChatMessageBuffer(socketEvents);
this.clearOldMessages = this.clearOldMessages.bind(this); this.clearOldMessages = this.clearOldMessages.bind(this);
@ -274,7 +261,7 @@ export class ChatProvider {
return this.chatMessageBuffer.getMessages(cid, limit); return this.chatMessageBuffer.getMessages(cid, limit);
} }
adminCommands(message, channelId, user) { async adminCommands(message, channelId, user) {
// admin commands // admin commands
const cmdArr = message.split(' '); const cmdArr = message.split(' ');
const cmd = cmdArr[0].substring(1); const cmd = cmdArr[0].substring(1);
@ -314,16 +301,21 @@ export class ChatProvider {
case 'mutec': { case 'mutec': {
if (args[0]) { if (args[0]) {
const cc = args[0].toLowerCase(); const cc = args[0].toLowerCase();
if (cc.length > 3) { const ret = await mutec(channelId, cc);
if (ret === null) {
return 'No legit country defined'; return 'No legit country defined';
} }
this.mutedCountries.push(cc); if (!ret) {
this.broadcastChatMessage( return `Cuntry ${cc} is already muted`;
'info', }
`Country ${cc} has been muted from en by ${initiator}`, if (ret) {
channelId, this.broadcastChatMessage(
this.infoUserId, 'info',
); `Country ${cc} has been muted from en by ${initiator}`,
channelId,
this.infoUserId,
);
}
return null; return null;
} }
return 'No country defined for mutec'; return 'No country defined for mutec';
@ -332,10 +324,13 @@ export class ChatProvider {
case 'unmutec': { case 'unmutec': {
if (args[0]) { if (args[0]) {
const cc = args[0].toLowerCase(); const cc = args[0].toLowerCase();
if (!this.mutedCountries.includes(cc)) { const ret = await unmutec(channelId, cc);
return `Country ${cc} is not muted`; 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( this.broadcastChatMessage(
'info', 'info',
`Country ${cc} has been unmuted from en by ${initiator}`, `Country ${cc} has been unmuted from en by ${initiator}`,
@ -344,17 +339,25 @@ export class ChatProvider {
); );
return null; return null;
} }
if (this.mutedCountries.length) { const ret = await unmutecAll(channelId);
if (ret) {
this.broadcastChatMessage( this.broadcastChatMessage(
'info', 'info',
`Countries ${this.mutedCountries} unmuted from en by ${initiator}`, `All countries unmuted from this channel by ${initiator}`,
channelId, channelId,
this.infoUserId, this.infoUserId,
); );
this.mutedCountries = [];
return null; 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: default:
@ -382,25 +385,44 @@ export class ChatProvider {
if (!name || !id) { if (!name || !id) {
return null; return null;
} }
const country = user.regUser.flag || 'xx';
const allowed = await checkIPAllowed(user.ip); if (!user.userlvl) {
if (!allowed.allowed) { const [allowed, needProxycheck] = await allowedChat(
logger.info( channelId,
`${name} / ${user.ip} tried to send chat message but is not allowed`, id,
user.ipSub,
country,
); );
switch (allowed.status) { console.log(allowed, needProxycheck, name, id, country);
case 1: 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`; 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`; return t`You are banned`;
case 3: } if (allowed === 3) {
return t`Your Internet Provider is banned`; return t`Your Internet Provider is banned`;
default: } if (allowed < 0) {
return t`You are not allowed to use chat`; 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 (needProxycheck) {
isIPAllowed(user.ip);
if (message.charAt(0) === '/' && user.userlvl) { }
} else if (message.charAt(0) === '/') {
return this.adminCommands(message, channelId, user); return this.adminCommands(message, channelId, user);
} }
@ -418,7 +440,6 @@ export class ChatProvider {
return t`You don\'t have access to this channel`; return t`You don\'t have access to this channel`;
} }
const country = user.regUser.flag || 'xx';
let displayCountry = country; let displayCountry = country;
if (user.userlvl !== 0) { if (user.userlvl !== 0) {
displayCountry = 'zz'; displayCountry = 'zz';
@ -436,27 +457,6 @@ export class ChatProvider {
return t`Your mail has to be verified in order to chat`; 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) { for (let i = 0; i < this.substitutes.length; i += 1) {
const subsitute = this.substitutes[i]; const subsitute = this.substitutes[i];
message = message.replace(subsitute.regexp, subsitute.replace); message = message.replace(subsitute.regexp, subsitute.replace);
@ -471,13 +471,6 @@ export class ChatProvider {
return t`Please use int channel`; 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) { if (user.last_message && user.last_message === message) {
user.message_repeat += 1; user.message_repeat += 1;
if (user.message_repeat >= 4) { if (user.message_repeat >= 4) {
@ -507,12 +500,6 @@ export class ChatProvider {
return this.chatMessageBuffer.broadcastChatMessage(...args); 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) { async mute(nameOrId, opts) {
const timeMin = opts.duration || null; const timeMin = opts.duration || null;
const initiator = opts.initiator || null; const initiator = opts.initiator || null;
@ -525,13 +512,9 @@ export class ChatProvider {
const { name, id } = searchResult; const { name, id } = searchResult;
const userPing = `@[${escapeMd(name)}](${id})`; const userPing = `@[${escapeMd(name)}](${id})`;
const key = `mute:${id}`; mute(id, timeMin);
if (timeMin) { if (printChannel) {
const ttl = timeMin * 60; if (timeMin) {
await redis.set(key, '', {
EX: ttl,
});
if (printChannel) {
this.broadcastChatMessage( this.broadcastChatMessage(
'info', 'info',
(initiator) (initiator)
@ -540,10 +523,7 @@ export class ChatProvider {
printChannel, printChannel,
this.infoUserId, this.infoUserId,
); );
} } else {
} else {
await redis.set(key, '');
if (printChannel) {
this.broadcastChatMessage( this.broadcastChatMessage(
'info', 'info',
(initiator) (initiator)
@ -569,9 +549,8 @@ export class ChatProvider {
const { name, id } = searchResult; const { name, id } = searchResult;
const userPing = `@[${escapeMd(name)}](${id})`; const userPing = `@[${escapeMd(name)}](${id})`;
const key = `mute:${id}`; const succ = await unmute(id);
const delKeys = await redis.del(key); if (!succ) {
if (delKeys !== 1) {
return `User ${userPing} is not muted`; return `User ${userPing} is not muted`;
} }
if (printChannel) { if (printChannel) {

115
src/data/redis/chat.js Normal file
View File

@ -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;
}

View File

@ -16,6 +16,13 @@ const scripts = {
return args.map((a) => ((typeof a === 'string') ? a : a.toString())); 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://') const client = createClient(REDIS_URL.startsWith('redis://')

View File

@ -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