diff --git a/src/bot.ts b/src/bot.ts index 1649dd72406cdbfaa6b282567d6f17f63bdbf106..ed6288719cd2a42a10826bfc28a6bade143c756a 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,7 +1,7 @@ import { DiscordBridgeConfig } from "./config"; import { DiscordClientFactory } from "./clientfactory"; import { DiscordStore } from "./store"; -import { DbGuildEmoji } from "./db/dbdataemoji"; +import { DbEmoji } from "./db/dbdataemoji"; import { DbEvent } from "./db/dbdataevent"; import { MatrixUser, RemoteUser, Bridge, Entry } from "matrix-appservice-bridge"; import { Util } from "./util"; @@ -19,6 +19,9 @@ import { Provisioner } from "./provisioner"; // messages get delayed from discord. const MSG_PROCESS_DELAY = 750; const MIN_PRESENCE_UPDATE_DELAY = 250; +const AVATAR_SIZE = 512; // matrix -> discord +const MAX_DISCORD_NAME_LENGTH = 32; +const DISCORD_NAME_START = 0; // TODO: This is bad. We should be serving the icon from the own homeserver. const MATRIX_ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org/mlxoESwIsTbJrfXyAAogrNxA"; class ChannelLookupResult { @@ -308,19 +311,18 @@ export class DiscordBot { }); } - public async GetGuildEmoji(guild: Discord.Guild, id: string): Promise<string> { - const dbEmoji: DbGuildEmoji = await this.store.Get(DbGuildEmoji, {emoji_id: id}); + public async GetEmoji(name: string, animated: boolean, id: string): Promise<string> { + if (!id.match(/^\d+$/)) { + throw new Error("Non-numerical ID"); + } + const dbEmoji: DbEmoji = await this.store.Get(DbEmoji, {emoji_id: id}); if (!dbEmoji.Result) { - // Fetch the emoji - if (!guild.emojis.has(id)) { - throw new Error("The guild does not contain the emoji"); - } - const emoji: Discord.Emoji = guild.emojis.get(id); + const url = "https://cdn.discordapp.com/emojis/" + id + (animated ? ".gif" : ".png"); const intent = this.bridge.getIntent(); - const mxcUrl = (await Util.UploadContentFromUrl(emoji.url, intent, emoji.name)).mxcUrl; - dbEmoji.EmojiId = emoji.id; - dbEmoji.GuildId = guild.id; - dbEmoji.Name = emoji.name; + const mxcUrl = (await Util.UploadContentFromUrl(url, intent, name)).mxcUrl; + dbEmoji.EmojiId = id; + dbEmoji.Name = name; + dbEmoji.Animated = animated; dbEmoji.MxcUrl = mxcUrl; await this.store.Insert(dbEmoji); } diff --git a/src/db/dbdataemoji.ts b/src/db/dbdataemoji.ts index 46d8b79cd424423455521b407ad74af4bf77b46f..d0534e7cc25e3c3d2ff44e4dfca8a7a1ec2334ad 100644 --- a/src/db/dbdataemoji.ts +++ b/src/db/dbdataemoji.ts @@ -2,10 +2,10 @@ import { DiscordStore } from "../store"; import { IDbData } from "./dbdatainterface"; import * as log from "npmlog"; -export class DbGuildEmoji implements IDbData { +export class DbEmoji implements IDbData { public EmojiId: string; - public GuildId: string; public Name: string; + public Animated: boolean; public MxcUrl: string; public CreatedAt: number; public UpdatedAt: number; @@ -14,15 +14,15 @@ export class DbGuildEmoji implements IDbData { public RunQuery(store: DiscordStore, params: any): Promise<null> { return store.db.getAsync(` SELECT * - FROM guild_emoji + FROM emoji WHERE emoji_id = $id`, { $id: params.emoji_id, }).then((row) => { this.Result = row !== undefined; if (this.Result) { this.EmojiId = row.emoji_id; - this.GuildId = row.guild_id; this.Name = row.name; + this.Animated = row.animated; this.MxcUrl = row.mxc_url; this.CreatedAt = row.created_at; this.UpdatedAt = row.updated_at; @@ -34,12 +34,12 @@ export class DbGuildEmoji implements IDbData { this.CreatedAt = new Date().getTime(); this.UpdatedAt = this.CreatedAt; return store.db.runAsync(` - INSERT INTO guild_emoji - (emoji_id,guild_id,name,mxc_url,created_at,updated_at) - VALUES ($emoji_id,$guild_id,$name,$mxc_url,$created_at,$updated_at);`, { + INSERT INTO emoji + (emoji_id,name,animated,mxc_url,created_at,updated_at) + VALUES ($emoji_id,$name,$animated,$mxc_url,$created_at,$updated_at);`, { $emoji_id: this.EmojiId, - $guild_id: this.GuildId, $name: this.Name, + $animated: this.Animated, $mxc_url: this.MxcUrl, $created_at: this.CreatedAt, $updated_at: this.UpdatedAt, @@ -50,16 +50,16 @@ export class DbGuildEmoji implements IDbData { // Ensure this has incremented by 1 for Insert+Update operations. this.UpdatedAt = new Date().getTime() + 1; return store.db.runAsync(` - UPDATE guild_emoji + UPDATE emoji SET name = $name, + animated = $animated, mxc_url = $mxc_url, updated_at = $updated_at WHERE - emoji_id = $emoji_id - AND guild_id = $guild_id`, { + emoji_id = $emoji_id`, { $emoji_id: this.EmojiId, - $guild_id: this.GuildId, $name: this.Name, + $animated: this.Animated, $mxc_url: this.MxcUrl, $updated_at: this.UpdatedAt, }); diff --git a/src/db/schema/v7.ts b/src/db/schema/v7.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bb8b70a6a67e63d4a6a2e0c9671bd693f351510 --- /dev/null +++ b/src/db/schema/v7.ts @@ -0,0 +1,37 @@ +import {IDbSchema} from "./dbschema"; +import {DiscordStore} from "../../store"; +import {DiscordClientFactory} from "../../clientfactory"; +import * as log from "npmlog"; +import * as Bluebird from "bluebird"; + +export class Schema implements IDbSchema { + public description = "create guild emoji table"; + public run(store: DiscordStore): Promise<null> { + return store.create_table(` + CREATE TABLE emoji ( + emoji_id TEXT NOT NULL, + name TEXT NOT NULL, + animated INTEGER NOT NULL, + mxc_url TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY(emoji_id) + );`, "emoji").then(() => { + // migrate existing emoji + return store.db.execAsync(` + INSERT INTO emoji + (emoji_id, name, animated, mxc_url, created_at, updated_at) + SELECT emoji_id, name, 0 AS animated, mxc_url, created_at, updated_at FROM guild_emoji; + `).error(() => { + // ignore errors + log.warning("DiscordSchema", "Failed to migrate old data to new table"); + }); + }); + } + + public rollBack(store: DiscordStore): Promise <null> { + return store.db.execAsync( + `DROP TABLE IF EXISTS emoji;`, + ); + } +} diff --git a/src/messageprocessor.ts b/src/messageprocessor.ts index cac98a036261a07861e7c3d27b641e27ecff5e0f..57e1cb35ad4af9a1d9c3b5151a155e9f73105eea 100644 --- a/src/messageprocessor.ts +++ b/src/messageprocessor.ts @@ -9,12 +9,13 @@ const USER_REGEX_POSTMARK = /<@!?([0-9]*)>/g; const CHANNEL_REGEX = /<#?([0-9]*)>/g; const CHANNEL_REGEX_POSTMARK = /<#?([0-9]*)>/g; const EMOJI_SIZE = "1em"; -const EMOJI_REGEX = /<:(\w+):([0-9]*)>/g; -const EMOJI_REGEX_POSTMARK = /<:(\w+):([0-9]*)>/g; +const EMOJI_REGEX = /<(a?):(\w+):([0-9]*)>/g; +const EMOJI_REGEX_POSTMARK = /<(a?):(\w+):([0-9]*)>/g; const MATRIX_TO_LINK = "https://matrix.to/#/"; -const NAME_EMOJI_REGEX_GROUP = 1; -const ID_EMOJI_REGEX_GROUP = 2; +const ANIMATED_EMOJI_REGEX_GROUP = 1; +const NAME_EMOJI_REGEX_GROUP = 2; +const ID_EMOJI_REGEX_GROUP = 3; marked.setOptions({ sanitize: true, @@ -144,11 +145,12 @@ export class MessageProcessor { public async ReplaceEmoji(content: string, msg: Discord.Message): Promise<string> { let results = EMOJI_REGEX.exec(content); while (results !== null) { + const animated = results[ANIMATED_EMOJI_REGEX_GROUP] === "a"; const name = results[NAME_EMOJI_REGEX_GROUP]; const id = results[ID_EMOJI_REGEX_GROUP]; try { - // we still fetch the mxcUrl to check if the emoji is valid - const mxcUrl = await this.opts.bot.GetGuildEmoji(msg.guild, id); + // we still fetch the mxcUrl to check if the emoji is valid= + const mxcUrl = await this.bot.GetEmoji(name, animated, id); content = content.replace(results[0], `:${name}:`); } catch (ex) { log.warn("MessageProcessor", @@ -163,10 +165,11 @@ export class MessageProcessor { public async ReplaceEmojiPostmark(content: string, msg: Discord.Message): Promise<string> { let results = EMOJI_REGEX_POSTMARK.exec(content); while (results !== null) { + const animated = results[ANIMATED_EMOJI_REGEX_GROUP] === "a"; const name = escapeHtml(results[NAME_EMOJI_REGEX_GROUP]); const id = results[ID_EMOJI_REGEX_GROUP]; try { - const mxcUrl = await this.opts.bot.GetGuildEmoji(msg.guild, id); + const mxcUrl = await this.bot.GetEmoji(name, animated, id); content = content.replace(results[0], `<img alt="${name}" src="${mxcUrl}" style="height: ${EMOJI_SIZE};"/>`); } catch (ex) { diff --git a/src/store.ts b/src/store.ts index 6ae5d09e3e25878207d4bfbdf5dc85adb948dc18..6bb4ec47129243d6775658c5c837b9482bf3f5dd 100644 --- a/src/store.ts +++ b/src/store.ts @@ -4,7 +4,7 @@ import * as Bluebird from "bluebird"; import * as fs from "fs"; import { IDbSchema } from "./db/schema/dbschema"; import { IDbData} from "./db/dbdatainterface"; -const CURRENT_SCHEMA = 6; +const CURRENT_SCHEMA = 7; /** * Stores data for specific users and data not specific to rooms. */ @@ -136,22 +136,22 @@ export class DiscordStore { public get_user_discord_ids(userId: string): Promise<string[]> { log.silly("SQL", "get_user_discord_ids => %s", userId); - return this.db.getAsync( + return this.db.allAsync( ` SELECT discord_id FROM user_id_discord_id - WHERE user_id = $userId + WHERE user_id = $userId; `, { $userId: userId, }, ).then( (rows) => { if (rows !== undefined) { - rows.map((row) => row.discord_id); + return rows.map((row) => row.discord_id); } else { return []; } }).catch( (err) => { - log.error("DiscordStore", "Error getting discord ids %s", err.Error); + log.error("DiscordStore", "Error getting discord ids: %s", err.Error); throw err; }); } diff --git a/test/test_messageprocessor.ts b/test/test_messageprocessor.ts index 9e3cf5d838adaeb7d06e2983e263db90a1b9a223..063a29d80d226e4ca9dde2557ebc5ced1c0c87c4 100644 --- a/test/test_messageprocessor.ts +++ b/test/test_messageprocessor.ts @@ -16,7 +16,7 @@ log.level = "silly"; // const assert = Chai.assert; const bot = { - GetGuildEmoji: (guild: Discord.Guild, id: string): Promise<string> => { + GetEmoji: (name: string, animated: boolean, id: string): Promise<string> => { if (id === "3333333") { return Promise.resolve("mxc://image"); } else { diff --git a/test/test_store.ts b/test/test_store.ts index 4d6c2bd969eb25da1539ba41b5b58213c98d7751..5ba015a11e8b8962c45631cac394a86f4469e807 100644 --- a/test/test_store.ts +++ b/test/test_store.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; // import * as Proxyquire from "proxyquire"; import { DiscordStore } from "../src/store"; import * as log from "npmlog"; -import { DbGuildEmoji } from "../src/db/dbdataemoji"; +import { DbEmoji } from "../src/db/dbdataemoji"; import { DbEvent } from "../src/db/dbdataevent"; Chai.use(ChaiAsPromised); @@ -29,13 +29,13 @@ describe("DiscordStore", () => { })).to.eventually.be.fulfilled; }); }); - describe("Get|Insert|Update<DbGuildEmoji>", () => { + describe("Get|Insert|Update<DbEmoji>", () => { it("should insert successfully", () => { const store = new DiscordStore(":memory:"); return expect(store.init().then(() => { - const emoji = new DbGuildEmoji(); + const emoji = new DbEmoji(); emoji.EmojiId = "123"; - emoji.GuildId = "456"; + emoji.Animated = false; emoji.Name = "TestEmoji"; emoji.MxcUrl = "TestUrl"; return store.Insert(emoji); @@ -44,37 +44,37 @@ describe("DiscordStore", () => { it("should get successfully", async () => { const store = new DiscordStore(":memory:"); await store.init(); - const insertEmoji = new DbGuildEmoji(); + const insertEmoji = new DbEmoji(); insertEmoji.EmojiId = "123"; - insertEmoji.GuildId = "456"; + insertEmoji.Animated = false; insertEmoji.Name = "TestEmoji"; insertEmoji.MxcUrl = "TestUrl"; await store.Insert(insertEmoji); - const getEmoji = await store.Get(DbGuildEmoji, {emoji_id: "123"}); + const getEmoji = await store.Get(DbEmoji, {emoji_id: "123"}); Chai.assert.equal(getEmoji.Name, "TestEmoji"); Chai.assert.equal(getEmoji.MxcUrl, "TestUrl"); }); it("should not return nonexistant emoji", async () => { const store = new DiscordStore(":memory:"); await store.init(); - const getEmoji = await store.Get(DbGuildEmoji, {emoji_id: "123"}); + const getEmoji = await store.Get(DbEmoji, {emoji_id: "123"}); Chai.assert.isFalse(getEmoji.Result); }); it("should update successfully", async () => { const store = new DiscordStore(":memory:"); await store.init(); - const insertEmoji = new DbGuildEmoji(); + const insertEmoji = new DbEmoji(); insertEmoji.EmojiId = "123"; - insertEmoji.GuildId = "456"; + insertEmoji.Animated = false; insertEmoji.Name = "TestEmoji"; insertEmoji.MxcUrl = "TestUrl"; await store.Insert(insertEmoji); insertEmoji.EmojiId = "123"; - insertEmoji.GuildId = "456"; + insertEmoji.Animated = false; insertEmoji.Name = "TestEmoji2"; insertEmoji.MxcUrl = "NewURL"; await store.Update(insertEmoji); - const getEmoji = await store.Get(DbGuildEmoji, {emoji_id: "123"}); + const getEmoji = await store.Get(DbEmoji, {emoji_id: "123"}); Chai.assert.equal(getEmoji.Name, "TestEmoji2"); Chai.assert.equal(getEmoji.MxcUrl, "NewURL"); Chai.assert.notEqual(getEmoji.CreatedAt, getEmoji.UpdatedAt);