mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-27 11:40:26 +00:00
Discord: add Mirror class (#1732)
Adds a class to persist a local mirror of data from a Discord Guild. Implements create and read functionality. Adds a function to `models` which converts an Emoji string-reference into an Emoji object. Test plan: Unit tests added. Paired with: @beanow
This commit is contained in:
parent
28115bba96
commit
898282becc
@ -101,6 +101,17 @@ export function emojiToRef({id, name}: Emoji): EmojiRef {
|
|||||||
return `${name}:${id}`;
|
return `${name}:${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Emoji object based on a string reference in the form:
|
||||||
|
* `${name}:${id}`
|
||||||
|
*/
|
||||||
|
export function refToEmoji(ref: EmojiRef): Emoji {
|
||||||
|
// TODO: Test that ref is in correct form
|
||||||
|
const [name, id] = ref.split(":");
|
||||||
|
if (!id) return {id: null, name};
|
||||||
|
return {id, name};
|
||||||
|
}
|
||||||
|
|
||||||
// Determines whether the message was created by a webhook or a Discord User
|
// Determines whether the message was created by a webhook or a Discord User
|
||||||
export function isAuthoredByNonUser(rawMessage: {
|
export function isAuthoredByNonUser(rawMessage: {
|
||||||
+webhook_id?: Snowflake,
|
+webhook_id?: Snowflake,
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import {emojiToRef, isAuthoredByNonUser} from "./models";
|
import {emojiToRef, refToEmoji, isAuthoredByNonUser} from "./models";
|
||||||
|
|
||||||
describe("plugins/discord/models", () => {
|
describe("plugins/discord/models", () => {
|
||||||
describe("model helper functions", () => {
|
describe("model helper functions", () => {
|
||||||
describe("emojiToRef", () => {
|
describe("emojiToRef", () => {
|
||||||
it("returns name if id is null", () => {
|
it("returns name if id is null", () => {
|
||||||
expect(emojiToRef({id: null, name: "testEmojiName"})).toBe(
|
expect(emojiToRef({id: null, name: "🐙"})).toBe("🐙");
|
||||||
"testEmojiName"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it("returns name and id if id is not null", () => {
|
it("returns name and id if id is not null", () => {
|
||||||
expect(emojiToRef({id: "testEmojiId", name: "testEmojiName"})).toBe(
|
expect(emojiToRef({id: "id", name: "name"})).toBe("name:id");
|
||||||
"testEmojiName:testEmojiId"
|
});
|
||||||
);
|
});
|
||||||
|
describe("refToEmoji", () => {
|
||||||
|
it("returns correct object if id is null", () => {
|
||||||
|
expect(refToEmoji("🐙")).toEqual({name: "🐙", id: null});
|
||||||
|
});
|
||||||
|
it("returns correct object if id is not null", () => {
|
||||||
|
expect(refToEmoji("name:id")).toEqual({name: "name", id: "id"});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("isAuthoredByNonUser", () => {
|
describe("isAuthoredByNonUser", () => {
|
||||||
|
500
src/plugins/discord/sqliteMirror.js
Normal file
500
src/plugins/discord/sqliteMirror.js
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import type Database from "better-sqlite3";
|
||||||
|
import stringify from "json-stable-stringify";
|
||||||
|
import * as Model from "./models";
|
||||||
|
import dedent from "../../util/dedent";
|
||||||
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
|
const VERSION = "DISCORD_MIRROR_v1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a local copy of data from a Discord Guild.
|
||||||
|
* Implements create and read functionality.
|
||||||
|
*
|
||||||
|
* Each Mirror instance is tied to a particular Guild. Trying to use a mirror
|
||||||
|
* for multiple Discord Guilds is not permitted; use separate Mirrors.
|
||||||
|
*
|
||||||
|
* Note that Mirror persists separate Tables for Users and Guild Members.
|
||||||
|
* Members are distinguished by membership in the Guild. Non-Member
|
||||||
|
* Discord Users can participate in a Guild's activity by leaving comments,
|
||||||
|
* reactions, etc. In our model, Members have a property, User, which
|
||||||
|
* represents a full User object. Because of this, we save the User in the
|
||||||
|
* AddMember method.
|
||||||
|
*/
|
||||||
|
export class SqliteMirror {
|
||||||
|
+_db: Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new SqliteMirror instance.
|
||||||
|
*
|
||||||
|
* Takes a Database and GuildId.
|
||||||
|
*/
|
||||||
|
constructor(db: Database, guildId: Model.Snowflake) {
|
||||||
|
this._db = db;
|
||||||
|
this._transaction(() => {
|
||||||
|
this._initialize(guildId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_transaction(queries: () => void) {
|
||||||
|
const db = this._db;
|
||||||
|
if (db.inTransaction) {
|
||||||
|
throw new Error("already in transaction");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
db.prepare("BEGIN").run();
|
||||||
|
queries();
|
||||||
|
if (db.inTransaction) {
|
||||||
|
db.prepare("COMMIT").run();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (db.inTransaction) {
|
||||||
|
db.prepare("ROLLBACK").run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialize(guild: Model.Snowflake) {
|
||||||
|
const db = this._db;
|
||||||
|
// We store the config in a singleton table `meta`, whose unique row
|
||||||
|
// has PRIMARY KEY `0`. Only the first ever insert will succeed; we
|
||||||
|
// are locked into the first config.
|
||||||
|
db.prepare(
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
zero INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
config TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
const config = stringify({
|
||||||
|
version: VERSION,
|
||||||
|
guild,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingConfig: string | void = db
|
||||||
|
.prepare("SELECT config FROM meta")
|
||||||
|
.pluck()
|
||||||
|
.get();
|
||||||
|
if (existingConfig === config) {
|
||||||
|
// Already set up; nothing to do.
|
||||||
|
return;
|
||||||
|
} else if (existingConfig !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"Database already populated with incompatible server or version"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
db.prepare("INSERT INTO meta (zero, config) VALUES (0, ?)").run(config);
|
||||||
|
|
||||||
|
const tables = [
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE channels (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE users (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
discriminator TEXT NOT NULL,
|
||||||
|
bot INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE members (
|
||||||
|
user_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
nick TEXT,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
author_id TEXT NOT NULL,
|
||||||
|
non_user_author INTEGER NOT NULL,
|
||||||
|
timestamp_ms INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(channel_id) REFERENCES channels(id)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
dedent`
|
||||||
|
CREATE INDEX messages__chanel_id
|
||||||
|
ON messages (channel_id)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE message_reactions (
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
author_id TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(channel_id) REFERENCES channels(id),
|
||||||
|
FOREIGN KEY(message_id) REFERENCES messages(id),
|
||||||
|
FOREIGN KEY(author_id) REFERENCES users(id),
|
||||||
|
CONSTRAINT value_object PRIMARY KEY (channel_id, message_id, author_id, emoji)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE INDEX message_reactions__channel_id__message_id
|
||||||
|
ON message_reactions (channel_id, message_id)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE TABLE message_mentions (
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(channel_id) REFERENCES channels(id),
|
||||||
|
FOREIGN KEY(message_id) REFERENCES messages(id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id),
|
||||||
|
CONSTRAINT value_object PRIMARY KEY (channel_id, message_id, user_id)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
dedent`\
|
||||||
|
CREATE INDEX message_mentions__channel_id__message_id
|
||||||
|
ON message_mentions (channel_id, message_id)
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
for (const sql of tables) {
|
||||||
|
db.prepare(sql).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users(): $ReadOnlyArray<Model.User> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
discriminator,
|
||||||
|
bot
|
||||||
|
FROM users`
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.map((x) => ({
|
||||||
|
id: x.id,
|
||||||
|
username: x.username,
|
||||||
|
discriminator: x.discriminator,
|
||||||
|
bot: x.bot === 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
user(id: Model.Snowflake): ?Model.User {
|
||||||
|
const user = this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
discriminator,
|
||||||
|
bot
|
||||||
|
FROM users
|
||||||
|
WHERE id = :id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get({id: id});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
discriminator: user.discriminator,
|
||||||
|
bot: user.bot === 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
members(): $ReadOnlyArray<Model.GuildMember> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
nick
|
||||||
|
FROM members`
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.map((x) => ({
|
||||||
|
user: NullUtil.get(
|
||||||
|
this.user(x.user_id),
|
||||||
|
`No user_id found for ${x.user_id}`
|
||||||
|
),
|
||||||
|
nick: x.nick,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
member(userId: Model.Snowflake): ?Model.GuildMember {
|
||||||
|
const member = this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
nick
|
||||||
|
FROM members
|
||||||
|
WHERE user_id = :user_id
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get({user_id: userId});
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
user: NullUtil.get(this.user(userId), `No user_id found for ${userId}`),
|
||||||
|
nick: member.nick,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channels(): $ReadOnlyArray<Model.Channel> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name
|
||||||
|
FROM channels`
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
messages(channel: Model.Snowflake): $ReadOnlyArray<Model.Message> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
channel_id,
|
||||||
|
author_id,
|
||||||
|
non_user_author,
|
||||||
|
timestamp_ms,
|
||||||
|
content
|
||||||
|
FROM messages
|
||||||
|
WHERE channel_id = :channel_id`
|
||||||
|
)
|
||||||
|
.all({channel_id: channel})
|
||||||
|
.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
channelId: m.channel_id,
|
||||||
|
authorId: m.author_id,
|
||||||
|
nonUserAuthor: m.non_user_author === 1,
|
||||||
|
timestampMs: m.timestamp_ms,
|
||||||
|
content: m.content,
|
||||||
|
reactionEmoji: this.reactionEmoji(m.channel_id, m.id),
|
||||||
|
mentions: this.mentions(m.channel_id, m.id),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
reactions(
|
||||||
|
channel: Model.Snowflake,
|
||||||
|
message: Model.Snowflake
|
||||||
|
): $ReadOnlyArray<Model.Reaction> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
channel_id,
|
||||||
|
message_id,
|
||||||
|
author_id,
|
||||||
|
emoji
|
||||||
|
FROM message_reactions
|
||||||
|
WHERE channel_id = :channel_id
|
||||||
|
AND message_id = :message_id`
|
||||||
|
)
|
||||||
|
.all({channel_id: channel, message_id: message})
|
||||||
|
.map((r) => ({
|
||||||
|
channelId: r.channel_id,
|
||||||
|
messageId: r.message_id,
|
||||||
|
authorId: r.author_id,
|
||||||
|
emoji: Model.refToEmoji(r.emoji),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
mentions(
|
||||||
|
channel: Model.Snowflake,
|
||||||
|
message: Model.Snowflake
|
||||||
|
): $ReadOnlyArray<Model.Snowflake> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT user_id
|
||||||
|
FROM message_mentions
|
||||||
|
WHERE channel_id = :channel_id
|
||||||
|
AND message_id = :message_id`
|
||||||
|
)
|
||||||
|
.all({channel_id: channel, message_id: message})
|
||||||
|
.map((res) => res.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser(user: Model.User) {
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
REPLACE INTO users (
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
discriminator,
|
||||||
|
bot
|
||||||
|
) VALUES (
|
||||||
|
:id,
|
||||||
|
:username,
|
||||||
|
:discriminator,
|
||||||
|
:bot
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
discriminator: user.discriminator,
|
||||||
|
bot: Number(user.bot),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because a User is represented in a Member object, we save the User in
|
||||||
|
* `addMember`.
|
||||||
|
*/
|
||||||
|
addMember(member: Model.GuildMember) {
|
||||||
|
this.addUser(member.user);
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
REPLACE INTO members (
|
||||||
|
user_id,
|
||||||
|
nick
|
||||||
|
) VALUES (
|
||||||
|
:user_id,
|
||||||
|
:nick
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
user_id: member.user.id,
|
||||||
|
nick: member.nick,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addChannel(channel: Model.Channel) {
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
REPLACE INTO channels (
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name
|
||||||
|
) VALUES (
|
||||||
|
:id,
|
||||||
|
:type,
|
||||||
|
:name
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
id: channel.id,
|
||||||
|
type: channel.type,
|
||||||
|
name: channel.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(message: Model.Message) {
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
REPLACE INTO messages (
|
||||||
|
id,
|
||||||
|
channel_id,
|
||||||
|
author_id,
|
||||||
|
non_user_author,
|
||||||
|
timestamp_ms,
|
||||||
|
content
|
||||||
|
) VALUES (
|
||||||
|
:id,
|
||||||
|
:channel_id,
|
||||||
|
:author_id,
|
||||||
|
:non_user_author,
|
||||||
|
:timestamp_ms,
|
||||||
|
:content
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
id: message.id,
|
||||||
|
channel_id: message.channelId,
|
||||||
|
author_id: message.authorId,
|
||||||
|
non_user_author: Number(message.nonUserAuthor),
|
||||||
|
timestamp_ms: message.timestampMs,
|
||||||
|
content: message.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addReaction(reaction: Model.Reaction) {
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
REPLACE INTO message_reactions (
|
||||||
|
channel_id,
|
||||||
|
message_id,
|
||||||
|
author_id,
|
||||||
|
emoji
|
||||||
|
) VALUES (
|
||||||
|
:channel_id,
|
||||||
|
:message_id,
|
||||||
|
:author_id,
|
||||||
|
:emoji
|
||||||
|
)`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
channel_id: reaction.channelId,
|
||||||
|
message_id: reaction.messageId,
|
||||||
|
author_id: reaction.authorId,
|
||||||
|
emoji: Model.emojiToRef(reaction.emoji),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMention(message: Model.Message, user: Model.Snowflake) {
|
||||||
|
this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
REPLACE INTO message_mentions (
|
||||||
|
channel_id,
|
||||||
|
message_id,
|
||||||
|
user_id
|
||||||
|
) VALUES (
|
||||||
|
:channel_id,
|
||||||
|
:message_id,
|
||||||
|
:user_id
|
||||||
|
)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
channel_id: message.channelId,
|
||||||
|
message_id: message.id,
|
||||||
|
user_id: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reactionEmoji(
|
||||||
|
channel: Model.Snowflake,
|
||||||
|
message: Model.Snowflake
|
||||||
|
): $ReadOnlyArray<Model.Emoji> {
|
||||||
|
return this._db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT DISTINCT
|
||||||
|
emoji
|
||||||
|
FROM message_reactions
|
||||||
|
WHERE channel_id = :channel_id
|
||||||
|
AND message_id = :message_id`
|
||||||
|
)
|
||||||
|
.all({channel_id: channel, message_id: message})
|
||||||
|
.map((e) => Model.refToEmoji(e.emoji));
|
||||||
|
}
|
||||||
|
}
|
462
src/plugins/discord/sqliteMirror.test.js
Normal file
462
src/plugins/discord/sqliteMirror.test.js
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import {SqliteMirror} from "./sqliteMirror";
|
||||||
|
import dedent from "../../util/dedent";
|
||||||
|
import {
|
||||||
|
type Channel,
|
||||||
|
type Message,
|
||||||
|
type GuildMember,
|
||||||
|
type Snowflake,
|
||||||
|
type Emoji,
|
||||||
|
type User,
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
describe("plugins/discord/sqliteMirror", () => {
|
||||||
|
const customEmoji = (): Emoji => ({id: "id", name: "name"});
|
||||||
|
const genericEmoji = (): Emoji => ({id: null, name: "🐙"});
|
||||||
|
|
||||||
|
const testChannel = (id: Snowflake): Channel => ({
|
||||||
|
id: id,
|
||||||
|
name: "testChannelName",
|
||||||
|
type: "GUILD_TEXT",
|
||||||
|
});
|
||||||
|
|
||||||
|
const testMessage = (
|
||||||
|
id: Snowflake,
|
||||||
|
channelId: Snowflake,
|
||||||
|
authorId: Snowflake
|
||||||
|
): Message => ({
|
||||||
|
id: id,
|
||||||
|
channelId: channelId,
|
||||||
|
authorId: authorId,
|
||||||
|
timestampMs: Date.parse("2020-03-03T23:35:10.615000+00:00"),
|
||||||
|
content: "Just going to drop this here",
|
||||||
|
reactionEmoji: [customEmoji()],
|
||||||
|
nonUserAuthor: false,
|
||||||
|
mentions: ["1", "23"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const testUser = (id: Snowflake): User => ({
|
||||||
|
id: id,
|
||||||
|
username: "username",
|
||||||
|
discriminator: "disc",
|
||||||
|
bot: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const testMember = (userId: Snowflake): GuildMember => ({
|
||||||
|
user: testUser(userId),
|
||||||
|
nick: "nickname",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("initializes a new database succsessfully", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
expect(() => new SqliteMirror(db, "0")).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a different config", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const _ = new SqliteMirror(db, "0");
|
||||||
|
expect(() => new SqliteMirror(db, "1")).toThrow(
|
||||||
|
"Database already populated with incompatible server or version"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the right set of tables", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
new SqliteMirror(db, "0");
|
||||||
|
const tables = db
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
||||||
|
.pluck()
|
||||||
|
.all();
|
||||||
|
expect(tables.sort()).toEqual(
|
||||||
|
[
|
||||||
|
"meta",
|
||||||
|
"channels",
|
||||||
|
"users",
|
||||||
|
"members",
|
||||||
|
"messages",
|
||||||
|
"message_reactions",
|
||||||
|
"message_mentions",
|
||||||
|
].sort()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("users", () => {
|
||||||
|
it("inserts users", async () => {
|
||||||
|
const user = testUser("1");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(user);
|
||||||
|
const result = db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
discriminator,
|
||||||
|
bot
|
||||||
|
FROM users
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
discriminator: user.discriminator,
|
||||||
|
bot: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("user", () => {
|
||||||
|
it("retrieves user", () => {
|
||||||
|
const user: User = testUser("1");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(user);
|
||||||
|
const result = sqliteMirror.user(user.id);
|
||||||
|
expect(result).toEqual(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null if user not in table", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
const result = sqliteMirror.user("1");
|
||||||
|
expect(result).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retrieves users", async () => {
|
||||||
|
const user1: User = testUser("1");
|
||||||
|
const user2: User = testUser("2");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(user1);
|
||||||
|
sqliteMirror.addUser(user2);
|
||||||
|
const result = sqliteMirror.users();
|
||||||
|
expect(result).toEqual([user1, user2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("members", () => {
|
||||||
|
it("inserts members", async () => {
|
||||||
|
const member = testMember("1");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addMember(member);
|
||||||
|
const result = db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
nick
|
||||||
|
FROM members`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
expect(result).toEqual({
|
||||||
|
user_id: member.user.id,
|
||||||
|
nick: member.nick,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inserts user", () => {
|
||||||
|
const member = testMember("1");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addMember(member);
|
||||||
|
expect(sqliteMirror.user(member.user.id)).toEqual(member.user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retrieves members", async () => {
|
||||||
|
const mem1: GuildMember = testMember("1");
|
||||||
|
const mem2: GuildMember = testMember("2");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addMember(mem1);
|
||||||
|
sqliteMirror.addMember(mem2);
|
||||||
|
const result = sqliteMirror.members();
|
||||||
|
expect(result).toEqual([mem1, mem2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("member", () => {
|
||||||
|
it("retrieves member", () => {
|
||||||
|
const member: GuildMember = testMember("1");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addMember(member);
|
||||||
|
const result = sqliteMirror.member(member.user.id);
|
||||||
|
expect(result).toEqual(member);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null if user not in table", () => {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
const result = sqliteMirror.member("1");
|
||||||
|
expect(result).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("channels", () => {
|
||||||
|
it("inserts channels", async () => {
|
||||||
|
const channel = testChannel("1");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addChannel(channel);
|
||||||
|
const result = await db
|
||||||
|
.prepare("SELECT id, type, name FROM channels")
|
||||||
|
.get();
|
||||||
|
expect(result).toEqual(channel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retrieves channels", () => {
|
||||||
|
const ch1: Channel = testChannel("1");
|
||||||
|
const ch2: Channel = testChannel("2");
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addChannel(ch1);
|
||||||
|
sqliteMirror.addChannel(ch2);
|
||||||
|
const result = sqliteMirror.channels();
|
||||||
|
expect(result).toEqual([ch1, ch2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("messages", () => {
|
||||||
|
it("inserts messages", async () => {
|
||||||
|
const messageId = "1";
|
||||||
|
const channelId = "2";
|
||||||
|
const authorId = "3";
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(authorId));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(testMessage(messageId, channelId, authorId));
|
||||||
|
const result = await db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
channel_id,
|
||||||
|
author_id,
|
||||||
|
non_user_author,
|
||||||
|
timestamp_ms,
|
||||||
|
content
|
||||||
|
FROM messages`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: "1",
|
||||||
|
channel_id: "2",
|
||||||
|
author_id: "3",
|
||||||
|
non_user_author: 0,
|
||||||
|
timestamp_ms: 1583278510615,
|
||||||
|
content: "Just going to drop this here",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retrieves messages", () => {
|
||||||
|
const messageId1 = "1";
|
||||||
|
const messageId2 = "2";
|
||||||
|
const channelId = "3";
|
||||||
|
const authorId = "4";
|
||||||
|
const mes1: Message = testMessage(messageId1, channelId, authorId);
|
||||||
|
const mes2: Message = testMessage(messageId2, channelId, authorId);
|
||||||
|
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(authorId));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(mes1);
|
||||||
|
sqliteMirror.addMessage(mes2);
|
||||||
|
|
||||||
|
for (const user of mes1.mentions) {
|
||||||
|
sqliteMirror.addUser(testUser(user));
|
||||||
|
sqliteMirror.addMention(mes1, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const user of mes2.mentions) {
|
||||||
|
sqliteMirror.addUser(testUser(user));
|
||||||
|
sqliteMirror.addMention(mes2, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const emoji of mes1.reactionEmoji) {
|
||||||
|
sqliteMirror.addReaction({
|
||||||
|
emoji,
|
||||||
|
channelId: mes1.channelId,
|
||||||
|
messageId: mes1.id,
|
||||||
|
authorId: mes1.authorId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const emoji of mes2.reactionEmoji) {
|
||||||
|
sqliteMirror.addReaction({
|
||||||
|
emoji,
|
||||||
|
channelId: mes2.channelId,
|
||||||
|
messageId: mes2.id,
|
||||||
|
authorId: mes2.authorId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = sqliteMirror.messages(channelId);
|
||||||
|
expect(result).toEqual([mes1, mes2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reactions", () => {
|
||||||
|
it("inserts reactions", async () => {
|
||||||
|
const channelId: Snowflake = "2";
|
||||||
|
const messageId: Snowflake = "3";
|
||||||
|
const authorId: Snowflake = "4";
|
||||||
|
const reaction = {
|
||||||
|
emoji: customEmoji(),
|
||||||
|
channelId: channelId,
|
||||||
|
messageId: messageId,
|
||||||
|
authorId: authorId,
|
||||||
|
};
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(authorId));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(testMessage(messageId, channelId, authorId));
|
||||||
|
sqliteMirror.addReaction(reaction);
|
||||||
|
const result = await db
|
||||||
|
.prepare(
|
||||||
|
dedent`\
|
||||||
|
SELECT
|
||||||
|
channel_id,
|
||||||
|
author_id,
|
||||||
|
message_id,
|
||||||
|
emoji
|
||||||
|
FROM message_reactions`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
expect(result).toEqual({
|
||||||
|
emoji: "name:id",
|
||||||
|
channel_id: channelId,
|
||||||
|
message_id: messageId,
|
||||||
|
author_id: authorId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("retrieves reactions", () => {
|
||||||
|
const channelId: Snowflake = "1";
|
||||||
|
const messageId: Snowflake = "2";
|
||||||
|
const authorId1 = "3";
|
||||||
|
const authorId2 = "4";
|
||||||
|
|
||||||
|
const reaction1 = {
|
||||||
|
emoji: customEmoji(),
|
||||||
|
channelId: channelId,
|
||||||
|
messageId: messageId,
|
||||||
|
authorId: authorId1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reaction2 = {
|
||||||
|
emoji: genericEmoji(),
|
||||||
|
channelId: channelId,
|
||||||
|
messageId: messageId,
|
||||||
|
authorId: authorId2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(authorId1));
|
||||||
|
sqliteMirror.addUser(testUser(authorId2));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(testMessage(messageId, channelId, authorId1));
|
||||||
|
sqliteMirror.addReaction(reaction1);
|
||||||
|
sqliteMirror.addReaction(reaction2);
|
||||||
|
const result = sqliteMirror.reactions(channelId, messageId);
|
||||||
|
expect(result).toEqual([reaction1, reaction2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mentions", () => {
|
||||||
|
it("inserts mentions", async () => {
|
||||||
|
const messageId: Snowflake = "1";
|
||||||
|
const channelId: Snowflake = "2";
|
||||||
|
const authorId: Snowflake = "3";
|
||||||
|
const message: Message = testMessage(messageId, channelId, authorId);
|
||||||
|
const userId: Snowflake = "45";
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(userId));
|
||||||
|
sqliteMirror.addUser(testUser(authorId));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(testMessage(messageId, channelId, authorId));
|
||||||
|
sqliteMirror.addMention(message, userId);
|
||||||
|
const result = await db
|
||||||
|
.prepare(
|
||||||
|
`\
|
||||||
|
SELECT
|
||||||
|
channel_id,
|
||||||
|
message_id,
|
||||||
|
user_id
|
||||||
|
FROM message_mentions`
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
expect(result).toEqual({
|
||||||
|
channel_id: channelId,
|
||||||
|
message_id: messageId,
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retrieves mentions", () => {
|
||||||
|
const messageId: Snowflake = "1";
|
||||||
|
const channelId = "2";
|
||||||
|
const authorId = "3";
|
||||||
|
const message: Message = testMessage(messageId, channelId, authorId);
|
||||||
|
const [userId1, userId2] = message.mentions;
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(userId1));
|
||||||
|
sqliteMirror.addUser(testUser(userId2));
|
||||||
|
sqliteMirror.addUser(testUser(authorId));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(testMessage(messageId, channelId, authorId));
|
||||||
|
sqliteMirror.addMention(message, userId1);
|
||||||
|
sqliteMirror.addMention(message, userId2);
|
||||||
|
const result = sqliteMirror.mentions(message.channelId, messageId);
|
||||||
|
expect(result).toEqual([userId1, userId2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reactionEmoji", () => {
|
||||||
|
it("retrieves emojis", () => {
|
||||||
|
const channelId: Snowflake = "1";
|
||||||
|
const messageId: Snowflake = "2";
|
||||||
|
const authorId1: Snowflake = "3";
|
||||||
|
const authorId2: Snowflake = "4";
|
||||||
|
const messageAuthorId: Snowflake = "5";
|
||||||
|
|
||||||
|
const reaction1 = {
|
||||||
|
emoji: customEmoji(),
|
||||||
|
channelId: channelId,
|
||||||
|
messageId: messageId,
|
||||||
|
authorId: authorId1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reaction2 = {
|
||||||
|
emoji: genericEmoji(),
|
||||||
|
channelId: channelId,
|
||||||
|
messageId: messageId,
|
||||||
|
authorId: authorId2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
|
sqliteMirror.addUser(testUser(authorId1));
|
||||||
|
sqliteMirror.addUser(testUser(authorId2));
|
||||||
|
sqliteMirror.addChannel(testChannel(channelId));
|
||||||
|
sqliteMirror.addMessage(
|
||||||
|
testMessage(messageId, channelId, messageAuthorId)
|
||||||
|
);
|
||||||
|
sqliteMirror.addReaction(reaction1);
|
||||||
|
sqliteMirror.addReaction(reaction2);
|
||||||
|
const result = sqliteMirror.reactionEmoji(channelId, messageId);
|
||||||
|
expect(result).toEqual([customEmoji(), genericEmoji()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user