mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-27 11:40:26 +00:00
Add Discord Fetch class and Discord types (#1703)
Adds a Fetcher class that takes fetch options and fetches against the correct endpoints, returning re-formatted request data and pagination info. Adds a models.js file that defines Discord types. Test Plan: Unit tests added for fetcher class and helper functions in models.js file. Paired with @Beanow
This commit is contained in:
parent
eda3d19772
commit
eb205e8680
141
src/plugins/discord/fetcher.js
Normal file
141
src/plugins/discord/fetcher.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import * as Model from "./models";
|
||||||
|
import {type Snowflake} from "./models";
|
||||||
|
|
||||||
|
type FetchEndpoint = (endpoint: string) => Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the Guild ID to fetch against, and the 'limit'
|
||||||
|
* parameter when fetching GuildMembers, Messages, and Reactions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
type FetchOptions = {|
|
||||||
|
membersLimit: number,
|
||||||
|
messagesLimit: number,
|
||||||
|
reactionsLimit: number,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type PageInfo = {|
|
||||||
|
+hasNextPage: boolean,
|
||||||
|
+endCursor: Snowflake | null,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type ResultPage<T> = {|
|
||||||
|
+pageInfo: PageInfo,
|
||||||
|
+results: $ReadOnlyArray<T>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetcher is responsible for:
|
||||||
|
* - Returning the correct endpoint to fetch against for Guilds, Channels,
|
||||||
|
* Members, and Reactions.
|
||||||
|
* - Formatting the returned results into the correct Typed objects
|
||||||
|
* - Returning pagination info in a PageInfo object, containing hasNextPage
|
||||||
|
* and endCursor properties.
|
||||||
|
* The endCursor property is calculated as the Id of the last object
|
||||||
|
* returned in the response results. We are assuming Discord provides
|
||||||
|
* consistent, ordered results.
|
||||||
|
* The hasNextPage property is a boolean calculated as whether the number of
|
||||||
|
* results recieved is equal to the `limit` property provided in the
|
||||||
|
* fetch request.
|
||||||
|
*
|
||||||
|
* Note that Discord doesn't support pagination for Channels, so we're
|
||||||
|
* returning an array of Channel objects in the corresponding method.
|
||||||
|
* See: https://discordapp.com/developers/docs/resources/guild#get-guild-channels
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class Fetcher {
|
||||||
|
+_fetch: FetchEndpoint;
|
||||||
|
+_options: FetchOptions;
|
||||||
|
|
||||||
|
constructor(fetchEndpoint: FetchEndpoint, options: FetchOptions) {
|
||||||
|
this._fetch = fetchEndpoint;
|
||||||
|
this._options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async guild(guildId: Snowflake): Promise<Model.Guild> {
|
||||||
|
const {id, name, permissions} = await this._fetch(`guilds/${guildId}`);
|
||||||
|
return {id: id, name: name, permissions: permissions};
|
||||||
|
}
|
||||||
|
|
||||||
|
async channels(guildId: Snowflake): Promise<$ReadOnlyArray<Model.Channel>> {
|
||||||
|
const response = await this._fetch(`/guilds/${guildId}/channels`);
|
||||||
|
return response.map((x) => ({
|
||||||
|
id: x.id,
|
||||||
|
name: x.name,
|
||||||
|
type: Model.channelTypeFromId(x.type),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async members(
|
||||||
|
guildId: Snowflake,
|
||||||
|
after: Snowflake
|
||||||
|
): Promise<ResultPage<Model.GuildMember>> {
|
||||||
|
const {membersLimit} = this._options;
|
||||||
|
const endpoint = `/guilds/${guildId}/members?after=${after}&limit=${membersLimit}`;
|
||||||
|
const response = await this._fetch(endpoint);
|
||||||
|
const results = response.map((x) => ({
|
||||||
|
user: {
|
||||||
|
id: x.user.id,
|
||||||
|
username: x.user.username,
|
||||||
|
discriminator: x.user.discriminator,
|
||||||
|
bot: x.user.bot || x.user.system || false,
|
||||||
|
},
|
||||||
|
nick: x.nick || null,
|
||||||
|
roles: x.roles,
|
||||||
|
}));
|
||||||
|
const hasNextPage = results.length === membersLimit;
|
||||||
|
const endCursor =
|
||||||
|
response.length > 0 ? response[response.length - 1].user.id : null;
|
||||||
|
const pageInfo = {hasNextPage, endCursor};
|
||||||
|
return {results, pageInfo};
|
||||||
|
}
|
||||||
|
|
||||||
|
async messages(
|
||||||
|
channel: Snowflake,
|
||||||
|
after: Snowflake
|
||||||
|
): Promise<ResultPage<Model.Message>> {
|
||||||
|
const {messagesLimit} = this._options;
|
||||||
|
const endpoint = `/channels/${channel}/messages?after=${after}&limit=${messagesLimit}`;
|
||||||
|
const response = await this._fetch(endpoint);
|
||||||
|
const results = response.map((x) => ({
|
||||||
|
id: x.id,
|
||||||
|
channelId: channel,
|
||||||
|
authorId: x.author.id,
|
||||||
|
timestampMs: Date.parse(x.timestamp),
|
||||||
|
content: x.content,
|
||||||
|
reactionEmoji: (x.reactions || []).map((r) => r.emoji),
|
||||||
|
nonUserAuthor: Model.isAuthoredByNonUser(x),
|
||||||
|
mentions: (x.mentions || []).map((user) => user.id),
|
||||||
|
}));
|
||||||
|
const hasNextPage = results.length === messagesLimit;
|
||||||
|
const endCursor =
|
||||||
|
response.length > 0 ? response[response.length - 1].id : null;
|
||||||
|
const pageInfo = {hasNextPage, endCursor};
|
||||||
|
return {results, pageInfo};
|
||||||
|
}
|
||||||
|
|
||||||
|
async reactions(
|
||||||
|
channel: Snowflake,
|
||||||
|
message: Snowflake,
|
||||||
|
emoji: Model.Emoji,
|
||||||
|
after: Snowflake
|
||||||
|
): Promise<ResultPage<Model.Reaction>> {
|
||||||
|
const {reactionsLimit} = this._options;
|
||||||
|
const emojiRef = Model.emojiToRef(emoji);
|
||||||
|
const endpoint = `/channels/${channel}/messages/${message}/reactions/${emojiRef}?after=${after}&limit=${reactionsLimit}`;
|
||||||
|
const response = await this._fetch(endpoint);
|
||||||
|
const results = response.map((x) => ({
|
||||||
|
emoji: x.emoji,
|
||||||
|
channelId: channel,
|
||||||
|
messageId: message,
|
||||||
|
authorId: x.id,
|
||||||
|
}));
|
||||||
|
const hasNextPage = results.length === reactionsLimit;
|
||||||
|
const endCursor =
|
||||||
|
response.length > 0 ? response[response.length - 1].id : null;
|
||||||
|
const pageInfo = {hasNextPage, endCursor};
|
||||||
|
return {results, pageInfo};
|
||||||
|
}
|
||||||
|
}
|
299
src/plugins/discord/fetcher.test.js
Normal file
299
src/plugins/discord/fetcher.test.js
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {Fetcher} from "./fetcher";
|
||||||
|
import {
|
||||||
|
type Channel,
|
||||||
|
type Guild,
|
||||||
|
type Reaction,
|
||||||
|
type Message,
|
||||||
|
type GuildMember,
|
||||||
|
type Snowflake,
|
||||||
|
type Emoji,
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: the 'any' type signature is assigned to server response objects
|
||||||
|
* to make explicit that they are untyped prior to transformation by the
|
||||||
|
* appropriate handlers
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("plugins/discord/fetcher", () => {
|
||||||
|
const defaultOptions = () => ({
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetch guild", () => {
|
||||||
|
it("passes correct endpoint", async () => {
|
||||||
|
const fetch = jest.fn(() => Promise.resolve([]));
|
||||||
|
const fetcher = new Fetcher(fetch, defaultOptions());
|
||||||
|
await fetcher.guild("1");
|
||||||
|
expect(fetch.mock.calls[0]).toEqual(["guilds/1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles response", async () => {
|
||||||
|
const expected: Guild = {id: "1", name: "guildname", permissions: 0};
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(expected));
|
||||||
|
const fetcher = new Fetcher(fetch, defaultOptions());
|
||||||
|
const guild = await fetcher.guild("1");
|
||||||
|
expect(guild).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetch channels", () => {
|
||||||
|
it("passes correct endpoint", async () => {
|
||||||
|
const fetch = jest.fn(() => Promise.resolve([]));
|
||||||
|
const fetcher = new Fetcher(fetch, defaultOptions());
|
||||||
|
await fetcher.channels("1");
|
||||||
|
expect(fetch.mock.calls[0]).toEqual(["/guilds/1/channels"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles response", async () => {
|
||||||
|
const testChannel = (id: string): any => ({
|
||||||
|
id: id,
|
||||||
|
name: "testChannelName",
|
||||||
|
type: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const testChannelObj = (id: Snowflake): Channel => ({
|
||||||
|
id: id,
|
||||||
|
name: "testChannelName",
|
||||||
|
type: "GUILD_TEXT",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: any[] = [testChannel("1")];
|
||||||
|
const expected: Channel[] = [testChannelObj("1")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, defaultOptions());
|
||||||
|
const data = await fetcher.channels("0");
|
||||||
|
expect(data).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetch members", () => {
|
||||||
|
const testMember = (userId: string): any => ({
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
username: "username",
|
||||||
|
discriminator: "disc",
|
||||||
|
bot: true,
|
||||||
|
},
|
||||||
|
nick: "nickname",
|
||||||
|
roles: ["test role"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = () => ({
|
||||||
|
membersLimit: 2,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes correct endpoint", async () => {
|
||||||
|
const fetch = jest.fn(() => Promise.resolve([]));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
await fetcher.members("1", "0");
|
||||||
|
expect(fetch.mock.calls[0]).toEqual([
|
||||||
|
"/guilds/1/members?after=0&limit=2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles response", async () => {
|
||||||
|
const response: GuildMember[] = [testMember("1")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
const {results} = await fetcher.members("1", "0");
|
||||||
|
expect(results).toEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct endCursor", async () => {
|
||||||
|
const response: any[] = [testMember("1"), testMember("2")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
const {endCursor} = (await fetcher.members("1", "0")).pageInfo;
|
||||||
|
expect(endCursor).toEqual("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("next page", () => {
|
||||||
|
it("next page = true", async () => {
|
||||||
|
const response: any[] = [testMember("1")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, {
|
||||||
|
membersLimit: 1,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 100,
|
||||||
|
});
|
||||||
|
const {hasNextPage} = (await fetcher.members("1", "0")).pageInfo;
|
||||||
|
expect(hasNextPage).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next page = false", async () => {
|
||||||
|
const response: any[] = [testMember("1")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, {
|
||||||
|
membersLimit: 2,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 100,
|
||||||
|
});
|
||||||
|
const {hasNextPage} = (await fetcher.members("1", "0")).pageInfo;
|
||||||
|
expect(hasNextPage).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetch reactions", () => {
|
||||||
|
const emoji: Emoji = {id: "1", name: "emojiname"};
|
||||||
|
|
||||||
|
const options = () => ({
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes correct endpoint", async () => {
|
||||||
|
const fetch = jest.fn(() => Promise.resolve([]));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
await fetcher.reactions("1", "2", emoji, "0");
|
||||||
|
expect(fetch.mock.calls[0]).toEqual([
|
||||||
|
`/channels/1/messages/2/reactions/emojiname:1?after=0&limit=2`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles response", async () => {
|
||||||
|
// response object from server, prior to type transformation
|
||||||
|
const response: any[] = [{id: "123", emoji}];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
const {results} = await fetcher.reactions("3", "2", emoji, "0");
|
||||||
|
const expected: Reaction[] = [
|
||||||
|
{emoji, channelId: "3", messageId: "2", authorId: "123"},
|
||||||
|
];
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct endCursor", async () => {
|
||||||
|
const response: any[] = [
|
||||||
|
{id: "1", emoji},
|
||||||
|
{id: "2", emoji},
|
||||||
|
];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
const {endCursor} = (
|
||||||
|
await fetcher.reactions("1", "2", emoji, "0")
|
||||||
|
).pageInfo;
|
||||||
|
expect(endCursor).toBe("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("next page", () => {
|
||||||
|
it("next page = true", async () => {
|
||||||
|
const response: any[] = [{id: 1, emoji}];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, {
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 1,
|
||||||
|
});
|
||||||
|
const {hasNextPage} = (
|
||||||
|
await fetcher.reactions("1", "2", emoji, "0")
|
||||||
|
).pageInfo;
|
||||||
|
expect(hasNextPage).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next page = false", async () => {
|
||||||
|
const response: any[] = [{id: 1, emoji}];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, {
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 100,
|
||||||
|
reactionsLimit: 2,
|
||||||
|
});
|
||||||
|
const {hasNextPage} = (
|
||||||
|
await fetcher.reactions("1", "2", emoji, "0")
|
||||||
|
).pageInfo;
|
||||||
|
expect(hasNextPage).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetch messages", () => {
|
||||||
|
const testMessage = (id: string): any => ({
|
||||||
|
id: id,
|
||||||
|
author: {id: "2"},
|
||||||
|
timestamp: "2020-03-03T23:35:10.615000+00:00",
|
||||||
|
content: "Just going to drop this here",
|
||||||
|
reactions: [{emoji: {id: "1", name: "testemoji"}}],
|
||||||
|
mentions: [{id: "4", username: "testuser"}],
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = () => ({
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 2,
|
||||||
|
reactionsLimit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes correct endpoint", async () => {
|
||||||
|
const fetch = jest.fn(() => Promise.resolve([]));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
await fetcher.messages("1", "0");
|
||||||
|
expect(fetch.mock.calls[0]).toEqual([
|
||||||
|
"/channels/1/messages?after=0&limit=2",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles response", async () => {
|
||||||
|
const response: any[] = [testMessage("1")];
|
||||||
|
|
||||||
|
const expected: Message[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
channelId: "123",
|
||||||
|
authorId: "2",
|
||||||
|
timestampMs: Date.parse("2020-03-03T23:35:10.615000+00:00"),
|
||||||
|
content: "Just going to drop this here",
|
||||||
|
reactionEmoji: [{id: "1", name: "testemoji"}],
|
||||||
|
nonUserAuthor: false,
|
||||||
|
mentions: ["4"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
const {results} = await fetcher.messages("123", "0");
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct endCursor", async () => {
|
||||||
|
const response: any[] = [testMessage("1"), testMessage("2")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, options());
|
||||||
|
const {endCursor} = (await fetcher.messages("123", "0")).pageInfo;
|
||||||
|
expect(endCursor).toBe("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("next page", () => {
|
||||||
|
it("next page = true", async () => {
|
||||||
|
const response: any[] = [testMessage("1")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, {
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 1,
|
||||||
|
reactionsLimit: 2,
|
||||||
|
});
|
||||||
|
const {hasNextPage} = (await fetcher.messages("123", "0")).pageInfo;
|
||||||
|
expect(hasNextPage).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next page = false", async () => {
|
||||||
|
const response: any[] = [testMessage("1")];
|
||||||
|
const fetch = jest.fn(() => Promise.resolve(response));
|
||||||
|
const fetcher = new Fetcher(fetch, {
|
||||||
|
membersLimit: 100,
|
||||||
|
messagesLimit: 2,
|
||||||
|
reactionsLimit: 2,
|
||||||
|
});
|
||||||
|
const {hasNextPage} = (await fetcher.messages("123", "0")).pageInfo;
|
||||||
|
expect(hasNextPage).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
134
src/plugins/discord/models.js
Normal file
134
src/plugins/discord/models.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/reference#snowflakes
|
||||||
|
export type Snowflake = string;
|
||||||
|
export const ZeroSnowflake: Snowflake = "0";
|
||||||
|
|
||||||
|
export type BotToken = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord Channels can be of various types, defined below.
|
||||||
|
* See: https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ChannelType =
|
||||||
|
| "GUILD_TEXT"
|
||||||
|
| "DM"
|
||||||
|
| "GUILD_VOICE"
|
||||||
|
| "GROUP_DM"
|
||||||
|
| "GUILD_CATEGORY"
|
||||||
|
| "GUILD_NEWS"
|
||||||
|
| "GUILD_STORE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Discord server returns an Id field of type number to represent
|
||||||
|
* the type of Channel. Here we convert that to a text representation,
|
||||||
|
* also based on: https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function channelTypeFromId(id: number): ChannelType {
|
||||||
|
switch (id) {
|
||||||
|
case 0:
|
||||||
|
return "GUILD_TEXT";
|
||||||
|
case 1:
|
||||||
|
return "DM";
|
||||||
|
case 2:
|
||||||
|
return "GUILD_VOICE";
|
||||||
|
case 3:
|
||||||
|
return "GROUP_DM";
|
||||||
|
case 4:
|
||||||
|
return "GUILD_CATEGORY";
|
||||||
|
case 5:
|
||||||
|
return "GUILD_NEWS";
|
||||||
|
case 6:
|
||||||
|
return "GUILD_STORE";
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown channel type ID: ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/resources/guild#guild-object
|
||||||
|
export type Guild = {|
|
||||||
|
+id: Snowflake,
|
||||||
|
+name: string,
|
||||||
|
+permissions: number,
|
||||||
|
|};
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/resources/channel#channel-object
|
||||||
|
export type Channel = {|
|
||||||
|
+id: Snowflake,
|
||||||
|
+type: ChannelType,
|
||||||
|
+name: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/resources/user#user-object
|
||||||
|
export type User = {|
|
||||||
|
+id: Snowflake,
|
||||||
|
+username: string,
|
||||||
|
+discriminator: string,
|
||||||
|
+bot: boolean,
|
||||||
|
|};
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/resources/guild#guild-member-object
|
||||||
|
export type GuildMember = {|
|
||||||
|
+user: User,
|
||||||
|
+nick: string | null,
|
||||||
|
+roles: $ReadOnlyArray<Snowflake>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
// From the Discord docs: "emoji takes the form of name:id for
|
||||||
|
// custom guild emoji, or Unicode characters."
|
||||||
|
// https://discordapp.com/developers/docs/resources/channel#create-message
|
||||||
|
export type Emoji = {|
|
||||||
|
+id: ?Snowflake,
|
||||||
|
+name: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type EmojiRef = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom emoji returned from the Discord server are defined
|
||||||
|
* by an `id` and `name` property. Generic emoji are defined by
|
||||||
|
* a unicode name property only, with a null id property.
|
||||||
|
* This function returns the appropriate reference based on this
|
||||||
|
* generic vs custom distinction.
|
||||||
|
*/
|
||||||
|
export function emojiToRef({id, name}: Emoji): EmojiRef {
|
||||||
|
// Built-in emoji, unicode names.
|
||||||
|
if (!id) return name;
|
||||||
|
|
||||||
|
// Custom emoji.
|
||||||
|
return `${name}:${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines whether the message was created by a webhook or a Discord User
|
||||||
|
export function isAuthoredByNonUser(rawMessage: {
|
||||||
|
+webhook_id?: Snowflake,
|
||||||
|
}): boolean {
|
||||||
|
return rawMessage.webhook_id != null || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/resources/channel#message-object
|
||||||
|
export type Message = {|
|
||||||
|
+id: Snowflake,
|
||||||
|
+channelId: Snowflake,
|
||||||
|
+authorId: Snowflake,
|
||||||
|
// Could be a message from a webhook, meaning the authorId isn't a user.
|
||||||
|
+nonUserAuthor: boolean,
|
||||||
|
+timestampMs: number,
|
||||||
|
+content: string,
|
||||||
|
// Normally includes reaction counters, but we don't care about counters.
|
||||||
|
// We could filter based on which types of emoji have been added though.
|
||||||
|
+reactionEmoji: $ReadOnlyArray<Emoji>,
|
||||||
|
// Snowflake of user IDs.
|
||||||
|
+mentions: $ReadOnlyArray<Snowflake>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
// https://discordapp.com/developers/docs/resources/channel#get-reactions
|
||||||
|
export type Reaction = {|
|
||||||
|
+channelId: Snowflake,
|
||||||
|
+messageId: Snowflake,
|
||||||
|
+authorId: Snowflake,
|
||||||
|
+emoji: Emoji,
|
||||||
|
|};
|
28
src/plugins/discord/models.test.js
Normal file
28
src/plugins/discord/models.test.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import {emojiToRef, isAuthoredByNonUser} from "./models";
|
||||||
|
|
||||||
|
describe("plugins/discord/models", () => {
|
||||||
|
describe("model helper functions", () => {
|
||||||
|
describe("emojiToRef", () => {
|
||||||
|
it("returns name if id is null", () => {
|
||||||
|
expect(emojiToRef({id: null, name: "testEmojiName"})).toBe(
|
||||||
|
"testEmojiName"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("returns name and id if id is not null", () => {
|
||||||
|
expect(emojiToRef({id: "testEmojiId", name: "testEmojiName"})).toBe(
|
||||||
|
"testEmojiName:testEmojiId"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("isAuthoredByNonUser", () => {
|
||||||
|
it("returns true if webhook_id property is provided in message", () => {
|
||||||
|
expect(isAuthoredByNonUser({webhook_id: "0"})).toBe(true);
|
||||||
|
});
|
||||||
|
it("returns false if webhook_id property is not provided in message", () => {
|
||||||
|
expect(isAuthoredByNonUser({})).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user