├── .gitattributes ├── README.md ├── bot ├── app.js ├── commands │ ├── help.js │ ├── index.js │ ├── players.js │ └── servers.js ├── config.json ├── helper.js └── package.json └── server ├── codam └── codwatcher.gsc ├── codwatcher.cfg └── codwatcher.log /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoD Watcher 2 | A Discord bot for Call of Duty 1 servers. Features include live server status, chat logs, report & ban alerts and spam & bad-word automute. 3 | 4 | ## Setup 5 | 6 | ### Gameserver 7 | Files regarding your CoD server are in "server" folder. Open it. 8 | 9 | 1. Add `codwatcher.log` and `codwatcher.cfg` to "main" (or fs_game) folder of your server. 10 | 2. Include `codwatcher.cfg` in your server configuration file with `exec codwatcher.cfg`. 11 | 3. Add `codwatcher.gsc` to "codam" folder of your server. 12 | 4. Add the mod to CoDaM "modlist.gsc" - `[[ register ]]( "CoD Watcher", codam\codwatcher::main );`. 13 | 14 | To use the plugin with MiscMod, do the following: 15 | 16 | 1. Open "codam/_mm_commands.gsc" 17 | 2. Search for `str = codam\_mm_mmm::strip(str);` 18 | 3. Add the following code above that line: 19 | ```c 20 | if(!self codam\codwatcher::message(str)) { 21 | creturn(); 22 | return; 23 | } 24 | ``` 25 | 26 | To use the plugin with other chat command systems, add the same block of code as for MiscMod in your chat message callback function. 27 | You will also need to include your mute/unmute logic in "codwatcher.gsc" file. 28 | 29 | ### Bot 30 | 31 | 1. Put the "bot" folder somewhere in your server directory. We'll assume you put it outside your "main" folder. 32 | 2. Head over to "bot" folder. 33 | 3. Cofigure the settings in "config.json". 34 | 4. Install the required modules with `npm install`. 35 | 5. Run the bot with the following command `node app.js`. -------------------------------------------------------------------------------- /bot/app.js: -------------------------------------------------------------------------------- 1 | // Bot 2 | const Discord = require('discord.js'); 3 | const bot = new Discord.Client(); 4 | bot.config = require('./config.json'); 5 | bot.helper = require('./helper'); 6 | 7 | bot.commands = new Discord.Collection(); 8 | const botCommands = require('./commands'); 9 | Object.keys(botCommands).map(key => { 10 | bot.commands.set(botCommands[key].name, botCommands[key]); 11 | }); 12 | 13 | bot.login(bot.config.token); 14 | bot.on('ready', () => { 15 | console.info('Bot is online.'); 16 | bot.user.setStatus('online'); 17 | 18 | statusBots = bot.helper.loginBots(bot); 19 | setInterval(function() { 20 | bot.helper.updateBots(statusBots); 21 | }, 5000); 22 | }); 23 | 24 | bot.on('message', msg => { 25 | if((!msg.content.startsWith(bot.config.prefix) || msg.author.bot) && !msg.mentions.has(bot.user)) return; 26 | let args = msg.content.slice(bot.config.prefix.length).split(' '); 27 | let command = args.shift().toLowerCase(); 28 | 29 | if(!bot.commands.has(command)) return; 30 | 31 | try { 32 | bot.commands.get(command).execute(msg, args, bot); 33 | } catch(error) { 34 | console.log(error); 35 | msg.channel.send('Error... Read console. :eyes:') 36 | } 37 | }); 38 | 39 | // Chat parser 40 | const Tail = require('tail').Tail; 41 | const Filter = require('bad-words'); 42 | const RCON = require('quake3-rcon'); 43 | const proxycheck_node = require('proxycheck-node.js'); 44 | const filter = new Filter(); 45 | let proxycheck = null; 46 | if(bot.config.proxycheck !== undefined && bot.config.proxycheck !== "") { 47 | proxycheck = new proxycheck_node({ api_key: bot.config.proxycheck }); 48 | } 49 | let checkedIPs = []; 50 | 51 | // To add/remove words from badword check use this. 52 | // filter.removeWords('word1', 'word2'); 53 | // filter.addWords('word3', 'word4'); 54 | 55 | function readChat(server) { 56 | let tail = new Tail(server.path + '/codwatcher.log', "\n", {}, true); 57 | let rcon = new RCON({ address: server.ip, port: server.port, password: server.pass }); 58 | 59 | tail.on('line', async line => { 60 | let data = line.split('%'); 61 | let msg = { authorID: data[0], authorName: data[1], content: data[2] }; 62 | 63 | if(msg.content.startsWith('"connect')) { 64 | if(proxycheck != null) { 65 | let ip = msg.content.split(' ')[1]; 66 | if(!checkedIPs.includes(ip)) { 67 | let res = await proxycheck.check(ip, { vpn: true }); 68 | 69 | if(res.status == 'ok' && res[ip].proxy == 'yes') 70 | rcon.send('set command "kickvpn ' + msg.authorID + '"'); 71 | else 72 | checkedIPs.push(ip); 73 | } 74 | } 75 | 76 | return; 77 | } 78 | 79 | if(filter.isProfane(msg.content)) { 80 | rcon.send('set command "badword ' + msg.authorID + '"'); 81 | } 82 | 83 | if(msg.content.startsWith('!report ')) { 84 | let report = bot.helper.parseCommand(msg.content); 85 | let channel = bot.channels.cache.get(bot.config.reportChannel); 86 | 87 | return channel.send(`${server.name} | \`${report.player}\` has been reported by \`${msg.authorName}\` for \`${report.reason}\`.`); 88 | } 89 | 90 | if(msg.content.startsWith('!ban ')) { 91 | let ban = bot.helper.parseCommand(msg.content); 92 | let channel = bot.channels.cache.get(bot.config.banChannel); 93 | 94 | return channel.send(`${server.name} | \`${ban.player}\` has been banned by \`${msg.authorName}\` for \`${ban.reason}\`.`); 95 | } 96 | 97 | if(msg.content.startsWith('!')) return; // Do not forward chat commands to chat log. 98 | 99 | let channel = bot.channels.cache.get(server.logChannel); 100 | channel.send(`\`${msg.authorName}: ${msg.content}\``); 101 | }); 102 | } 103 | 104 | bot.config.servers.forEach(server => { 105 | readChat(server); 106 | }); 107 | -------------------------------------------------------------------------------- /bot/commands/help.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | 3 | module.exports = { 4 | name: 'help', 5 | description: 'Shows help embed', 6 | execute(msg, args, bot) { 7 | let prefix = bot.config.prefix; 8 | 9 | let embed = new Discord.MessageEmbed() 10 | .setColor(bot.config.color) 11 | .setTitle('CoD Watcher') 12 | .addField('Show servers', '`' + prefix + 'servers`', true) 13 | .addField('Show players', '`' + prefix + 'players `', true) 14 | .setFooter('@dftd/codwatcher'); 15 | 16 | msg.channel.send(embed); 17 | } 18 | } -------------------------------------------------------------------------------- /bot/commands/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Help: require('./help'), 3 | Players: require('./players'), 4 | Servers: require('./servers'), 5 | }; -------------------------------------------------------------------------------- /bot/commands/players.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | 3 | module.exports = { 4 | name: 'players', 5 | description: 'Gets server players.', 6 | execute(msg, args, bot, settings) { 7 | let srv, error = 'Please specify a server.'; 8 | 9 | if(!args.length) return msg.channel.send(err); 10 | 11 | for(server of bot.config.servers) 12 | if(server.id == args[0].toLowerCase()) srv = server; 13 | 14 | if(!srv) return msg.reply(error); 15 | 16 | let embed = new Discord.MessageEmbed() 17 | .setColor(bot.config.color) 18 | .setTitle('Loading...') 19 | .setDescription('Querying the server for info.'); 20 | 21 | msg.channel.send(embed).then(async m => { 22 | await bot.helper.query(srv).then(status => { 23 | if(status) { 24 | let players = { name: [], kill: [], ping: [] }; 25 | for(player of status.players) { 26 | if(!player.name) continue; 27 | players.name.push(player.name.trim()); 28 | players.kill.push(player.raw.frags); 29 | players.ping.push(player.raw.ping); 30 | } 31 | 32 | if(players.name.length) { 33 | msg.channel.send(new Discord.MessageEmbed() 34 | .setColor(bot.config.color) 35 | .setTitle(status.name + ' players') 36 | .setDescription(``) 37 | .addField('Name', players.name, true) 38 | .addField('Kills', players.kill, true) 39 | .addField('Ping', players.ping, true) 40 | ); 41 | } else { 42 | msg.channel.send(new Discord.MessageEmbed() 43 | .setColor(bot.config.color) 44 | .setTitle(status.name + ' is empty.') 45 | ); 46 | } 47 | } else { 48 | msg.channel.send(new Discord.MessageEmbed() 49 | .setColor(bot.config.color) 50 | .setTitle(status.name + ' is unreachable.') 51 | ); 52 | } 53 | }); 54 | 55 | m.delete(); 56 | }); 57 | } 58 | }; -------------------------------------------------------------------------------- /bot/commands/servers.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | 3 | module.exports = { 4 | name: 'servers', 5 | description: 'Shows gameservers.', 6 | execute(msg, args, bot) { 7 | let embed = new Discord.MessageEmbed() 8 | .setColor(bot.config.color) 9 | .setTitle('Loading...') 10 | .setDescription('Querying the servers for info.'); 11 | 12 | msg.channel.send(embed).then(async m => { 13 | let embeds = []; 14 | for(server of bot.config.servers) { 15 | embeds.push(bot.helper.query(server).then(status => { 16 | if(status) { 17 | msg.channel.send(new Discord.MessageEmbed() 18 | .setColor(bot.config.color) 19 | .setTitle(status.name) 20 | .addField('Players', status.players.length + '/' + status.maxplayers, true) 21 | .addField('Map', status.map, true) 22 | .addField('Connect', ``, true) 23 | .setThumbnail('https://image.gametracker.com/images/maps/160x120/cod/' + status.map + '.jpg') 24 | ); 25 | } else { 26 | msg.channel.send(new Discord.MessageEmbed() 27 | .setColor(bot.config.color) 28 | .setTitle(status.name + ' is unreachable.') 29 | ); 30 | } 31 | })); 32 | } 33 | 34 | await Promise.all(embeds); 35 | m.delete(); 36 | }); 37 | } 38 | }; -------------------------------------------------------------------------------- /bot/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "Main bot token", 3 | "prefix": "!", 4 | "color": "#00E098", 5 | "reportChannel": "Report alert channel ID", 6 | "banChannel": "Ban alert channel ID", 7 | "proxycheck": "proxycheck.io API key for blocking VPN, leave empty to disable", 8 | "servers": [ 9 | { 10 | "id": "dm", 11 | "name": "Deathmatch", 12 | "ip": "127.0.0.0", 13 | "port": "28960", 14 | "token": "Deathmatch bot token", 15 | "path": "../dm/main/", 16 | "pass": "RCON password", 17 | "logChannel": "Chat log channel ID" 18 | }, 19 | { 20 | "id": "sd", 21 | "name": "Search & Destroy", 22 | "ip": "127.0.0.0", 23 | "port": "28961", 24 | "token": "Search & Destroy bot token", 25 | "path": "../sd/main/", 26 | "pass": "RCON password", 27 | "logChannel": "Chat log channel ID" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /bot/helper.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const Gamedig = require('gamedig'); 3 | 4 | module.exports.query = async function(server) { 5 | let options = { 6 | type: 'cod', 7 | host: server.ip, 8 | port: server.port, 9 | socketTimeout: 3000, 10 | maxAttempts: 3 11 | }; 12 | 13 | try { 14 | return await Gamedig.query(options); 15 | } catch(err) { 16 | console.error(err); 17 | } 18 | 19 | return null; 20 | } 21 | 22 | module.exports.loginBots = function(mainBot) { 23 | let statusBots = []; 24 | mainBot.config.servers.forEach(server => { 25 | let bot = new Discord.Client(); 26 | bot.login(server.token); 27 | bot.gameserver = server; 28 | bot.on('ready', () => statusBots.push(bot)); 29 | }); 30 | 31 | return statusBots; 32 | } 33 | 34 | module.exports.updateBots = function(statusBots) { 35 | statusBots.forEach(async bot => { 36 | bot.user.setStatus('online'); 37 | let status = await exports.query(bot.gameserver); 38 | 39 | if(status) { 40 | bot.user.setActivity(status.players.length + '/' + status.maxplayers + ' - ' + status.map, { type: 'PLAYING' }); 41 | } else { 42 | bot.user.setActivity('server offline', { type: 'WATCHING' }); 43 | } 44 | }); 45 | } 46 | 47 | module.exports.parseCommand = function(cmd) { 48 | let split = cmd.split(' '); 49 | let player = split[1]; 50 | let reason = split.slice(2).join(' '); 51 | 52 | return { player, reason }; 53 | } -------------------------------------------------------------------------------- /bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codwatcher", 3 | "version": "1.0.1", 4 | "description": "A Discord bot for Call of Duty 1 servers.", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "dependencies": { 10 | "bad-words": "^3.0.4", 11 | "discord.js": "^12.0.2", 12 | "gamedig": "^3.0.3", 13 | "proxycheck-node.js": "^2.0.0-a", 14 | "quake3-rcon": "https://github.com/dftd/node-quake3-rcon/tarball/master", 15 | "tail": "2.2.1", 16 | "then-request": "^6.0.2" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/dftd/codwatcher.git" 21 | }, 22 | "keywords": [ 23 | "call-of-duty", 24 | "cod1", 25 | "discord.js" 26 | ], 27 | "author": "dftd", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/dftd/codwatcher/issues" 31 | }, 32 | "homepage": "https://github.com/dftd/codwatcher#readme" 33 | } 34 | -------------------------------------------------------------------------------- /server/codam/codwatcher.gsc: -------------------------------------------------------------------------------- 1 | /* 2 | * Register callbacks & commands. 3 | */ 4 | main(phase, register) { 5 | switch(phase) { 6 | case "init": 7 | init(register); 8 | case "load": 9 | case "standalone": 10 | load(); 11 | break; 12 | } 13 | } 14 | 15 | init(register) { 16 | file = getCvar("fs_basepath") + "/main/codwatcher.log"; 17 | 18 | if(!isDefined(level.dftd)) level.dftd = []; 19 | 20 | level.dftd["chatmute"] = spawnStruct(); 21 | level.dftd["chatmute"].file = file; 22 | level.dftd["chatmute"].spam = getCvarInt("dftd_chatmute_spam"); // 1 to enable anti-spam, 0 to disable anti-spam. 23 | level.dftd["chatmute"].spamCount = getCvarInt("dftd_chatmute_spam_count"); // Amount of messages before muting. 24 | level.dftd["chatmute"].badword = getCvarInt("dftd_chatmute_badword"); // 1 to enable anti-badword, 0 to disable anti-badword. 25 | level.dftd["chatmute"].badwordCount = getCvarInt("dftd_chatmute_badword_count"); // Amount of messages before muting. 26 | level.dftd["chatmute"].unmute = getCvarInt("dftd_chatmute_unmute"); // Seconds until automated unmute (may differ if sv_fps is changed). 27 | 28 | [[ register ]] ("gt_endMap", ::clearChatLog, "thread"); 29 | [[ register ]] ("PlayerConnect", ::logConnect, "thread"); 30 | } 31 | 32 | load() { 33 | _F = level.codam_f_commander; 34 | if(!isDefined(_F)) return; 35 | 36 | name = "codam/CoDWatcher"; 37 | [[ _F ]] (name, "badword", ::badWord, "nowait"); 38 | [[ _F ]] (name, "kickvpn", ::kickVPN, "nowait"); 39 | } 40 | 41 | /* 42 | * Called when a message is sent in chat. 43 | * If returns false, message won't be displayed (player is muted). 44 | */ 45 | message(msg) { 46 | display = true; 47 | 48 | if(level.dftd["chatmute"].spam && self spamMute(msg)) 49 | display = false; 50 | 51 | self thread logMessage(msg); 52 | 53 | return display; 54 | } 55 | 56 | /* 57 | * Temporarily mutes the player if they send X repeated messages of the same content. 58 | * Returns if player got muted or not (don't log the message if muted). 59 | */ 60 | spamMute(msg) { 61 | if(!isDefined(self.pers["spammute"])) { 62 | self.pers["spammute"] = []; 63 | self.pers["spammute"]["count"] = 0; 64 | self.pers["spammute"]["message"] = ""; 65 | } 66 | 67 | if(msg == self.pers["spammute"]["message"]) { 68 | self.pers["spammute"]["count"]++; 69 | } else { 70 | self.pers["spammute"]["count"] = 0; 71 | } 72 | 73 | self.pers["spammute"]["message"] = msg; 74 | 75 | if(self.pers["spammute"]["count"] >= level.dftd["chatmute"].spamCount) { 76 | sendChatMessage(namefix(self.name) + "^7^7 has been temporarily muted due to chat spam."); 77 | self mutePlayer(); 78 | self thread unmuteTimer(); 79 | 80 | return true; 81 | } 82 | 83 | return false; 84 | } 85 | 86 | /* 87 | * Called when node.js app detects a bad word in a message. 88 | * Either mute the player immediatelly or warn them. 89 | */ 90 | badWord(args, a1) { 91 | if(!isDefined(args) || (args.size < 2)) return; 92 | player = codam\utils::playerFromId(args[1]); 93 | if(!isDefined(player)) return; 94 | if(!level.dftd["chatmute"].badword) return; 95 | 96 | if(!isDefined(player.pers["badmute"])) 97 | player.pers["badmute"] = 1; 98 | else 99 | player.pers["badmute"]++; 100 | 101 | if(player.pers["badmute"] >= level.dftd["chatmute"].badwordCount) { 102 | sendChatMessage(namefix(player.name) + "^7^7 has been temporarily muted due to using bad words."); 103 | player mutePlayer(); 104 | player thread unmuteTimer(); 105 | } else { 106 | sendChatMessage("^1Warning:^7 Continuous usage of bad words will lead to a temporary mute.", player); 107 | } 108 | } 109 | 110 | /* 111 | * Called when node.js app detects a VPN client to kick them. 112 | */ 113 | kickVPN(args, a1) { 114 | if(!isDefined(args) || (args.size < 2)) return; 115 | player = codam\utils::playerFromId(args[1]); 116 | if(!isDefined(player)) return; 117 | 118 | wait 5; // If the player takes longer to connect it doesn't kick them(?) 119 | 120 | message = "To connect please turn ^1off^7 your VPN/Proxy."; 121 | player dropclient(message); 122 | } 123 | 124 | /* 125 | * Called after a player is muted. This will automatically unmute them after X seconds. 126 | */ 127 | unmuteTimer() { 128 | self endon("disconnect"); 129 | 130 | wait(level.dftd["chatmute"].unmute); 131 | 132 | self unmutePlayer(); 133 | 134 | self.pers["spammute"]["count"] = 0; 135 | self.pers["spammute"]["message"] = ""; 136 | sendChatMessage(namefix(self.name) + "^7^7 has been unmuted."); 137 | } 138 | 139 | /* 140 | * Log the message to be parsed by bad-word node.js app. 141 | */ 142 | logMessage(msg) { 143 | if(fexists(level.dftd["chatmute"].file)) { 144 | line = ""; 145 | line += self getEntityNumber(); 146 | line += "%%" + strip(monotone(self.name)); 147 | line += "%%" + strip(monotone(msg)); 148 | line += "\n"; 149 | 150 | f = fopen(level.dftd["chatmute"].file, "a"); 151 | if(f != -1) fwrite(line, f); 152 | fclose(f); 153 | } 154 | } 155 | 156 | /* 157 | * Log when user connects to check for VPN. 158 | */ 159 | logConnect(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, b0, b1, b2, b2, b4, b5, b6, b7, b8, b9) { 160 | msg = "\"connect "; 161 | msg += self getip(); 162 | self thread logMessage(msg); 163 | } 164 | 165 | /* 166 | * Clear the chat log file when map ends. 167 | */ 168 | clearChatLog(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9) { 169 | if(fexists(level.dftd["chatmute"].file)) { 170 | fclose(fopen(level.dftd["chatmute"].file, "w")); 171 | } 172 | } 173 | 174 | // Helpers... 175 | mutePlayer() { 176 | if(hasMiscmod()) { 177 | self.pers["mm_mute"] = true; 178 | rID = ""; 179 | if(getCvar("tmp_mm_muted") != "") rID += getCvar("tmp_mm_muted"); 180 | rID += self getEntityNumber(); 181 | rID += ";"; 182 | setCvar("tmp_mm_muted", rID); 183 | } else { 184 | // Implement here the mute logic for your chat command system. 185 | } 186 | } 187 | 188 | unmutePlayer() { 189 | if(hasMiscmod()) { 190 | self.pers["mm_mute"] = undefined; 191 | codam\_mm_commands::_removeMuted(self getEntityNumber()); 192 | } else { 193 | // Implement here the unmute logic for your chat command system. 194 | } 195 | } 196 | 197 | hasMiscmod() { 198 | return isDefined(level.miscmodversion); 199 | } 200 | 201 | sendChatMessage(msg, player) { 202 | prefix = "Server"; 203 | if(isDefined(level.nameprefix)) prefix = level.nameprefix; 204 | 205 | if(!isDefined(player)) 206 | sendservercommand("i \"^7^7" + prefix + "^7: " + msg + "\""); 207 | else 208 | player sendservercommand("i \"^7^7" + prefix + "^7: " + msg + "\""); 209 | } 210 | 211 | // All following functions are from Miscmod, _mm_mmm.gsc. Credits to Cato. 212 | 213 | namefix(normalName) { 214 | if(!isDefined(normalName)) return ""; 215 | 216 | allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!'#/&()=?+`^~*-.,;<>|$�@:[]{}_ "; 217 | badName = false; 218 | for(i = 0; i < normalName.size; i++) { 219 | matchFound = false; 220 | 221 | for(z = 0; z < allowedChars.size; z++) { 222 | if(normalName[i] == allowedChars[z]) { 223 | matchFound = true; 224 | break; 225 | } 226 | } 227 | 228 | if(!matchFound) { 229 | badName = true; 230 | break; 231 | } 232 | } 233 | 234 | if(badName) { 235 | fixedName = ""; 236 | for(i = 0; i < normalName.size; i++) { 237 | for(z = 0; z < allowedChars.size; z++) { 238 | if(normalName[i] == allowedChars[z]) { 239 | fixedName += normalName[i]; 240 | break; 241 | } 242 | } 243 | } 244 | 245 | return fixedName; 246 | } 247 | 248 | return normalName; 249 | } 250 | 251 | validate_number(input, isfloat) { 252 | if(!isDefined(input)) return false; 253 | 254 | if(!isDefined(isfloat)) isfloat = false; 255 | 256 | dot = false; 257 | 258 | input += ""; 259 | for(i = 0; i < input.size; i++) { 260 | switch(input[i]) { 261 | case "0": case "1": case "2": 262 | case "3": case "4": case "5": 263 | case "6": case "7": case "8": 264 | case "9": 265 | break; 266 | case ".": 267 | if(i == 0 || !isfloat || dot) return false; 268 | 269 | dot = true; 270 | break; 271 | default: 272 | return false; 273 | } 274 | } 275 | 276 | return true; 277 | } 278 | 279 | monotone(str, loop) { 280 | if(!isDefined(str)) return ""; 281 | 282 | _str = ""; 283 | for(i = 0; i < str.size; i++) { 284 | if(str[i] == "^" && ((i + 1) < str.size && (validate_number(str[i + 1])))) { 285 | i++; 286 | continue; 287 | } 288 | 289 | _str += str[i]; 290 | } 291 | 292 | if(!isDefined(loop)) _str = monotone(_str, true); 293 | 294 | return _str; 295 | } 296 | 297 | strip(s) { 298 | if(s == "") return ""; 299 | 300 | s2 = ""; 301 | s3 = ""; 302 | 303 | i = 0; 304 | while(i < s.size && s[i] == " ") 305 | i++; 306 | 307 | if(i == s.size) return ""; 308 | 309 | for(; i < s.size; i++) 310 | s2 += s[i]; 311 | 312 | i = (s2.size - 1); 313 | while(s2[i] == " " && i > 0) 314 | i--; 315 | 316 | for(j = 0; j <= i; j++) 317 | s3 += s2[j]; 318 | 319 | return s3; 320 | } 321 | -------------------------------------------------------------------------------- /server/codwatcher.cfg: -------------------------------------------------------------------------------- 1 | set dftd_chatmute_spam "1" // 1 to enable anti-spam, 0 to disable anti-spam. 2 | set dftd_chatmute_spam_count "5" // Amount of messages before muting. 3 | set dftd_chatmute_badword "1" // 1 to enable anti-badword, 0 to disable anti-badword. 4 | set dftd_chatmute_badword_count "3" // Amount of messages before muting. 5 | set dftd_chatmute_unmute "30" // Seconds until automated unmute (may differ if sv_fps is changed). -------------------------------------------------------------------------------- /server/codwatcher.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patriksh/codwatcher/df91be9ce37f0b80ce5eb9955c94a2300633410c/server/codwatcher.log --------------------------------------------------------------------------------