store country mutes in redis and make them channel specific
This commit is contained in:
parent
ecbcbe43fd
commit
dd757b035e
|
@ -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
115
src/data/redis/chat.js
Normal 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;
|
||||||
|
}
|
|
@ -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://')
|
||||||
|
|
41
src/data/redis/lua/allowedChat.lua
Normal file
41
src/data/redis/lua/allowedChat.lua
Normal 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
|
Loading…
Reference in New Issue
Block a user