let IRC = require("irc-framework") const c = (s) => String.fromCodePoint(s); const bold = c(0x02) const color = c(0x03) const reset = c(0x0f) let fs = require("fs") const nodemailer = require("nodemailer"); let conf = JSON.parse(fs.readFileSync("wallopsserv.json")) let transporter = nodemailer.createTransport({ host: conf.mail.host, port: conf.mail.port, secure: conf.mail.secure, auth: { user: conf.mail.user, pass: conf.mail.pass } }); const bot = new IRC.Client({ nick: conf.user.nick, username: conf.user.ident, gecos: conf.user.gecos, version: conf.user.nick, host: conf.server.host, tls: conf.server.tls, port: conf.server.port }); let map = conf.relays let exemptUsers = conf.cooldownExempt let admin = conf.admin let map2 = conf.shortMap bot.connect(); let cd = {} let globCd = 0 let wallops = [] let joins = [] if (!fs.existsSync("log.txt")) fs.writeFileSync("log.txt", "") let log = fs.readFileSync("log.txt").toString().split("\n").filter(x => x) let lockActive = false function logS(str) { let d = new Date() log.push(`[${d.getUTCDate().toString().padStart(2, "0")}/${d.getUTCMonth().toString().padStart(2, "0")} ${d.getUTCHours().toString().padStart(2, "0")}:${d.getUTCMinutes().toString().padStart(2, "0")}:${d.getUTCSeconds().toString().padStart(2, "0")}] ${str}`) if (log.length > conf.logLength) log.shift() fs.writeFileSync("log.txt", log.join("\n")) } logS("Bot started") bot.on("registered", () => { bot.raw("oper " + conf.oper.name + " " + conf.oper.pass); bot.join(conf.channel); bot.join("#*Serv") bot.mode(conf.user.nick, conf.user.mode) }); function genIdentity(e) { if (map2[genIdentity2(e)]) { return `${bold}${color}14[${color}13${map2[genIdentity2(e)]}${color}14]` } let identity = `${bold}${color}14[` let tn = e.nick let bridge = "" if (tn.includes("/") && tn.split("/").length === 2) { let els = tn.split("/") if (map[els[1]]) { tn = els[0] bridge = map[els[1]] } } if (tn || e.ident) { identity += `${color}07${tn}${color}03!${color}12${e.ident}${color}03@${color}10${e.hostname.replace(/~/g, "")}` } else { identity += `${color}03~${color}10${e.hostname.replace(/~/g, "")}` } if (bridge) { identity += `${color}03~${color}12${bridge}` } identity += `${color}14]` return identity } function genIdentity2(e) { let identity = `` let tn = e.nick let bridge = "" if (tn.includes("/") && tn.split("/").length === 2) { let els = tn.split("/") if (map[els[1]]) { tn = els[0] bridge = map[els[1]] } } if (tn || e.ident) { identity += `${tn}!${e.ident}@${e.hostname.replace(/~/g, "")}` } else { identity += `~${e.hostname.replace(/~/g, "")}` } if (bridge) { identity += `~${bridge}` } return identity } bot.on("wallops", (wallop) => { if (wallop.nick.toLowerCase() !== conf.user.nick.toLowerCase()) { bot.notice(conf.channel, `${genIdentity(wallop)} ${reset}${wallop.message}`) } }) bot.on("privmsg", (e) => { if (e.target === conf.user.nick) { bot.notice(e.nick, bold + "+--------------- WallopsServ ---------------+") bot.notice(e.nick, "" + "| WallopsServ provides a channel to send |") bot.notice(e.nick, "" + "| and receive wallops. The prefix " + conf.prefix + " can be |") bot.notice(e.nick, "" + "| used in the channel to send one. |") bot.notice(e.nick, "" + "| Channel: " + conf.channel.padEnd(31, " ") + " |") bot.notice(e.nick, bold + "+--------------- WallopsServ ---------------+") return } if (e.target === conf.channel && admin.includes(genIdentity2(e)) && e.message.startsWith(conf.cmdPrefix)) { let args = e.message.slice(conf.cmdPrefix.length).split(" ") let cmd = args.shift() switch (cmd) { case "rehash": logS(genIdentity2(e) + " rehashed the config") conf = JSON.parse(fs.readFileSync("wallopsserv.json")) map = conf.relays exemptUsers = conf.cooldownExempt admin = conf.admin map2 = conf.shortMap bot.notice(conf.channel, "Rehashed.") break case "sendlog": logS(genIdentity2(e) + " requested the log") transporter.sendMail({ from: conf.mail.sender, to: conf.mail.receiver, subject: "Log request", text: "The log is attached to this message.", attachments: [ { filename: "log.txt", contentDisposition: "inline", content: log.join("\n"), contentType: "text/plain" } ] }) bot.notice(conf.channel, "Log sent.") break case "forcelock": if (args.length < 1) return bot.say(conf.channel, "No type specified") if (args[0] !== "i" && args[0] !== "m") return bot.say(conf.channel, "Invalid lock type") logS("Channel locked: spam") lockActive = true bot.say(conf.channel, bold + "Locking channel for 60 seconds") bot.mode(conf.channel, "+" + args[0]) bot.raw("locops", "#wallops has been locked due to request") transporter.sendMail({ from: conf.mail.sender, to: conf.mail.receiver, subject: "#wallops has been locked (on demand)", text: "#wallops has been locked via the request of " + genIdentity2(e) + " with mode +" + args[0] + ".\nA log has been attached.", attachments: [ { filename: "log.txt", contentDisposition: "inline", content: log.join("\n"), contentType: "text/plain" } ] }) setTimeout(() => { lockActive = false bot.say(conf.channel, bold + "Channel unlocked") bot.mode(conf.channel, "-" + args[0]) logS("Channel unlocked") }, 60000) break } return } if (e.target === conf.channel && e.message === conf.cmdPrefix + "hostmask") { bot.say(conf.channel, "Your hostmask is: " + genIdentity2(e)) return } if (!e.message.startsWith(conf.prefix) || e.message.length < 2) return if (e.target === conf.channel) { if (e.message.length > 1000) { bot.say(conf.channel, `${e.nick}: Message too long`) return } if (!exemptUsers.includes(genIdentity2(e))) { if (cd[e.nick.toLowerCase()] > Date.now()) { bot.say(conf.channel, `${e.nick}: Whow, slow down there! (${bold}${((cd[e.nick.toLowerCase()] - Date.now()) / 1000).toFixed(1)}s${reset})`) return } if (globCd > Date.now()) { bot.say(conf.channel, `${e.nick}: Whow, slow down there! (${bold}${((globCd - Date.now()) / 1000).toFixed(1)}s${reset} channel)`) return } globCd = Date.now() + 5000 cd[e.nick.toLowerCase()] = Date.now() + 20000 } bot.raw(`wallops :${genIdentity(e)} ${reset}${e.message.slice(1)}`) logS(genIdentity2(e) + ` (${e.tags["unrealircd.org/userip"] || "???"})` + ": " + e.message.slice(1)) if (!exemptUsers.includes(genIdentity2(e))) { wallops.push(Date.now() + 60000) wallops = wallops.filter(l => l > Date.now()) if (wallops.length > 9 && !lockActive) { logS("Channel locked: spam") lockActive = true bot.say(conf.channel, bold + "Locking channel for 30 seconds") bot.mode(conf.channel, "+m") bot.raw("locops", "#wallops has been locked temporarily due to spam") transporter.sendMail({ from: conf.mail.sender, to: conf.mail.receiver, subject: "#wallops has been locked (probable spam attack)", text: "#wallops has been locked due to a probable spam attack.\nA log has been attached.", attachments: [ { filename: "log.txt", contentDisposition: "inline", content: log.join("\n"), contentType: "text/plain" } ] }) setTimeout(() => { lockActive = false bot.say(conf.channel, bold + "Channel unlocked") bot.mode(conf.channel, "-m") logS("Channel unlocked") }, 30000) } } } }); bot.on("join", (evt) => { if (evt.channel !== conf.channel || evt.nick === conf.user.nick) return logS(`${evt.nick}!${evt.ident}@${evt.hostname}${evt.gecos ? `#${evt.gecos}` : ""} (${evt.tags["unrealircd.org/userip"] || "???"}) joins`) if (evt.nick.includes("/")) return joins = joins.filter(l => l[0] !== evt.nick.toLowerCase()) joins.push([evt.nick.toLowerCase(), Date.now() + 60000]) joins = joins.filter(l => l[1] > Date.now()) if (joins.length > 5 && !lockActive) { logS("Channel locked: too many joins") lockActive = true bot.say(conf.channel, bold + "Locking channel for 60 seconds") bot.mode(conf.channel, "+i") bot.raw("locops", "#wallops has been locked temporarily due to too many joins") transporter.sendMail({ from: conf.mail.sender, to: conf.mail.receiver, subject: "#wallops has been locked (too many joins)", text: "#wallops has been locked due to too many joins.\nA log has been attached.", attachments: [ { filename: "log.txt", contentDisposition: "inline", content: log.join("\n"), contentType: "text/plain" } ] }) setTimeout(() => { lockActive = false bot.say(conf.channel, bold + "Channel unlocked") bot.mode(conf.channel, "-i") logS("Channel unlocked") }, 60000) } }) bot.on("part", (evt) => { if (evt.nick === conf.user.nick) return logS(`${evt.nick}!${evt.ident}@${evt.hostname}${evt.gecos ? `#${evt.gecos}` : ""} (${evt.tags["unrealircd.org/userip"] || "???"}) parts`) }) bot.on("quit", (evt) => { if (evt.nick === conf.user.nick) return logS(`${evt.nick}!${evt.ident}@${evt.hostname}${evt.gecos ? `#${evt.gecos}` : ""} (${evt.tags["unrealircd.org/userip"] || "???"}) quits`) }) bot.on("kick", (e) => { if (e.kicked === conf.user.nick && e.channel === conf.channel) { bot.join(conf.channel); bot.raw(`kill ${e.nick} :Go away and don't disturb me (kicked WallopsServ from ${conf.channel})`); } }); bot.on("raw", (s) => { if (!s.from_server) return if (!s.line.endsWith("\r\n")) return let r = s.line.slice(0, -2).split(" ") if (r[0].startsWith(":") && r[1] === "KILL" && r[2].toLowerCase() === conf.user.nick) { logS("Killed by " + r[0].slice(1)) transporter.sendMail({ from: conf.mail.sender, to: conf.mail.receiver, subject: "Bot killed", text: "The bot has been killed by " + r[0].slice(1) + "\nA log has been attached.", attachments: [ { filename: "log.txt", contentDisposition: "inline", content: log.join("\n"), contentType: "text/plain" } ] }) } })