mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-24 10:18:11 +00:00
Discord: add Mirror class (#1780)
Creates a class to handle the implementation of fetching data from the Discord server and saving it to the local mirror. Test plan: Unit tests added. paired with @beanow
This commit is contained in:
parent
ea175a390a
commit
7797c120e6
@ -25,6 +25,30 @@ export type ResultPage<T> = {|
|
|||||||
+results: $ReadOnlyArray<T>,
|
+results: $ReadOnlyArray<T>,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to fetch Discord data
|
||||||
|
*/
|
||||||
|
export interface DiscordFetcher {
|
||||||
|
channels(guildId: Snowflake): Promise<$ReadOnlyArray<Model.Channel>>;
|
||||||
|
|
||||||
|
members(
|
||||||
|
guildId: Snowflake,
|
||||||
|
after: Snowflake
|
||||||
|
): Promise<ResultPage<Model.GuildMember>>;
|
||||||
|
|
||||||
|
messages(
|
||||||
|
channel: Snowflake,
|
||||||
|
after: Snowflake
|
||||||
|
): Promise<ResultPage<Model.Message>>;
|
||||||
|
|
||||||
|
reactions(
|
||||||
|
channel: Snowflake,
|
||||||
|
message: Snowflake,
|
||||||
|
emoji: Model.Emoji,
|
||||||
|
after: Snowflake
|
||||||
|
): Promise<ResultPage<Model.Reaction>>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetcher is responsible for:
|
* Fetcher is responsible for:
|
||||||
* - Returning the correct endpoint to fetch against for Guilds, Channels,
|
* - Returning the correct endpoint to fetch against for Guilds, Channels,
|
||||||
@ -43,7 +67,7 @@ export type ResultPage<T> = {|
|
|||||||
* returning an array of Channel objects in the corresponding method.
|
* returning an array of Channel objects in the corresponding method.
|
||||||
* See: https://discordapp.com/developers/docs/resources/guild#get-guild-channels
|
* See: https://discordapp.com/developers/docs/resources/guild#get-guild-channels
|
||||||
*/
|
*/
|
||||||
export class Fetcher {
|
export class Fetcher implements DiscordFetcher {
|
||||||
+_fetch: FetchEndpoint;
|
+_fetch: FetchEndpoint;
|
||||||
+_options: FetchOptions;
|
+_options: FetchOptions;
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@ import {
|
|||||||
type Reaction,
|
type Reaction,
|
||||||
type Message,
|
type Message,
|
||||||
type GuildMember,
|
type GuildMember,
|
||||||
type Snowflake,
|
|
||||||
type Emoji,
|
type Emoji,
|
||||||
} from "./models";
|
} from "./models";
|
||||||
|
import {testChannel} from "./testUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: the 'any' type signature is assigned to server response objects
|
* Note: the 'any' type signature is assigned to server response objects
|
||||||
@ -50,20 +50,14 @@ describe("plugins/discord/fetcher", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles response", async () => {
|
it("handles response", async () => {
|
||||||
const testChannel = (id: string): any => ({
|
const testChannelResp = (id: string): any => ({
|
||||||
id: id,
|
id: id,
|
||||||
name: "testChannelName",
|
name: "testChannelName",
|
||||||
type: 0,
|
type: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const testChannelObj = (id: Snowflake): Channel => ({
|
const response: any[] = [testChannelResp("1")];
|
||||||
id: id,
|
const expected: Channel[] = [testChannel("1")];
|
||||||
name: "testChannelName",
|
|
||||||
type: "GUILD_TEXT",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: any[] = [testChannel("1")];
|
|
||||||
const expected: Channel[] = [testChannelObj("1")];
|
|
||||||
const fetch = jest.fn(() => Promise.resolve(response));
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
const fetcher = new Fetcher(fetch, defaultOptions());
|
const fetcher = new Fetcher(fetch, defaultOptions());
|
||||||
const data = await fetcher.channels("0");
|
const data = await fetcher.channels("0");
|
||||||
|
78
src/plugins/discord/mirror.js
Normal file
78
src/plugins/discord/mirror.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {type ResultPage} from "./fetcher";
|
||||||
|
import {type SqliteMirror} from "./sqliteMirror";
|
||||||
|
import {type Snowflake} from "./models";
|
||||||
|
import * as Model from "./models";
|
||||||
|
import * as nullUtil from "../../util/null";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors data from the Discord API into a local sqlite db.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function fetchDiscord(
|
||||||
|
sqliteMirror: SqliteMirror,
|
||||||
|
streams: DepaginatedFetcher
|
||||||
|
) {
|
||||||
|
for (const member of await streams.members()) {
|
||||||
|
sqliteMirror.addMember(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const channel of await streams.channels()) {
|
||||||
|
sqliteMirror.addChannel(channel);
|
||||||
|
|
||||||
|
for (const message of await streams.messages(channel.id)) {
|
||||||
|
sqliteMirror.addMessage(message);
|
||||||
|
|
||||||
|
for (const emoji of message.reactionEmoji) {
|
||||||
|
for (const reaction of await streams.reactions(
|
||||||
|
channel.id,
|
||||||
|
message.id,
|
||||||
|
emoji
|
||||||
|
)) {
|
||||||
|
sqliteMirror.addReaction(reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: most of this is about wrapping pagination.
|
||||||
|
export interface DepaginatedFetcher {
|
||||||
|
members(after: ?Snowflake): Promise<$ReadOnlyArray<Model.GuildMember>>;
|
||||||
|
channels(): Promise<$ReadOnlyArray<Model.Channel>>;
|
||||||
|
messages(
|
||||||
|
channel: Snowflake,
|
||||||
|
after: ?Snowflake
|
||||||
|
): Promise<$ReadOnlyArray<Model.Message>>;
|
||||||
|
reactions(
|
||||||
|
channel: Snowflake,
|
||||||
|
message: Snowflake,
|
||||||
|
emoji: Model.Emoji,
|
||||||
|
after: ?Snowflake
|
||||||
|
): Promise<$ReadOnlyArray<Model.Reaction>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPages<T>(
|
||||||
|
fetchPage: (endCursor: Snowflake) => Promise<ResultPage<T>>,
|
||||||
|
endCursor: Snowflake
|
||||||
|
): Promise<$ReadOnlyArray<T>> {
|
||||||
|
let after = endCursor;
|
||||||
|
let hasNextPage = true;
|
||||||
|
let allResults = [];
|
||||||
|
|
||||||
|
while (hasNextPage) {
|
||||||
|
const {results, pageInfo} = await fetchPage(after);
|
||||||
|
allResults = allResults.concat(results);
|
||||||
|
|
||||||
|
hasNextPage = pageInfo.hasNextPage;
|
||||||
|
if (hasNextPage) {
|
||||||
|
after = nullUtil.get(
|
||||||
|
pageInfo.endCursor,
|
||||||
|
`Found null endCursor with hasNextPage = true`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allResults;
|
||||||
|
}
|
137
src/plugins/discord/mirror.test.js
Normal file
137
src/plugins/discord/mirror.test.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import {fetchDiscord, DepaginatedFetcher, getPages} from "./mirror";
|
||||||
|
import {SqliteMirror} from "./sqliteMirror";
|
||||||
|
import * as Model from "./models";
|
||||||
|
import {type Snowflake} from "./models";
|
||||||
|
import {
|
||||||
|
testChannel,
|
||||||
|
testMember,
|
||||||
|
testMessage,
|
||||||
|
customEmoji,
|
||||||
|
testReaction,
|
||||||
|
} from "./testUtils";
|
||||||
|
|
||||||
|
describe("plugins/discord/mirror", () => {
|
||||||
|
describe("updateMirror", () => {
|
||||||
|
const activeChannelId = "12";
|
||||||
|
const activeMessageid = "34";
|
||||||
|
|
||||||
|
function testMirror() {
|
||||||
|
const db = new Database(":memory:");
|
||||||
|
return new SqliteMirror(db, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMembers() {
|
||||||
|
return [testMember("1"), testMember("2"), testMember("3")];
|
||||||
|
}
|
||||||
|
|
||||||
|
function testChannels() {
|
||||||
|
return [testChannel(activeChannelId), testChannel("2"), testChannel("3")];
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMessages(channelId) {
|
||||||
|
if (channelId === activeChannelId) {
|
||||||
|
const authorId = "1";
|
||||||
|
const reactionEmoji = [customEmoji()];
|
||||||
|
return [
|
||||||
|
testMessage(activeMessageid, channelId, authorId, reactionEmoji),
|
||||||
|
testMessage("2", channelId, authorId, []),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testReactions(channelId, messageId) {
|
||||||
|
if (channelId === activeChannelId && messageId === activeMessageid) {
|
||||||
|
return [
|
||||||
|
testReaction(channelId, messageId, "1"),
|
||||||
|
testReaction(channelId, messageId, "2"),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockStream(): DepaginatedFetcher {
|
||||||
|
return {
|
||||||
|
members(): Promise<$ReadOnlyArray<Model.GuildMember>> {
|
||||||
|
return Promise.resolve(testMembers());
|
||||||
|
},
|
||||||
|
channels(): Promise<$ReadOnlyArray<Model.Channel>> {
|
||||||
|
return Promise.resolve(testChannels());
|
||||||
|
},
|
||||||
|
messages(channel: Snowflake): Promise<$ReadOnlyArray<Model.Message>> {
|
||||||
|
return Promise.resolve(testMessages(channel));
|
||||||
|
},
|
||||||
|
reactions(
|
||||||
|
channel: Snowflake,
|
||||||
|
message: Snowflake
|
||||||
|
): Promise<$ReadOnlyArray<Model.Reaction>> {
|
||||||
|
return Promise.resolve(testReactions(channel, message));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTestData() {
|
||||||
|
const mirror = testMirror();
|
||||||
|
const stream = mockStream();
|
||||||
|
return {mirror, stream};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fetches members", async () => {
|
||||||
|
const {mirror, stream} = setupTestData();
|
||||||
|
await fetchDiscord(mirror, stream);
|
||||||
|
expect(mirror.members()).toEqual(testMembers());
|
||||||
|
});
|
||||||
|
it("fetches channels", async () => {
|
||||||
|
const {mirror, stream} = setupTestData();
|
||||||
|
await fetchDiscord(mirror, stream);
|
||||||
|
expect(mirror.channels()).toEqual(testChannels());
|
||||||
|
});
|
||||||
|
it("fetches messages", async () => {
|
||||||
|
const {mirror, stream} = setupTestData();
|
||||||
|
await fetchDiscord(mirror, stream);
|
||||||
|
|
||||||
|
expect(mirror.messages(activeMessageid)).toEqual(testMessages("1"));
|
||||||
|
});
|
||||||
|
it("fetches reactions", async () => {
|
||||||
|
const {mirror, stream} = setupTestData();
|
||||||
|
await fetchDiscord(mirror, stream);
|
||||||
|
expect(mirror.reactions(activeMessageid, activeChannelId)).toEqual(
|
||||||
|
testReactions("1", "1")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get pages", () => {
|
||||||
|
it("handles pagination correctly", async () => {
|
||||||
|
const fetch = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
pageInfo: {hasNextPage: true, endCursor: "1"},
|
||||||
|
results: [0, 1],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
pageInfo: {hasNextPage: true, endCursor: "3"},
|
||||||
|
results: [2, 3],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
pageInfo: {hasNextPage: false, endCursor: "4"},
|
||||||
|
results: [4],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await getPages(fetch, "0");
|
||||||
|
expect(results).toEqual([0, 1, 2, 3, 4]);
|
||||||
|
expect(fetch.mock.calls).toEqual([["0"], ["1"], ["3"]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -8,47 +8,18 @@ import {
|
|||||||
type Message,
|
type Message,
|
||||||
type GuildMember,
|
type GuildMember,
|
||||||
type Snowflake,
|
type Snowflake,
|
||||||
type Emoji,
|
|
||||||
type User,
|
type User,
|
||||||
} from "./models";
|
} from "./models";
|
||||||
|
import {
|
||||||
|
testChannel,
|
||||||
|
testUser,
|
||||||
|
testMember,
|
||||||
|
testMessage,
|
||||||
|
customEmoji,
|
||||||
|
genericEmoji,
|
||||||
|
} from "./testUtils";
|
||||||
|
|
||||||
describe("plugins/discord/sqliteMirror", () => {
|
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", () => {
|
describe("constructor", () => {
|
||||||
it("initializes a new database succsessfully", () => {
|
it("initializes a new database succsessfully", () => {
|
||||||
const db = new Database(":memory:");
|
const db = new Database(":memory:");
|
||||||
@ -261,8 +232,12 @@ describe("plugins/discord/sqliteMirror", () => {
|
|||||||
const messageId2 = "2";
|
const messageId2 = "2";
|
||||||
const channelId = "3";
|
const channelId = "3";
|
||||||
const authorId = "4";
|
const authorId = "4";
|
||||||
const mes1: Message = testMessage(messageId1, channelId, authorId);
|
const mes1: Message = testMessage(messageId1, channelId, authorId, [
|
||||||
const mes2: Message = testMessage(messageId2, channelId, authorId);
|
customEmoji(),
|
||||||
|
]);
|
||||||
|
const mes2: Message = testMessage(messageId2, channelId, authorId, [
|
||||||
|
customEmoji(),
|
||||||
|
]);
|
||||||
|
|
||||||
const db = new Database(":memory:");
|
const db = new Database(":memory:");
|
||||||
const sqliteMirror = new SqliteMirror(db, "0");
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
@ -271,16 +246,6 @@ describe("plugins/discord/sqliteMirror", () => {
|
|||||||
sqliteMirror.addMessage(mes1);
|
sqliteMirror.addMessage(mes1);
|
||||||
sqliteMirror.addMessage(mes2);
|
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) {
|
for (const emoji of mes1.reactionEmoji) {
|
||||||
sqliteMirror.addReaction({
|
sqliteMirror.addReaction({
|
||||||
emoji,
|
emoji,
|
||||||
@ -407,8 +372,11 @@ describe("plugins/discord/sqliteMirror", () => {
|
|||||||
const messageId: Snowflake = "1";
|
const messageId: Snowflake = "1";
|
||||||
const channelId = "2";
|
const channelId = "2";
|
||||||
const authorId = "3";
|
const authorId = "3";
|
||||||
const message: Message = testMessage(messageId, channelId, authorId);
|
const userId1 = "4";
|
||||||
const [userId1, userId2] = message.mentions;
|
const userId2 = "5";
|
||||||
|
const message: Message = testMessage(messageId, channelId, authorId, [
|
||||||
|
customEmoji(),
|
||||||
|
]);
|
||||||
const db = new Database(":memory:");
|
const db = new Database(":memory:");
|
||||||
const sqliteMirror = new SqliteMirror(db, "0");
|
const sqliteMirror = new SqliteMirror(db, "0");
|
||||||
sqliteMirror.addUser(testUser(userId1));
|
sqliteMirror.addUser(testUser(userId1));
|
||||||
|
62
src/plugins/discord/testUtils.js
Normal file
62
src/plugins/discord/testUtils.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
type Channel,
|
||||||
|
type Message,
|
||||||
|
type GuildMember,
|
||||||
|
type Snowflake,
|
||||||
|
type Emoji,
|
||||||
|
type User,
|
||||||
|
type Reaction,
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
export const customEmoji = (): Emoji => ({id: "id", name: "name"});
|
||||||
|
export const genericEmoji = (): Emoji => ({id: null, name: "🐙"});
|
||||||
|
|
||||||
|
export const testChannel = (id: Snowflake): Channel => ({
|
||||||
|
id: id,
|
||||||
|
name: "testChannelName",
|
||||||
|
type: "GUILD_TEXT",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const testMessage = (
|
||||||
|
id: Snowflake,
|
||||||
|
channelId: Snowflake,
|
||||||
|
authorId: Snowflake,
|
||||||
|
reactionEmoji: ?$ReadOnlyArray<Emoji>,
|
||||||
|
mentions: ?$ReadOnlyArray<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: reactionEmoji || [],
|
||||||
|
nonUserAuthor: false,
|
||||||
|
mentions: mentions || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const testUser = (id: Snowflake): User => ({
|
||||||
|
id: id,
|
||||||
|
username: "username",
|
||||||
|
discriminator: "disc",
|
||||||
|
bot: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const testMember = (userId: Snowflake): GuildMember => ({
|
||||||
|
user: testUser(userId),
|
||||||
|
nick: "nickname",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const testReaction = (
|
||||||
|
channelId: Snowflake,
|
||||||
|
messageId: Snowflake,
|
||||||
|
authorId: Snowflake
|
||||||
|
): Reaction => {
|
||||||
|
return {
|
||||||
|
channelId: channelId,
|
||||||
|
messageId: messageId,
|
||||||
|
authorId: authorId,
|
||||||
|
emoji: customEmoji(),
|
||||||
|
};
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user