diff --git a/bot.ts b/bot.ts deleted file mode 100644 index 5817c1d..0000000 --- a/bot.ts +++ /dev/null @@ -1,130 +0,0 @@ -import c from "config"; -import { Client, Message, RichEmbed } from "discord.js"; -import process from "process"; -import OuterXRegExp from "xregexp"; - -import commands from "./commands"; -import ConniebotDatabase from "./db-management"; -import embed from "./embed"; -import startup from "./startup"; -import { logMessage } from "./utils"; -import x2i from "./x2i"; - -const bot = new Client(); -const db = new ConniebotDatabase(); - -/** - * Convert a message object into a string in the form of guildname: message{0, 100} - */ -function messageSummary({ guild, content }: Message) { - const guildName = guild ? guild.name : "unknown guild"; - return `${guildName}: ${content.substr(0, 100)}`; -} - -/** - * Looks for a reply message. - * - * @param message Received message. - */ -async function command(message: Message) { - // commands - const prefixRegex = OuterXRegExp.build( - `(?:^${OuterXRegExp.escape(c.get("prefix"))})(\\S*) ?(.*)`, [], - ); - - const toks = message.content.match(prefixRegex); - if (!toks) return; - - const [, cmd, args] = toks; - const cb = commands[cmd]; - if (!cb) return; - - try { - const log = await cb(message, db, ...args.split(" ")); - logMessage(`success:command/${cmd}`, log); - } catch (err) { - // TODO: error reporting - logMessage(`error:command/${cmd}`, err); - } -} - -/** - * Sends an x2i string (but also could be used for simple embeds) - * - * @param message Message to reply to - */ -async function x2iExec(message: Message) { - let results = x2i(message.content); - const parsed = Boolean(results && results.length !== 0); - if (parsed) { - const response = new RichEmbed().setColor( - c.get("embeds.colors.success"), - ); - let logCode = "all"; - - // check timeout - const charMax = parseInt(c.get("embeds.timeoutChars"), 10); - if (results.length > charMax) { - results = `${results.slice(0, charMax - 1)}…`; - - response - .addField("Timeout", c.get("embeds.timeoutMessage")) - .setColor(c.get("embeds.colors.warning")); - - logCode = "partial"; - } - - response.setDescription(results); - logMessage(`processed:x2i/${logCode}`, messageSummary(message)); - - try { - await embed(message.channel, response); - logMessage("success:x2i"); - } catch (err) { - logMessage("error:x2i", err); - } - } - - return parsed; -} - -/** - * Acts for a response to a message. - * - * @param message Message to parse for responses - */ -async function parse(message: Message) { - if (message.author.bot) return; - if (await x2iExec(message)) return; - await command(message); -} - -/** - * Record the error and proceed to crash. - * - * @param err The error to catch. - * @param exit Should exit? (eg ECONNRESET would not require reset) - */ -async function panicResponsibly(err: any, exit = true) { - console.log(err); - await db.addError(err); - if (exit) { - process.exit(1); - } -} - -process.once("uncaughtException", panicResponsibly); - -if (!c.has("token")) { - throw new Error("Couldn't find a token to connect with."); -} - -bot.on("ready", () => startup(bot, db)) - .on("message", parse) - .on("error", err => { - if (err && err.message && err.message.includes("ECONNRESET")) { - return console.log("connection reset. oops!"); - } - panicResponsibly(err); - }) - .login(c.get("token")); diff --git a/commands.ts b/commands.ts deleted file mode 100644 index 30ad4f2..0000000 --- a/commands.ts +++ /dev/null @@ -1,71 +0,0 @@ -import c from "config"; -import { Message } from "discord.js"; - -import ConniebotDatabase from "./db-management"; -import help from "./help"; - -type Command = (message: Message, db: ConniebotDatabase, ...args: string[]) => Promise; - -interface ICommands { - [key: string]: Command; -} - -/** - * Tries to respond in a timely fashion. - * - * @param message Message to respond to (read time) - * @param roundtrip Should the heartbeat be sent to the message ("roundtrip") - */ -async function ping(message: Message, _: any, roundtrip?: string) { - // received message - const created = message.createdTimestamp; - const elapsedMsg = `${Date.now() - created} ms`; - - // wait for send - const pingReturn = await message.channel.send(`I'm alive! (${elapsedMsg})`); - const pingMsg = Array.isArray(pingReturn) ? pingReturn[0] : pingReturn; - const roundtripMsg = `${Date.now() - created} ms`; - - if (roundtrip === "roundtrip") { - pingMsg.edit(`${pingMsg}, roundtrip ${roundtripMsg}`); - } - - return `${elapsedMsg}, ${roundtripMsg}`; -} - -/** - * Set channel for an arbitrary event (currently only uses `restart` and `events`) - * - * @param db Database instance. - * @param event The event name (only the first 50 characters are used) - */ -async function setChannel(message: Message, db: ConniebotDatabase, event: string) { - if (message.author.id !== c.get("owner")) { - return message.reply("Sorry, but you don't have permissions to do that."); - } - - if (!event) { - return message.reply("Sorry, you need to specify an event."); - } - - const channel = message.channel; - let returnMessage: string; - - try { - await db.setChannel(event.substr(0, 50), channel.id); - returnMessage = `Got it! Will send notifications for ${event} to ${message.channel}.`; - } catch (err) { - console.log(err); - returnMessage = "Something went wrong while trying to set notifications."; - } - - return channel.send(returnMessage); -} - -const commands = { - help: message => help(message.channel, message.client.user), - notif: setChannel, - ping, -} as ICommands; - -export default commands; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..25d4974 --- /dev/null +++ b/index.ts @@ -0,0 +1,18 @@ +import c from "config"; + +import Conniebot from "./src"; +import commands from "./src/commands"; + +const token = "token"; +const database = "database"; + +if (!c.has(token)) { + throw new TypeError("Couldn't find a token to connect with."); +} + +if (!c.has(database)) { + throw new TypeError("No database filename listed."); +} + +const conniebot = new Conniebot(c.get(token), c.get(database)); +conniebot.registerCommands(commands); diff --git a/package-lock.json b/package-lock.json index e23d6f2..36b3aa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "conniebot", - "version": "3.1.0", + "version": "3.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d1157a5..f7c9c46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "conniebot", - "version": "3.1.0", + "version": "3.1.1", "license": "MIT", "repository": { "type": "git", @@ -9,7 +9,7 @@ "scripts": { "lint": "tslint -p . -c tslint.json -e './**/*.json'", "fix": "tslint --fix -p . -c tslint.json -e './**/*.json'", - "start": "nodemon --exitcrash --ignore *.sqlite -x ts-node bot.ts", + "start": "nodemon --exitcrash --ignore *.sqlite -x ts-node index.ts", "forever": "forever start --uid conniebot --killSignal=SIGTERM -a -c \"npm start\" ./", "test": "npm run lint" }, diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..9ed1bb2 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,70 @@ +import c from "config"; + +import { ICommands } from "."; +import help from "./help"; + +/** + * Extension methods for different reply commands. + * + * All functions are bound to the instance of the currently running Conniebot. + */ +const commands: ICommands = { + /** + * Funnels a message object to the actual {@link help} function. + */ + async help(message) { + help(message.channel, message.client.user); + }, + + /** + * Set channel for an arbitrary event. (see {@link INotifRow}) + * + * @param event The event name (only the first 50 characters are used) + */ + async notif(message, event) { + if (message.author.id !== c.get("owner")) { + return message.reply("Sorry, but you don't have permissions to do that."); + } + + if (!event) { + return message.reply("Sorry, you need to specify an event."); + } + + const channel = message.channel; + let returnMessage: string; + + try { + await this.db.setChannel(event, channel.id); + returnMessage = `Got it! Will send notifications for ${event} to ${message.channel}.`; + } catch (err) { + console.log(err); + returnMessage = "Something went wrong while trying to set notifications."; + } + + return channel.send(returnMessage); + }, + + /** + * Tries to respond in a timely fashion. + * + * @param roundtrip Should the heartbeat be sent to the message ("roundtrip") + */ + async ping(message, roundtrip?) { + // received message + const created = message.createdTimestamp; + const elapsedMsg = `${Date.now() - created} ms`; + + // wait for send + const pingReturn = await message.channel.send(`I'm alive! (${elapsedMsg})`); + const pingMsg = Array.isArray(pingReturn) ? pingReturn[0] : pingReturn; + const roundtripMsg = `${Date.now() - created} ms`; + + if (roundtrip === "roundtrip") { + pingMsg.edit(`${pingMsg}, roundtrip ${roundtripMsg}`); + } + + return `${elapsedMsg}, ${roundtripMsg}`; + }, +}; + +export default commands; diff --git a/db-management.ts b/src/db-management.ts similarity index 53% rename from db-management.ts rename to src/db-management.ts index 3940d4b..ab4acac 100644 --- a/db-management.ts +++ b/src/db-management.ts @@ -1,33 +1,85 @@ -import c from "config"; import SQL from "sql-template-strings"; import sqlite, { Database } from "sqlite"; +/** + * Key-value table of events. + * + * Currently used events: + * - `restart`: Notify restart. + * - `errors`: Notify errors. (may want to keep stack traces secret, etc) + */ interface INotifRow { + /** + * Event name (cuts off at 50 characters). + */ event: string; + + /** + * Channel ID that corresponds to the string, taken from + * [`Channel.id`](https://discord.js.org/#/docs/main/stable/class/Channel?scrollTo=id). + */ channel: string; } +/** + * A whole bunch of unsent errors. + */ interface IUnsentErrorsRow { + /** + * Autoincremented ID column. + */ id: number; + + /** + * Date that error happened (more specifically, when it was caught). + */ date: Date; + + /* tslint:disable: max-line-length */ + /** + * Stacktrace, if available. (see + * [`Error.prototype.stack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack)) + * + * `stack` is technically non-standard, and not every throw will give an Error object, so we + * default to {@link message}. + */ + /* tslint:enable: max-line-length */ stacktrace: string; + + /* tslint:disable: max-line-length */ + /** + * Message, if available. (first tries + * [`Error.message`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message), + * then defaults to stringifying) + */ + /* tslint:enable: max-line-length */ message: string; } +/** + * Sent errors, for future auditing purposes. + */ interface ISentErrorsRow extends IUnsentErrorsRow { + /** + * Date that error was sent. + */ dateSent: Date; } +/** + * Database manager for Conniebot. Uses SQLite. + */ export default class ConniebotDatabase { + /** + * Pending or completed database connection. + */ private db: Promise; - constructor(dbFile?: string) { - dbFile = dbFile || c.get("database"); - - if (!dbFile) { - throw TypeError("No database filename listed."); - } - + /** + * @param dbFile Filename of database file. Should be a `.sqlite` file. Relative to command + * directory. + */ + constructor(dbFile: string) { if (!dbFile.endsWith(".sqlite")) { console.log("Database file is not marked as `.sqlite`."); } @@ -35,6 +87,11 @@ export default class ConniebotDatabase { this.db = this.init(dbFile); } + /** + * Open a file and initialize the tables if they haven't already been created. + * + * @param fname Database filename. Relative to command directory. + */ private async init(fname: string) { const db = await sqlite.open(fname); @@ -72,7 +129,7 @@ export default class ConniebotDatabase { public async setChannel(event: string, channel: string) { return (await this.db).run( - SQL`INSERT INTO notifs(event, channel) VALUES(${event}, ${channel}) + SQL`INSERT INTO notifs(event, channel) VALUES(${event.substr(0, 50)}, ${channel}) ON CONFLICT(event) DO UPDATE SET channel=excluded.channel`, ); } @@ -84,10 +141,15 @@ export default class ConniebotDatabase { public async addError(err: any) { return (await this.db).run( SQL`INSERT INTO unsentErrors(date, stacktrace, message) - VALUES(${new Date()}, ${err.message || String(err)}, ${err.stack})`, + VALUES(${new Date()}, ${err.stack}, ${err.message || String(err)})`, ); } + /** + * Migrate error to Sent Errors table, black-holing it if the ID already exists for some reason. + * + * @param id Error ID to migrate. + */ public async moveError(id: number) { const db = await this.db; diff --git a/embed.ts b/src/embed.ts similarity index 92% rename from embed.ts rename to src/embed.ts index 0520849..17a578c 100644 --- a/embed.ts +++ b/src/embed.ts @@ -4,7 +4,7 @@ import { Channel, RichEmbed } from "discord.js"; import { isTextChannel } from "./utils"; /** - * Grabs body from RichEmbed, optionally discarding headers + * Grabs body from RichEmbed, optionally discarding headers. * * @param message Message to grab body text from * @param headersImportant Should keep headers? @@ -18,7 +18,7 @@ function handleBody(message: RichEmbed, headersImportant = false) { } /** - * Convert RichEmbed to String + * Convert RichEmbed to String. * * @param message Message to grab body text from * @param headersImportant Should keep headers? @@ -33,7 +33,7 @@ function strip(message: RichEmbed, headersImportant = false) { } /** - * Send message to channel + * Send message to channel. * * @param channel Channel to send message to * @param message Message to send diff --git a/help.ts b/src/help.ts similarity index 100% rename from help.ts rename to src/help.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b1b59cd --- /dev/null +++ b/src/index.ts @@ -0,0 +1,154 @@ +import c from "config"; +import { Client, ClientOptions, Message, RichEmbed } from "discord.js"; +import process from "process"; +import OuterXRegExp from "xregexp"; + +import ConniebotDatabase from "./db-management"; +import embed from "./embed"; +import startup from "./startup"; +import { logMessage, messageSummary } from "./utils"; +import x2i from "./x2i"; + +export type CommandCallback = + (this: Conniebot, message: Message, ...args: string[]) => Promise; + +export interface ICommands { + [key: string]: CommandCallback; +} + +export default class Conniebot { + public bot: Client; + public db: ConniebotDatabase; + private commands: ICommands; + + constructor(token: string, dbFile: string, clientOptions?: ClientOptions) { + this.bot = new Client(clientOptions); + this.db = new ConniebotDatabase(dbFile); + this.commands = {}; + + this.bot.on("ready", () => startup(this.bot, this.db)) + .on("message", this.parse) + .on("error", err => { + if (err && err.message && err.message.includes("ECONNRESET")) { + return console.log("connection reset. oops!"); + } + this.panicResponsibly(err); + }) + .login(token); + + process.once("uncaughtException", this.panicResponsibly); + } + + /** + * Record the error and proceed to crash. + * + * @param err The error to catch. + * @param exit Should exit? (eg ECONNRESET would not require reset) + */ + private panicResponsibly = async (err: any, exit = true) => { + console.log(err); + await this.db.addError(err); + if (exit) { + process.exit(1); + } + } + + /** + * Looks for a reply message. + * + * @param message Received message. + */ + private async command(message: Message) { + // commands + const prefixRegex = OuterXRegExp.build( + `(?:^${OuterXRegExp.escape(c.get("prefix"))})(\\S*) ?(.*)`, [], + ); + + const toks = message.content.match(prefixRegex); + if (!toks) return; + const [, cmd, args] = toks; + + // assume that command has already been bound + // no way currently to express this without clearing the types + const cb: any = this.commands[cmd]; + if (!cb) return; + + try { + const log = await cb(message, ...args.split(" ")); + logMessage(`success:command/${cmd}`, log); + } catch (err) { + // TODO: error reporting + logMessage(`error:command/${cmd}`, err); + } + } + + /** + * Sends an x2i string (but also could be used for simple embeds) + * + * @param message Message to reply to + */ + private async x2iExec(message: Message) { + let results = x2i(message.content); + const parsed = Boolean(results && results.length !== 0); + if (parsed) { + const response = new RichEmbed().setColor( + c.get("embeds.colors.success"), + ); + let logCode = "all"; + + // check timeout + const charMax = parseInt(c.get("embeds.timeoutChars"), 10); + if (results.length > charMax) { + results = `${results.slice(0, charMax - 1)}…`; + + response + .addField("Timeout", c.get("embeds.timeoutMessage")) + .setColor(c.get("embeds.colors.warning")); + + logCode = "partial"; + } + + response.setDescription(results); + logMessage(`processed:x2i/${logCode}`, messageSummary(message)); + + try { + await embed(message.channel, response); + logMessage("success:x2i"); + } catch (err) { + logMessage("error:x2i", err); + } + } + + return parsed; + } + + /** + * Acts for a response to a message. + * + * @param message Message to parse for responses + */ + protected parse = async (message: Message) => { + if (message.author.bot) return; + if (await this.x2iExec(message)) return; + await this.command(message); + } + + /** + * Register multiple commands at once. + */ + public registerCommands(callbacks: ICommands) { + for (const [name, cmd] of Object.entries(callbacks)) { + this.register(name, cmd); + } + } + + /** + * Register a single custom command. + * + * @param command Command name that comes after prefix. Name must be `\S+`. + * @param callback Callback upon seeing the name. `this` will be bound automatically. + */ + public register(command: string, callback: CommandCallback) { + this.commands[command] = callback.bind(this); + } +} diff --git a/startup.ts b/src/startup.ts similarity index 92% rename from startup.ts rename to src/startup.ts index 2e93df5..c408c1e 100644 --- a/startup.ts +++ b/src/startup.ts @@ -27,6 +27,9 @@ async function notifyRestart(bot: Client, db: ConniebotDatabase) { } } +/** + * Notify channel of any new errors that haven't been able to send. + */ async function notifyNewErrors(bot: Client, db: ConniebotDatabase) { const [errors, errorChannelId] = await Promise.all( [db.getUnsentErrors(), db.getChannel("errors")], @@ -67,6 +70,9 @@ async function updateActivity(bot: Client) { } } +/** + * Run through {@link updateActivity}, {@link notifyRestart}, {@link notifyNewErrors}. + */ export default async function startup(bot: Client, db: ConniebotDatabase) { console.log("Bot ready. Setting up..."); await Promise.all([updateActivity, notifyRestart, notifyNewErrors].map(fn => fn(bot, db))); diff --git a/utils.ts b/src/utils.ts similarity index 71% rename from utils.ts rename to src/utils.ts index 28117b2..b987f8c 100644 --- a/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Channel, TextChannel } from "discord.js"; +import { Channel, Message, TextChannel } from "discord.js"; /** * Prints a formatted message with a related object. @@ -13,7 +13,7 @@ export function logMessage(status: string, message?: any) { /** * Check if channel is a TextChannel. Technically it can be a guild, dm or group dm channel, but - * the default discord.js type for a text based channel is not actually a type and so must have + * the default discord.js type for a text based channel is not actually a type, so we have to have * this workaround. */ export function isTextChannel(channel: Channel): channel is TextChannel { @@ -32,3 +32,11 @@ export async function sendMessage(msg: string, channel: TextChannel) { return false; } } + +/** + * Convert a message object into a string in the form of guildname: message{0, 100} + */ +export function messageSummary({ guild, content }: Message) { + const guildName = guild ? guild.name : "unknown guild"; + return `${guildName}: ${content.substr(0, 100)}`; +} diff --git a/x2i/apie-keys.yaml b/src/x2i/apie-keys.yaml similarity index 100% rename from x2i/apie-keys.yaml rename to src/x2i/apie-keys.yaml diff --git a/x2i/index.ts b/src/x2i/index.ts similarity index 76% rename from x2i/index.ts rename to src/x2i/index.ts index 0d99cfd..ddd4ee4 100644 --- a/x2i/index.ts +++ b/src/x2i/index.ts @@ -1,4 +1,6 @@ import fs from "fs"; +import path from "path"; + import yaml from "js-yaml"; import OuterXRegExp from "xregexp"; @@ -26,38 +28,51 @@ interface IMatchInstructions { } const regex = OuterXRegExp( - `(?:(^|[\`\\p{White_Space}])) # must be preceded by whitespace or surrounded by code brackets - ([A-Za-z]*) # key, to lower (2) - ([/[]) # bracket left (3) - (\\S|\\S.*?\\S) # body (4) - ([/\\]]) # bracket right (5) - (?=$|[\`\\p{White_Space}\\pP]) # must be followed by a white space or punctuation`, + `# must be preceded by whitespace or surrounded by code brackets, or on its own line + (?:(^|[\`\\p{White_Space}])) + + # ($2) key, to lower + ([A-Za-z]*) # consumes non-tagged brackets to avoid reading the insides accidentally + # ($3) bracket left + ([/[]) + # ($4) body + ( + \\S # single character (eg x/t/) + |\\S.*?[^_\p{White_Space}] # any characters not surrounded by whitespace, ignores _/ + ) + # ($5) bracket right + ([/\\]]) + + # must be followed by a white space or punctuation (lookahead) + (?=$|[\`\\p{White_Space}\\pP])`, "gmx"); -const defaultMatchAction = (left: string, match: string, right: string) => left + match + right; - const matchType: { [key: string]: IMatchInstructions } = { p: { join: (_, match) => `*${match}`, - keys: readKeys("./x2i/apie-keys.yaml"), + keys: readKeys("./apie-keys.yaml"), }, x: { - keys: readKeys("./x2i/x2i-keys.yaml"), + keys: readKeys("./x2i-keys.yaml"), }, z: { - keys: readKeys("./x2i/z2i-keys.yaml"), + keys: readKeys("./z2i-keys.yaml"), }, }; +function defaultMatchAction(left: string, match: string, right: string) { + return left + match + right; +} + /** * Read translation keys from file. Escapes strings first. * - * @param fpath File to key definitions. (yaml, utf8) + * @param fpath File to key definitions. (yaml, utf8) Relative to {@link __dirname}. * @returns Compiled keys. */ function readKeys(fpath: string) { return yaml - .safeLoad(fs.readFileSync(fpath, "utf8")) + .safeLoad(fs.readFileSync(path.join(__dirname, fpath), "utf8")) .map(compileKey) .filter(Boolean) as CompiledReplacer[]; } diff --git a/x2i/x2i-keys.yaml b/src/x2i/x2i-keys.yaml similarity index 100% rename from x2i/x2i-keys.yaml rename to src/x2i/x2i-keys.yaml diff --git a/x2i/z2i-keys.yaml b/src/x2i/z2i-keys.yaml similarity index 99% rename from x2i/z2i-keys.yaml rename to src/x2i/z2i-keys.yaml index a477c16..8ee0cc1 100644 --- a/x2i/z2i-keys.yaml +++ b/src/x2i/z2i-keys.yaml @@ -99,10 +99,6 @@ - ƙ - - q_< - ʠ -- - ;\ - - ǃ͡¡ -- - +\ - - '|||' - - t`_m - ȶ - - d`_m @@ -286,15 +282,13 @@ - - _/ - ̌ - - _; - - ⁿ + - ͋ - - _=\ - "˭" - - _= - ̩ - - "=" - ̩ -- - _> - - ʼ - - _?\ - ˁ - - _? @@ -520,6 +514,34 @@ - ˤ - - +? - ˀ +- - _<\ + - "↓" +- - _>\ + - "↑" +- - _> + - ʼ +- - _< + - ʼ↓ +- - <\ + - ʢ +- - '>\' + - ʡ +- delimiters: ['<', '>'] + translations: + - - '1' + - ˩ + - - '2' + - ˨ + - - '3' + - ˧ + - - '4' + - ˦ + - - '5' + - ˥ + - - 'F' + - ↘ + - - 'R' + - ↗ - - a - a - - ä @@ -796,8 +818,6 @@ - ʫ - - Z - ʒ -- - ^\ - - ğ - - '.' - '.' - - '"' @@ -818,28 +838,6 @@ - æ - - '}' - ʉ -- - _<\ - - "↓" -- - _>\ - - "↑" -- - _< - - ʼ↓ -- - <\ - - ʢ -- - '>\' - - ʡ -- delimiters: ['<', '>'] - translations: - - - '1' - - ˩ - - - '2' - - ˨ - - - '3' - - ˧ - - - '4' - - ˦ - - - '5' - - ˥ - - '1' - ɨ - - 2\ @@ -886,7 +884,7 @@ - ʕ - - '?' - ʔ -- - \^ +- - ^\ - ğ - - ^ - ꜛ @@ -920,6 +918,10 @@ - ‖ - - '|' - '|' +- - ;\ + - ǃ͡¡ +- - +\ + - '⦀' - - '`' - ˞ - - ; diff --git a/tsconfig.json b/tsconfig.json index 82f1c4e..8d236b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2017", "module": "commonjs", "strict": true, "noImplicitAny": true, diff --git a/tslint.json b/tslint.json index c011757..700df1e 100644 --- a/tslint.json +++ b/tslint.json @@ -68,9 +68,6 @@ "es6": true, "node": true }, - "plugins": [ - "react" - ], "extends": [ "tslint:recommended" ]