diff --git a/package-lock.json b/package-lock.json index d00a2faf..bf54bad3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9717,6 +9717,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "seedrandom": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.1.tgz", + "integrity": "sha512-1/02Y/rUeU1CJBAGLebiC5Lbo5FnB22gQbIFFYTLkwvp1xdABZJH1sn4ZT1MzXmPpzv+Rf/Lu2NcsLJiK4rcDg==" + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", diff --git a/package.json b/package.json index 35fd1783..bcb78641 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "passport-custom": "^1.0.5", "passport-oauth2": "^1.5.0", "reflect-metadata": "^0.1.12", + "seedrandom": "^3.0.1", "tlds": "^1.203.1", "tmp": "0.0.33", "ts-node": "^3.3.0", diff --git a/src/plugins/LogServer.ts b/src/plugins/LogServer.ts new file mode 100644 index 00000000..9a94b638 --- /dev/null +++ b/src/plugins/LogServer.ts @@ -0,0 +1,92 @@ +import http, { ServerResponse } from "http"; +import { GlobalPlugin, IPluginOptions, logger } from "knub"; +import { GuildArchives } from "../data/GuildArchives"; +import { sleep } from "../utils"; +import moment from "moment-timezone"; + +const DEFAULT_PORT = 9920; +const archivesRegex = /^\/(spam-logs|archives)\/([a-z0-9\-]+)\/?$/i; + +function notFound(res: ServerResponse) { + res.statusCode = 404; + res.end("Not Found"); +} + +interface ILogServerPluginConfig { + port: number; +} + +export class LogServerPlugin extends GlobalPlugin { + public static pluginName = "log_server"; + + protected archives: GuildArchives; + protected server: http.Server; + + protected getDefaultOptions(): IPluginOptions { + return { + config: { + port: DEFAULT_PORT, + }, + }; + } + + async onLoad() { + this.archives = new GuildArchives(null); + + this.server = http.createServer(async (req, res) => { + const pathMatch = req.url.match(archivesRegex); + if (!pathMatch) return notFound(res); + + const logId = pathMatch[2]; + + if (pathMatch[1] === "spam-logs") { + res.statusCode = 301; + res.setHeader("Location", `/archives/${logId}`); + return; + } + + if (pathMatch) { + const log = await this.archives.find(logId); + if (!log) return notFound(res); + + let body = log.body; + + // Add some metadata at the end of the log file (but only if it doesn't already have it directly in the body) + if (log.body.indexOf("Log file generated on") === -1) { + const createdAt = moment(log.created_at).format("YYYY-MM-DD [at] HH:mm:ss [(+00:00)]"); + body += `\n\nLog file generated on ${createdAt}`; + + if (log.expires_at !== null) { + const expiresAt = moment(log.expires_at).format("YYYY-MM-DD [at] HH:mm:ss [(+00:00)]"); + body += `\nExpires at ${expiresAt}`; + } + } + + res.setHeader("Content-Type", "text/plain; charset=UTF-8"); + res.end(body); + } + }); + + const port = this.getConfig().port; + let retried = false; + + this.server.on("error", async (err: any) => { + if (err.code === "EADDRINUSE" && !retried) { + logger.info("Got EADDRINUSE, retrying in 2 sec..."); + retried = true; + await sleep(2000); + this.server.listen(port); + } else { + throw err; + } + }); + + this.server.listen(port); + } + + async onUnload() { + return new Promise(resolve => { + this.server.close(() => resolve()); + }); + } +} diff --git a/src/plugins/Tags.ts b/src/plugins/Tags.ts index bcb8902b..dad4e1b7 100644 --- a/src/plugins/Tags.ts +++ b/src/plugins/Tags.ts @@ -1,6 +1,6 @@ import { decorators as d, IPluginOptions, logger } from "knub"; import { Message, TextChannel } from "eris"; -import { errorMessage, successMessage } from "../utils"; +import { errorMessage, successMessage, stripObjectToScalars } from "../utils"; import { GuildTags } from "../data/GuildTags"; import { GuildSavedMessages } from "../data/GuildSavedMessages"; import { SavedMessage } from "../data/entities/SavedMessage"; @@ -174,13 +174,14 @@ export class TagsPlugin extends ZeppelinPlugin { msg.channel.createMessage(`Tag source:\n${url}`); } - async renderTag(body, args = []) { + async renderTag(body, args = [], extraData = {}) { const dynamicVars = {}; const maxTagFnCalls = 25; let tagFnCalls = 0; const data = { args, + ...extraData, ...this.tagFunctions, set(name, val) { if (typeof name !== "string") return; @@ -206,7 +207,7 @@ export class TagsPlugin extends ZeppelinPlugin { if (msg.is_bot) return; const member = await this.getMember(msg.user_id); - if (!this.hasPermission("can_use", { member, channelId: msg.channel_id })) return; + if (!member || !this.hasPermission("can_use", { member, channelId: msg.channel_id })) return; if (!msg.data.content) return; if (msg.is_bot) return; @@ -229,7 +230,10 @@ export class TagsPlugin extends ZeppelinPlugin { // Format the string try { - body = await this.renderTag(body, tagArgs); + body = await this.renderTag(body, tagArgs, { + member: stripObjectToScalars(member, ["user"]), + user: stripObjectToScalars(member.user), + }); } catch (e) { if (e instanceof TemplateParseError) { logger.warn(`Invalid tag format!\nError: ${e.message}\nFormat: ${tag.body}`); diff --git a/src/plugins/availablePlugins.ts b/src/plugins/availablePlugins.ts index 82974437..b778e437 100644 --- a/src/plugins/availablePlugins.ts +++ b/src/plugins/availablePlugins.ts @@ -22,6 +22,7 @@ import { BotControlPlugin } from "./BotControl"; import { UsernameSaver } from "./UsernameSaver"; import { CustomEventsPlugin } from "./CustomEvents"; import { GuildInfoSaverPlugin } from "./GuildInfoSaver"; +import { LogServerPlugin } from "./LogServer"; /** * Plugins available to be loaded for individual guilds @@ -65,4 +66,4 @@ export const basePlugins = [ /** * Available global plugins (can't be loaded per-guild, only globally) */ -export const availableGlobalPlugins = [BotControlPlugin, UsernameSaver]; +export const availableGlobalPlugins = [BotControlPlugin, UsernameSaver, LogServerPlugin]; diff --git a/src/templateFormatter.ts b/src/templateFormatter.ts index 3149eb53..2449a5d9 100644 --- a/src/templateFormatter.ts +++ b/src/templateFormatter.ts @@ -1,4 +1,5 @@ import { has, get } from "./utils"; +import seedrandom from "seedrandom"; const TEMPLATE_CACHE_SIZE = 200; const templateCache: Map = new Map(); @@ -274,6 +275,9 @@ const baseValues = { concat(...args) { return [...args].join(""); }, + concatArr(arr, separator = "") { + return arr.join(separator); + }, eq(...args) { if (args.length < 2) return true; for (let i = 1; i < args.length; i++) { @@ -299,7 +303,7 @@ const baseValues = { if (end != null && isNaN(end)) return ""; return arg1.slice(parseInt(start, 10), end && parseInt(end, 10)); }, - rand(from, to) { + rand(from, to, seed = null) { if (isNaN(from)) return 0; if (to == null) { @@ -313,7 +317,9 @@ const baseValues = { [from, to] = [to, from]; } - return Math.round(Math.random() * (to - from) + from); + let randValue = seed != null ? new seedrandom(seed)() : Math.random(); + + return Math.round(randValue * (to - from) + from); }, add(...args) { return args.reduce((result, arg) => {