mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-27 11:40:26 +00:00
Move Experimental Discord plugin into master (#1843)
Since the discord branch is being used in production by many projects (MetaGame, AraCred, RaidGuild, etc), and we also want to start using it for SourceCred, it would be a good idea to merge it into master in a separate experimental-discord plugin so the proper discord plugin can be developed in parallel while allowing the other projects to stay up to date with master. paired with @decentralion
This commit is contained in:
parent
1abed5f0ed
commit
a38860a3d2
@ -1 +1 @@
|
||||
[{"type":"sourcecred/project","version":"0.5.1"},{"discourseServer":null,"id":"sourcecred-test/example-github","identities":[],"initiatives":null,"repoIds":[{"name":"example-github","owner":"sourcecred-test"}],"timelineCredParams":null}]
|
||||
[{"type":"sourcecred/project","version":"0.5.2"},{"discord":null,"discourseServer":null,"id":"sourcecred-test/example-github","identities":[],"initiatives":null,"repoIds":[{"name":"example-github","owner":"sourcecred-test"}],"timelineCredParams":null}]
|
@ -4,6 +4,7 @@ import {type Project} from "../core/project";
|
||||
import {type Weights as WeightsT} from "../core/weights";
|
||||
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
|
||||
import {type TimelineCredParameters} from "../analysis/timeline/params";
|
||||
import {type DiscordToken} from "../plugins/experimental-discord/params";
|
||||
import {type GithubToken} from "../plugins/github/token";
|
||||
import {type CacheProvider} from "../backend/cache";
|
||||
import {DataDirectory} from "../backend/dataDirectory";
|
||||
@ -17,6 +18,7 @@ export type LoadOptions = {|
|
||||
+plugins: $ReadOnlyArray<PluginDeclaration>,
|
||||
+sourcecredDirectory: string,
|
||||
+githubToken: ?GithubToken,
|
||||
+discordToken: ?DiscordToken,
|
||||
+initiativesDirectory: ?string,
|
||||
|};
|
||||
|
||||
@ -30,6 +32,7 @@ export async function load(
|
||||
const {
|
||||
sourcecredDirectory,
|
||||
githubToken,
|
||||
discordToken,
|
||||
project,
|
||||
params,
|
||||
weightsOverrides,
|
||||
@ -39,6 +42,7 @@ export async function load(
|
||||
const context = new LoadContext({
|
||||
cache: (data: CacheProvider),
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
initiativesDirectory,
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import {type WeightedGraph as WeightedGraphT} from "../core/weightedGraph";
|
||||
import * as WeightedGraph from "../core/weightedGraph";
|
||||
import {type TimelineCredParameters} from "../analysis/timeline/params";
|
||||
import {type GithubToken} from "../plugins/github/token";
|
||||
import {type DiscordToken} from "../plugins/experimental-discord/params";
|
||||
import {type CacheProvider} from "./cache";
|
||||
import {TaskReporter} from "../util/taskReporter";
|
||||
import {TimelineCred} from "../analysis/timeline/timelineCred";
|
||||
@ -14,8 +15,9 @@ import {type PluginLoaders as PluginLoadersT} from "./pluginLoaders";
|
||||
import * as ComputeFunction from "./computeFunction";
|
||||
import * as PluginLoaders from "./pluginLoaders";
|
||||
import {default as githubLoader} from "../plugins/github/loader";
|
||||
import {default as identityLoader} from "../plugins/identity/loader";
|
||||
import {default as discordLoader} from "../plugins/experimental-discord/loader";
|
||||
import {default as discourseLoader} from "../plugins/discourse/loader";
|
||||
import {default as identityLoader} from "../plugins/identity/loader";
|
||||
import {default as initiativesLoader} from "../plugins/initiatives/loader";
|
||||
import {type PluginDeclarations} from "../analysis/pluginDeclaration";
|
||||
|
||||
@ -29,6 +31,7 @@ export type LoadContextOptions = {|
|
||||
+cache: CacheProvider,
|
||||
+reporter: TaskReporter,
|
||||
+githubToken: ?GithubToken,
|
||||
+discordToken: ?DiscordToken,
|
||||
+initiativesDirectory: ?string,
|
||||
|};
|
||||
|
||||
@ -84,6 +87,7 @@ export class LoadContext {
|
||||
+_compute: ComputeFunctionT = TimelineCred.compute;
|
||||
+_pluginLoaders: PluginLoadersT = {
|
||||
github: githubLoader,
|
||||
discord: discordLoader,
|
||||
discourse: discourseLoader,
|
||||
identity: identityLoader,
|
||||
initiatives: initiativesLoader,
|
||||
|
@ -70,6 +70,7 @@ const mockProxyMethods = (
|
||||
describe("src/backend/loadContext", () => {
|
||||
describe("LoadContext", () => {
|
||||
const githubToken = validateToken("0".repeat(40));
|
||||
const discordToken = "fakeBotToken";
|
||||
const project = createProject({id: "testing-project"});
|
||||
const params = {alpha: 0.123};
|
||||
const initiativesDirectory = fakes.initiativesDirectory;
|
||||
@ -90,6 +91,7 @@ describe("src/backend/loadContext", () => {
|
||||
const loadContext = new LoadContext({
|
||||
cache,
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
initiativesDirectory,
|
||||
});
|
||||
@ -121,6 +123,7 @@ describe("src/backend/loadContext", () => {
|
||||
const loadContext = new LoadContext({
|
||||
cache,
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
initiativesDirectory,
|
||||
});
|
||||
@ -134,6 +137,7 @@ describe("src/backend/loadContext", () => {
|
||||
const expectedEnv = {
|
||||
initiativesDirectory,
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
cache,
|
||||
};
|
||||
@ -183,6 +187,7 @@ describe("src/backend/loadContext", () => {
|
||||
const loadContext = new LoadContext({
|
||||
cache,
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
initiativesDirectory,
|
||||
});
|
||||
@ -195,6 +200,7 @@ describe("src/backend/loadContext", () => {
|
||||
const expectedEnv = {
|
||||
initiativesDirectory,
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
cache,
|
||||
};
|
||||
@ -219,6 +225,7 @@ describe("src/backend/loadContext", () => {
|
||||
const loadContext = new LoadContext({
|
||||
cache,
|
||||
githubToken,
|
||||
discordToken,
|
||||
reporter,
|
||||
initiativesDirectory,
|
||||
});
|
||||
|
@ -7,9 +7,11 @@ import * as WeightedGraph from "../core/weightedGraph";
|
||||
import {type PluginDeclaration} from "../analysis/pluginDeclaration";
|
||||
import {type CacheProvider} from "./cache";
|
||||
import {type GithubToken} from "../plugins/github/token";
|
||||
import {type DiscordToken} from "../plugins/experimental-discord/params";
|
||||
import {type Loader as GithubLoader} from "../plugins/github/loader";
|
||||
import {type Loader as IdentityLoader} from "../plugins/identity/loader";
|
||||
import {type Loader as DiscordLoader} from "../plugins/experimental-discord/loader";
|
||||
import {type Loader as DiscourseLoader} from "../plugins/discourse/loader";
|
||||
import {type Loader as IdentityLoader} from "../plugins/identity/loader";
|
||||
import {type Loader as InitiativesLoader} from "../plugins/initiatives/loader";
|
||||
import {type LoadedInitiativesDirectory} from "../plugins/initiatives/initiativesDirectory";
|
||||
import {
|
||||
@ -25,6 +27,7 @@ import {
|
||||
*/
|
||||
export type PluginLoaders = {|
|
||||
+github: GithubLoader,
|
||||
+discord: DiscordLoader,
|
||||
+discourse: DiscourseLoader,
|
||||
+identity: IdentityLoader,
|
||||
+initiatives: InitiativesLoader,
|
||||
@ -52,19 +55,21 @@ opaque type PluginGraphs = {|
|
||||
type MirrorEnv = {
|
||||
+initiativesDirectory: ?string,
|
||||
+githubToken: ?GithubToken,
|
||||
+discordToken: ?DiscordToken,
|
||||
+reporter: TaskReporter,
|
||||
+cache: CacheProvider,
|
||||
};
|
||||
|
||||
type GraphEnv = {
|
||||
+githubToken: ?GithubToken,
|
||||
+discordToken: ?DiscordToken,
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all relevant PluginDeclarations for a given Project.
|
||||
*/
|
||||
export function declarations(
|
||||
{github, discourse, identity, initiatives}: PluginLoaders,
|
||||
{github, discourse, discord, identity, initiatives}: PluginLoaders,
|
||||
project: Project
|
||||
): $ReadOnlyArray<PluginDeclaration> {
|
||||
const plugins: PluginDeclaration[] = [];
|
||||
@ -74,6 +79,9 @@ export function declarations(
|
||||
if (project.discourseServer != null) {
|
||||
plugins.push(discourse.declaration());
|
||||
}
|
||||
if (project.discord != null) {
|
||||
plugins.push(discord.declaration());
|
||||
}
|
||||
if (project.identities.length) {
|
||||
plugins.push(identity.declaration());
|
||||
}
|
||||
@ -87,8 +95,8 @@ export function declarations(
|
||||
* Updates all mirrors into cache as requested by the Project.
|
||||
*/
|
||||
export async function updateMirror(
|
||||
{github, discourse, initiatives}: PluginLoaders,
|
||||
{githubToken, cache, reporter, initiativesDirectory}: MirrorEnv,
|
||||
{github, discourse, discord, initiatives}: PluginLoaders,
|
||||
{githubToken, discordToken, cache, reporter, initiativesDirectory}: MirrorEnv,
|
||||
project: Project
|
||||
): Promise<CachedProject> {
|
||||
const tasks: Promise<void>[] = [];
|
||||
@ -105,6 +113,14 @@ export async function updateMirror(
|
||||
github.updateMirror(project.repoIds, githubToken, cache, reporter)
|
||||
);
|
||||
}
|
||||
if (project.discord) {
|
||||
if (!discordToken) {
|
||||
throw new Error("Tried to load Discord, but no Discord bot token set");
|
||||
}
|
||||
tasks.push(
|
||||
discord.updateMirror(project.discord, discordToken, cache, reporter)
|
||||
);
|
||||
}
|
||||
|
||||
let loadedInitiativesDirectory: ?LoadedInitiativesDirectory;
|
||||
if (project.initiatives) {
|
||||
@ -160,8 +176,8 @@ export async function createReferenceDetector(
|
||||
* Creates PluginGraphs containing all plugins requested by the Project.
|
||||
*/
|
||||
export async function createPluginGraphs(
|
||||
{github, discourse, initiatives}: PluginLoaders,
|
||||
{githubToken}: GraphEnv,
|
||||
{github, discourse, discord, initiatives}: PluginLoaders,
|
||||
{githubToken, discordToken}: GraphEnv,
|
||||
{cache, project, loadedInitiativesDirectory}: CachedProject,
|
||||
referenceDetector: ReferenceDetector
|
||||
): Promise<PluginGraphs> {
|
||||
@ -177,6 +193,12 @@ export async function createPluginGraphs(
|
||||
}
|
||||
tasks.push(github.createGraph(project.repoIds, githubToken, cache));
|
||||
}
|
||||
if (project.discord) {
|
||||
if (!discordToken) {
|
||||
throw new Error("Tried to load Discord, but no Discord bot token set");
|
||||
}
|
||||
tasks.push(discord.createGraph(project.discord, cache));
|
||||
}
|
||||
|
||||
if (loadedInitiativesDirectory) {
|
||||
tasks.push(
|
||||
|
@ -22,6 +22,7 @@ export function createWG(name: string) {
|
||||
|
||||
const mockGraphs = {
|
||||
github: createWG("github"),
|
||||
discord: createWG("discord"),
|
||||
discourse: createWG("discourse"),
|
||||
initiatives: createWG("initiatives"),
|
||||
contracted: createWG("identity-contracted"),
|
||||
@ -29,6 +30,7 @@ const mockGraphs = {
|
||||
|
||||
const fakes = {
|
||||
githubDeclaration: ({fake: "githubDeclaration"}: any),
|
||||
discordDeclaration: ({fake: "discordDeclaration"}: any),
|
||||
githubReferences: ({fake: "githubReferences"}: any),
|
||||
discourseDeclaration: ({fake: "discourseDeclaration"}: any),
|
||||
discourseReferences: ({fake: "discourseReferences"}: any),
|
||||
@ -59,6 +61,11 @@ const mockPluginLoaders = () => ({
|
||||
referenceDetector: jest.fn().mockResolvedValue(fakes.githubReferences),
|
||||
createGraph: jest.fn().mockResolvedValue(mockGraphs.github),
|
||||
},
|
||||
discord: {
|
||||
declaration: jest.fn().mockReturnValue(fakes.discordDeclaration),
|
||||
updateMirror: jest.fn(),
|
||||
createGraph: jest.fn().mockResolvedValue(mockGraphs.discord),
|
||||
},
|
||||
discourse: {
|
||||
declaration: jest.fn().mockReturnValue(fakes.discourseDeclaration),
|
||||
updateMirror: jest.fn(),
|
||||
@ -149,6 +156,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const cache = mockCacheProvider();
|
||||
const reporter = new TestTaskReporter();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const initiativesDirectory = null;
|
||||
const project = createProject({
|
||||
id: "has-discourse",
|
||||
@ -158,7 +166,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
await PluginLoaders.updateMirror(
|
||||
loaders,
|
||||
{githubToken, cache, reporter, initiativesDirectory},
|
||||
{githubToken, discordToken, cache, reporter, initiativesDirectory},
|
||||
project
|
||||
);
|
||||
|
||||
@ -177,6 +185,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const initiativesDirectory = null;
|
||||
const reporter = new TestTaskReporter();
|
||||
const project = createProject({
|
||||
@ -187,7 +196,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const p = PluginLoaders.updateMirror(
|
||||
loaders,
|
||||
{githubToken, cache, reporter, initiativesDirectory},
|
||||
{githubToken, discordToken, cache, reporter, initiativesDirectory},
|
||||
project
|
||||
);
|
||||
|
||||
@ -203,6 +212,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const cache = mockCacheProvider();
|
||||
const reporter = new TestTaskReporter();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const initiativesDirectory = __dirname;
|
||||
const project = createProject({
|
||||
id: "has-initiatives",
|
||||
@ -212,7 +222,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
await PluginLoaders.updateMirror(
|
||||
loaders,
|
||||
{githubToken, cache, reporter, initiativesDirectory},
|
||||
{githubToken, discordToken, cache, reporter, initiativesDirectory},
|
||||
project
|
||||
);
|
||||
|
||||
@ -233,6 +243,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const initiativesDirectory = null;
|
||||
const reporter = new TestTaskReporter();
|
||||
const project = createProject({
|
||||
@ -243,7 +254,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const p = PluginLoaders.updateMirror(
|
||||
loaders,
|
||||
{githubToken, cache, reporter, initiativesDirectory},
|
||||
{githubToken, discordToken, cache, reporter, initiativesDirectory},
|
||||
project
|
||||
);
|
||||
|
||||
@ -258,6 +269,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = exampleGithubToken;
|
||||
const discordToken = null;
|
||||
const reporter = new TestTaskReporter();
|
||||
const initiativesDirectory = null;
|
||||
const project = createProject({
|
||||
@ -268,7 +280,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
await PluginLoaders.updateMirror(
|
||||
loaders,
|
||||
{githubToken, cache, reporter, initiativesDirectory},
|
||||
{githubToken, discordToken, cache, reporter, initiativesDirectory},
|
||||
project
|
||||
);
|
||||
|
||||
@ -291,6 +303,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const project = createProject({
|
||||
id: "has-discourse",
|
||||
discourseServer: {serverUrl: "http://foo.bar"},
|
||||
@ -300,7 +313,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const pluginGraphs = await PluginLoaders.createPluginGraphs(
|
||||
loaders,
|
||||
{githubToken},
|
||||
{githubToken, discordToken},
|
||||
cachedProject,
|
||||
references
|
||||
);
|
||||
@ -325,6 +338,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const cache = mockCacheProvider();
|
||||
const loadedInitiativesDirectory = mockLoadedDirectory();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const project = createProject({
|
||||
id: "has-initiatives",
|
||||
initiatives: {remoteUrl: "http://example.com/initiatives"},
|
||||
@ -334,7 +348,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const pluginGraphs = await PluginLoaders.createPluginGraphs(
|
||||
loaders,
|
||||
{githubToken},
|
||||
{githubToken, discordToken},
|
||||
cachedProject,
|
||||
references
|
||||
);
|
||||
@ -358,6 +372,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = null;
|
||||
const discordToken = null;
|
||||
const project = createProject({
|
||||
id: "has-github",
|
||||
repoIds: [exampleRepoId],
|
||||
@ -367,7 +382,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const p = PluginLoaders.createPluginGraphs(
|
||||
loaders,
|
||||
{githubToken},
|
||||
{githubToken, discordToken},
|
||||
cachedProject,
|
||||
references
|
||||
);
|
||||
@ -384,6 +399,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = exampleGithubToken;
|
||||
const discordToken = null;
|
||||
const project = createProject({
|
||||
id: "has-github",
|
||||
repoIds: [exampleRepoId],
|
||||
@ -393,7 +409,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const pluginGraphs = await PluginLoaders.createPluginGraphs(
|
||||
loaders,
|
||||
{githubToken},
|
||||
{githubToken, discordToken},
|
||||
cachedProject,
|
||||
references
|
||||
);
|
||||
@ -419,6 +435,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
const loaders = mockPluginLoaders();
|
||||
const cache = mockCacheProvider();
|
||||
const githubToken = exampleGithubToken;
|
||||
const discordToken = null;
|
||||
const loadedInitiativesDirectory = mockLoadedDirectory();
|
||||
const project = createProject({
|
||||
id: "has-github-discourse-initiatives",
|
||||
@ -431,7 +448,7 @@ describe("src/backend/pluginLoaders", () => {
|
||||
// When
|
||||
const references = await PluginLoaders.createReferenceDetector(
|
||||
loaders,
|
||||
{githubToken},
|
||||
{githubToken, discordToken},
|
||||
cachedProject
|
||||
);
|
||||
|
||||
|
@ -7,6 +7,7 @@ import deepFreeze from "deep-freeze";
|
||||
import fs from "fs-extra";
|
||||
import {type Weights, fromJSON as weightsFromJSON} from "../core/weights";
|
||||
import {validateToken, type GithubToken} from "../plugins/github/token";
|
||||
import {type DiscordToken} from "../plugins/experimental-discord/params";
|
||||
|
||||
export type PluginName = "git" | "github";
|
||||
|
||||
@ -33,6 +34,10 @@ export function githubToken(): ?GithubToken {
|
||||
return validateToken(envToken);
|
||||
}
|
||||
|
||||
export function discordToken(): ?DiscordToken {
|
||||
return process.env.SOURCECRED_DISCORD_TOKEN || null;
|
||||
}
|
||||
|
||||
export async function loadWeights(path: string): Promise<Weights> {
|
||||
if (!(await fs.exists(path))) {
|
||||
throw new Error("Could not find the weights file");
|
||||
|
42
src/cli/discord.js
Normal file
42
src/cli/discord.js
Normal file
@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import * as Common from "./common";
|
||||
import {type Command} from "./command";
|
||||
import {LoggingTaskReporter} from "../util/taskReporter";
|
||||
import {DataDirectory} from "../backend/dataDirectory";
|
||||
import Loader from "../plugins/experimental-discord/loader";
|
||||
|
||||
function die(std, message) {
|
||||
std.err("fatal: " + message);
|
||||
std.err("fatal: run 'sourcecred help discord' for help");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TODO: hack
|
||||
const reactionWeights = {"sourcecred:678399364418502669": 4};
|
||||
|
||||
const discord: Command = async (args, std) => {
|
||||
if (args.length !== 1) {
|
||||
return die(std, "Expected one positional argument (or --help).");
|
||||
}
|
||||
const [guildId] = args;
|
||||
|
||||
const taskReporter = new LoggingTaskReporter();
|
||||
const dir = new DataDirectory(Common.sourcecredDirectory());
|
||||
const token = process.env.SOURCECRED_DISCORD_TOKEN || null;
|
||||
if (!token) {
|
||||
throw new Error("Expecting a SOURCECRED_DISCORD_TOKEN");
|
||||
}
|
||||
|
||||
const opts = {
|
||||
guildId,
|
||||
reactionWeights,
|
||||
};
|
||||
|
||||
await Loader.updateMirror(opts, token, dir, taskReporter);
|
||||
const wg = await Loader.createGraph(opts, dir);
|
||||
console.log(wg.graph, wg.weights);
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default discord;
|
@ -102,6 +102,7 @@ const command: Command = async (args, std) => {
|
||||
plugins,
|
||||
sourcecredDirectory: Common.sourcecredDirectory(),
|
||||
githubToken: null,
|
||||
discordToken: null,
|
||||
initiativesDirectory: null,
|
||||
},
|
||||
taskReporter
|
||||
|
@ -149,6 +149,7 @@ const loadCommand: Command = async (args, std) => {
|
||||
params,
|
||||
weightsOverrides: weights,
|
||||
plugins,
|
||||
discordToken: Common.discordToken(),
|
||||
sourcecredDirectory: Common.sourcecredDirectory(),
|
||||
githubToken,
|
||||
initiativesDirectory,
|
||||
|
@ -22,6 +22,7 @@ const load: JestMockFn = (require("../api/load").load: any);
|
||||
|
||||
describe("cli/load", () => {
|
||||
const exampleGithubToken = validateToken("0".repeat(40));
|
||||
const exampleDiscordToken = "fakeBotToken";
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Tests should call `newSourcecredDirectory` directly when they
|
||||
@ -34,6 +35,7 @@ describe("cli/load", () => {
|
||||
const dirname = tmp.dirSync().name;
|
||||
process.env.SOURCECRED_DIRECTORY = dirname;
|
||||
process.env.SOURCECRED_GITHUB_TOKEN = exampleGithubToken;
|
||||
process.env.SOURCECRED_DISCORD_TOKEN = exampleDiscordToken;
|
||||
process.env.SOURCECRED_INITIATIVES_DIRECTORY = tmp.dirSync().name;
|
||||
return dirname;
|
||||
}
|
||||
@ -82,6 +84,7 @@ describe("cli/load", () => {
|
||||
plugins: [githubDeclaration],
|
||||
sourcecredDirectory: Common.sourcecredDirectory(),
|
||||
githubToken: exampleGithubToken,
|
||||
discordToken: exampleDiscordToken,
|
||||
initiativesDirectory: Common.initiativesDirectory(),
|
||||
};
|
||||
expect(await invocation).toEqual({
|
||||
@ -107,6 +110,7 @@ describe("cli/load", () => {
|
||||
plugins: [githubDeclaration],
|
||||
sourcecredDirectory: Common.sourcecredDirectory(),
|
||||
githubToken: exampleGithubToken,
|
||||
discordToken: exampleDiscordToken,
|
||||
initiativesDirectory: Common.initiativesDirectory(),
|
||||
});
|
||||
expect(await invocation).toEqual({
|
||||
@ -145,6 +149,7 @@ describe("cli/load", () => {
|
||||
plugins: [githubDeclaration],
|
||||
sourcecredDirectory: Common.sourcecredDirectory(),
|
||||
githubToken: exampleGithubToken,
|
||||
discordToken: exampleDiscordToken,
|
||||
initiativesDirectory: Common.initiativesDirectory(),
|
||||
};
|
||||
expect(await invocation).toEqual({
|
||||
|
@ -12,6 +12,7 @@ import output from "./output";
|
||||
import clear from "./clear";
|
||||
import genProject from "./genProject";
|
||||
import discourse from "./discourse";
|
||||
import discord from "./discord";
|
||||
|
||||
const sourcecred: Command = async (args, std) => {
|
||||
if (args.length === 0) {
|
||||
@ -35,6 +36,8 @@ const sourcecred: Command = async (args, std) => {
|
||||
return output(args.slice(1), std);
|
||||
case "gen-project":
|
||||
return genProject(args.slice(1), std);
|
||||
case "discord":
|
||||
return discord(args.slice(1), std);
|
||||
case "discourse":
|
||||
return discourse(args.slice(1), std);
|
||||
default:
|
||||
|
@ -7,6 +7,7 @@ import {type ProjectParameters as Initiatives} from "../plugins/initiatives/para
|
||||
import {type Identity} from "../plugins/identity/identity";
|
||||
import {type DiscourseServer} from "../plugins/discourse/server";
|
||||
import type {TimelineCredParameters} from "../analysis/timeline/params";
|
||||
import {type ProjectOptions as Discord} from "../plugins/experimental-discord/params";
|
||||
|
||||
export type ProjectId = string;
|
||||
|
||||
@ -26,24 +27,26 @@ export type ProjectId = string;
|
||||
* the future (e.g. showing the last update time for each of the project's data
|
||||
* dependencies).
|
||||
*/
|
||||
export type Project = ProjectV051;
|
||||
export type Project = ProjectV052;
|
||||
export type SupportedProject =
|
||||
| ProjectV030
|
||||
| ProjectV031
|
||||
| ProjectV040
|
||||
| ProjectV050
|
||||
| ProjectV051
|
||||
| ProjectV050;
|
||||
| ProjectV052;
|
||||
|
||||
export type ProjectV051 = {|
|
||||
export type ProjectV052 = {|
|
||||
+id: ProjectId,
|
||||
+initiatives: Initiatives | null,
|
||||
+repoIds: $ReadOnlyArray<RepoId>,
|
||||
+discourseServer: DiscourseServer | null,
|
||||
+discord: Discord | null,
|
||||
+identities: $ReadOnlyArray<Identity>,
|
||||
+timelineCredParams: $Shape<TimelineCredParameters> | null,
|
||||
|};
|
||||
|
||||
const COMPAT_INFO = {type: "sourcecred/project", version: "0.5.1"};
|
||||
const COMPAT_INFO = {type: "sourcecred/project", version: "0.5.2"};
|
||||
|
||||
/**
|
||||
* Creates a new Project instance with default values.
|
||||
@ -61,6 +64,7 @@ export function createProject(p: $Shape<Project>): Project {
|
||||
discourseServer: null,
|
||||
initiatives: null,
|
||||
timelineCredParams: null,
|
||||
discord: null,
|
||||
...p,
|
||||
};
|
||||
}
|
||||
@ -83,10 +87,25 @@ export function encodeProjectId(id: ProjectId): string {
|
||||
return base64url.encode(id);
|
||||
}
|
||||
|
||||
const upgradeFrom050 = (p: ProjectV050): ProjectV051 => ({
|
||||
const upgradeFrom051 = (p: ProjectV051): ProjectV052 => ({
|
||||
...p,
|
||||
discord: null,
|
||||
});
|
||||
|
||||
export type ProjectV051 = {|
|
||||
+id: ProjectId,
|
||||
+initiatives: Initiatives | null,
|
||||
+repoIds: $ReadOnlyArray<RepoId>,
|
||||
+discourseServer: DiscourseServer | null,
|
||||
+identities: $ReadOnlyArray<Identity>,
|
||||
+timelineCredParams: $Shape<TimelineCredParameters> | null,
|
||||
|};
|
||||
|
||||
const upgradeFrom050 = (p: ProjectV050) =>
|
||||
upgradeFrom051({
|
||||
...p,
|
||||
timelineCredParams: {},
|
||||
});
|
||||
});
|
||||
|
||||
export type ProjectV050 = {|
|
||||
+id: ProjectId,
|
||||
@ -143,4 +162,5 @@ const upgrades = {
|
||||
"0.3.1": upgradeFrom030,
|
||||
"0.4.0": upgradeFrom040,
|
||||
"0.5.0": upgradeFrom050,
|
||||
"0.5.1": upgradeFrom051,
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
|
||||
import {makeRepoId} from "../plugins/github/repoId";
|
||||
import {toCompat} from "../util/compat";
|
||||
import type {ProjectV050} from "./project";
|
||||
import type {ProjectV050, ProjectV051} from "./project";
|
||||
|
||||
describe("core/project", () => {
|
||||
const foobar = deepFreeze(makeRepoId("foo", "bar"));
|
||||
@ -27,6 +27,7 @@ describe("core/project", () => {
|
||||
initiatives: null,
|
||||
identities: [],
|
||||
timelineCredParams: null,
|
||||
discord: null,
|
||||
});
|
||||
const p2: Project = deepFreeze({
|
||||
id: "@foo",
|
||||
@ -40,6 +41,7 @@ describe("core/project", () => {
|
||||
},
|
||||
],
|
||||
timelineCredParams: null,
|
||||
discord: {guildId: "1234", reactionWeights: {}},
|
||||
});
|
||||
describe("to/from JSON", () => {
|
||||
it("round trip is identity", () => {
|
||||
@ -77,6 +79,7 @@ describe("core/project", () => {
|
||||
// It should strip the apiUsername field, keeping just serverUrl.
|
||||
discourseServer: {serverUrl: "https://example.com"},
|
||||
initiatives: null,
|
||||
discord: null,
|
||||
timelineCredParams: {},
|
||||
}: Project)
|
||||
);
|
||||
@ -107,6 +110,7 @@ describe("core/project", () => {
|
||||
// It should strip the apiUsername field, keeping just serverUrl.
|
||||
discourseServer: {serverUrl: "https://example.com"},
|
||||
initiatives: null,
|
||||
discord: null,
|
||||
timelineCredParams: {},
|
||||
}: Project)
|
||||
);
|
||||
@ -133,6 +137,7 @@ describe("core/project", () => {
|
||||
...body,
|
||||
// It should add a default initiatives field.
|
||||
initiatives: null,
|
||||
discord: null,
|
||||
timelineCredParams: {},
|
||||
}: Project)
|
||||
);
|
||||
@ -160,10 +165,37 @@ describe("core/project", () => {
|
||||
...body,
|
||||
// It should add default params field.
|
||||
timelineCredParams: {},
|
||||
discord: null,
|
||||
}: Project)
|
||||
);
|
||||
});
|
||||
});
|
||||
it("should upgrade from 0.5.1 formatting", () => {
|
||||
// Given
|
||||
const body: ProjectV051 = {
|
||||
id: "example-050",
|
||||
repoIds: [foobar, foozod],
|
||||
discourseServer: {serverUrl: "https://example.com"},
|
||||
identities: [],
|
||||
initiatives: null,
|
||||
timelineCredParams: {},
|
||||
};
|
||||
const compat = toCompat(
|
||||
{type: "sourcecred/project", version: "0.5.1"},
|
||||
body
|
||||
);
|
||||
|
||||
// When
|
||||
const project = projectFromJSON(compat);
|
||||
|
||||
// Then
|
||||
expect(project).toEqual(
|
||||
({
|
||||
...body,
|
||||
discord: null,
|
||||
}: Project)
|
||||
);
|
||||
});
|
||||
describe("encodeProjectId", () => {
|
||||
it("is a base64-url encoded id", () => {
|
||||
const project = {id: "foo bar", repoIds: []};
|
||||
@ -202,6 +234,7 @@ describe("core/project", () => {
|
||||
repoIds: [],
|
||||
identities: [],
|
||||
timelineCredParams: null,
|
||||
discord: null,
|
||||
});
|
||||
});
|
||||
it("treats input shape as overrides", () => {
|
||||
@ -219,6 +252,12 @@ describe("core/project", () => {
|
||||
},
|
||||
],
|
||||
timelineCredParams: {alpha: 0.2, intervalDecay: 0.5},
|
||||
discord: {
|
||||
guildId: "123",
|
||||
reactionWeights: {
|
||||
emoji: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// When
|
||||
|
@ -39,6 +39,12 @@ describe("core/project_io", () => {
|
||||
identities: [{username: "foo", aliases: ["github/foo", "discourse/foo"]}],
|
||||
initiatives: {remoteUrl: "https://example.com/initiatives"},
|
||||
timelineCredParams: {alpha: 0.2, intervalDecay: 0.5},
|
||||
discord: {
|
||||
guildId: "123",
|
||||
reactionWeights: {
|
||||
emoji: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("setupProjectDirectory results in a loadable project", async () => {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
set -eu
|
||||
|
||||
snapshots_dir=src/plugins/discord/snapshots
|
||||
snapshots_dir=src/plugins/experimental-discord/snapshots
|
||||
test_instance_url="https://discordapp.com/api"
|
||||
|
||||
if [ ! "$(jq --version)" ]; then
|
||||
|
68
src/plugins/experimental-discord/README.md
Normal file
68
src/plugins/experimental-discord/README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# SourceCred Discord plugin
|
||||
|
||||
This plugin loads data from a Discord server.
|
||||
|
||||
## Developer notes
|
||||
|
||||
Discord developer docs can be found at:
|
||||
https://discordapp.com/developers/docs
|
||||
|
||||
### Setting up a bot
|
||||
|
||||
To query the API with a reasonable rate-limit, you should set up a Discord app,
|
||||
and a Discord bot as part of that app.
|
||||
|
||||
```
|
||||
Discord app
|
||||
└── Discord bot
|
||||
```
|
||||
|
||||
You can create both on the developer portal here:
|
||||
https://discordapp.com/developers/applications
|
||||
|
||||
|
||||
Permissions this bot will need:
|
||||
|
||||
- `View Channels`
|
||||
- `Read Message History`
|
||||
|
||||
Represented as `66560` integer.
|
||||
|
||||
Then, someone with appropriate permissions needs to invite this bot to the server.
|
||||
There isn't a simple generator for this link on the dev portal, you'll need to format it yourself like this:
|
||||
|
||||
`https://discordapp.com/api/oauth2/authorize?client_id={{clientID}}&scope=bot&permissions={{permissionsInt}}`
|
||||
|
||||
And open it in the browser, as a logged-in server admin.
|
||||
|
||||
Read more: https://discordapp.com/developers/docs/topics/oauth2#bot-authorization-flow
|
||||
|
||||
### Authenticating API requests
|
||||
|
||||
You'll want to set your bot's token in a bot style Authorization header.
|
||||
|
||||
https://discordapp.com/developers/docs/reference#authentication
|
||||
|
||||
`Authorization: Bot MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs`
|
||||
|
||||
### Finding configuration values / parameters
|
||||
|
||||
Many of the ID's you will need for configuration are exposed using the Discord client.
|
||||
|
||||
By enabling developer mode (under Appearance > Advanced > Developer Mode).
|
||||
A right-click menu option "Copy ID" will appear.
|
||||
|
||||
Not every element will support this "Copy ID". For example custom emoji.
|
||||
You can find some of those out by using the browser version of Discord and inspecting the DOM.
|
||||
|
||||
Otherwise you can also use the API and query for it yourself with tools like Postman or CURL.
|
||||
|
||||
|
||||
With the bot invited and auth set up. Find out the Guild ID using:
|
||||
|
||||
`GET https://discordapp.com/api/users/@me/guilds`
|
||||
|
||||
Find your custom emoji using:
|
||||
|
||||
`GET https://discordapp.com/api/guilds/{{discordGuildId}}/emojis`
|
||||
|
@ -0,0 +1,394 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads channels 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "678348980756938813",
|
||||
"name": "Text Channels",
|
||||
"type": "GUILD_CATEGORY",
|
||||
},
|
||||
Object {
|
||||
"id": "678348980798881844",
|
||||
"name": "Voice Channels",
|
||||
"type": "GUILD_CATEGORY",
|
||||
},
|
||||
Object {
|
||||
"id": "678348980849213472",
|
||||
"name": "general",
|
||||
"type": "GUILD_TEXT",
|
||||
},
|
||||
Object {
|
||||
"id": "678348980878573661",
|
||||
"name": "General",
|
||||
"type": "GUILD_VOICE",
|
||||
},
|
||||
Object {
|
||||
"id": "678394406507905129",
|
||||
"name": "pagination",
|
||||
"type": "GUILD_TEXT",
|
||||
},
|
||||
Object {
|
||||
"id": "678696874869522446",
|
||||
"name": "notmymessage",
|
||||
"type": "GUILD_TEXT",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads emojis 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "678399364418502669",
|
||||
"name": "sourcecred",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads guilds 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "453243919774253079",
|
||||
"name": "sourcecred",
|
||||
"permissions": 104193601,
|
||||
},
|
||||
Object {
|
||||
"id": "629411177947987986",
|
||||
"name": "MetaGame",
|
||||
"permissions": 104324673,
|
||||
},
|
||||
Object {
|
||||
"id": "678348980639498428",
|
||||
"name": "SourceCred Test Server",
|
||||
"permissions": 33620992,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads members 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"nick": null,
|
||||
"roles": Array [
|
||||
"678349848684003359",
|
||||
"678350026946117694",
|
||||
],
|
||||
"user": Object {
|
||||
"bot": false,
|
||||
"discriminator": "5887",
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"nick": null,
|
||||
"roles": Array [
|
||||
"678349848684003359",
|
||||
"678350026946117694",
|
||||
],
|
||||
"user": Object {
|
||||
"bot": false,
|
||||
"discriminator": "8636",
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"nick": null,
|
||||
"roles": Array [
|
||||
"678349848684003359",
|
||||
"678350026946117694",
|
||||
],
|
||||
"user": Object {
|
||||
"bot": false,
|
||||
"discriminator": "8658",
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"nick": null,
|
||||
"roles": Array [
|
||||
"678349848684003359",
|
||||
"678350026946117694",
|
||||
],
|
||||
"user": Object {
|
||||
"bot": false,
|
||||
"discriminator": "2386",
|
||||
"id": "439050857921904640",
|
||||
"username": "Brian Litwin",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"nick": null,
|
||||
"roles": Array [
|
||||
"678359229433905152",
|
||||
],
|
||||
"user": Object {
|
||||
"bot": true,
|
||||
"discriminator": "1705",
|
||||
"id": "678351352770068560",
|
||||
"username": "CredBot-Beanow",
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads messages 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "10",
|
||||
"id": "678394455849566208",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812242234,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "9",
|
||||
"id": "678394451462193154",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812241188,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "8",
|
||||
"id": "678394448497082388",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812240481,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "7",
|
||||
"id": "678394445351092275",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812239731,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "6",
|
||||
"id": "678394442301833247",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812239004,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "5",
|
||||
"id": "678394436757094410",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [
|
||||
Object {
|
||||
"id": "678399364418502669",
|
||||
"name": "sourcecred",
|
||||
},
|
||||
],
|
||||
"timestampMs": 1581812237682,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "4",
|
||||
"id": "678394433233747978",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812236842,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "3",
|
||||
"id": "678394431149178940",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812236345,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "2",
|
||||
"id": "678394428569813013",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812235730,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "1",
|
||||
"id": "678394426153893948",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [
|
||||
Object {
|
||||
"id": null,
|
||||
"name": "👍",
|
||||
},
|
||||
],
|
||||
"timestampMs": 1581812235154,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads messages 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "20",
|
||||
"id": "678394498098659349",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812252307,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "19",
|
||||
"id": "678394493124476939",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812251121,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "18",
|
||||
"id": "678394489391415348",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812250231,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "17",
|
||||
"id": "678394486644146187",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812249576,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "16",
|
||||
"id": "678394484291141700",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812249015,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "15",
|
||||
"id": "678394480184918016",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [
|
||||
Object {
|
||||
"id": null,
|
||||
"name": "😆",
|
||||
},
|
||||
],
|
||||
"timestampMs": 1581812248036,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "14",
|
||||
"id": "678394477106167818",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812247302,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "13",
|
||||
"id": "678394473428025354",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812246425,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "12",
|
||||
"id": "678394469048909841",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812245381,
|
||||
},
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"content": "11",
|
||||
"id": "678394468373626890",
|
||||
"mentions": Array [],
|
||||
"nonUserAuthor": false,
|
||||
"reactionEmoji": Array [],
|
||||
"timestampMs": 1581812245220,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads reactions 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"authorId": "143776454050709505",
|
||||
"channelId": "678394406507905129",
|
||||
"emoji": Object {
|
||||
"id": "678399364418502669",
|
||||
"name": "sourcecred",
|
||||
},
|
||||
"messageId": "678394436757094410",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`plugins/experimental-discord/fetcher snapshot testing loads roles 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"id": "678348980639498428",
|
||||
"name": "@everyone",
|
||||
},
|
||||
Object {
|
||||
"id": "678349848684003359",
|
||||
"name": "allow-adding-data",
|
||||
},
|
||||
Object {
|
||||
"id": "678350026946117694",
|
||||
"name": "admins",
|
||||
},
|
||||
Object {
|
||||
"id": "678359229433905152",
|
||||
"name": "CredBot-Beanow",
|
||||
},
|
||||
]
|
||||
`;
|
271
src/plugins/experimental-discord/createGraph.js
Normal file
271
src/plugins/experimental-discord/createGraph.js
Normal file
@ -0,0 +1,271 @@
|
||||
// @flow
|
||||
|
||||
import {escape} from "entities";
|
||||
import {type WeightedGraph as WeightedGraphT} from "../../core/weightedGraph";
|
||||
import {type Weights, type NodeWeight} from "../../core/weights";
|
||||
import {
|
||||
Graph,
|
||||
NodeAddress,
|
||||
EdgeAddress,
|
||||
type Node,
|
||||
type Edge,
|
||||
type NodeAddressT,
|
||||
type EdgeAddressT,
|
||||
} from "../../core/graph";
|
||||
import {SqliteMirrorRepository} from "./mirrorRepository";
|
||||
import {
|
||||
memberNodeType,
|
||||
messageNodeType,
|
||||
reactionNodeType,
|
||||
authorsMessageEdgeType,
|
||||
addsReactionEdgeType,
|
||||
reactsToEdgeType,
|
||||
mentionsEdgeType,
|
||||
} from "./declaration";
|
||||
import * as Model from "./models";
|
||||
|
||||
// Display this many characters in description.
|
||||
const MESSAGE_LENGTH = 30;
|
||||
|
||||
function messageUrl(
|
||||
guild: Model.Snowflake,
|
||||
channel: Model.Snowflake,
|
||||
message: Model.Snowflake
|
||||
) {
|
||||
return `https://discordapp.com/channels/${guild}/${channel}/${message}`;
|
||||
}
|
||||
|
||||
export function userAddress(userId: Model.Snowflake): NodeAddressT {
|
||||
return NodeAddress.append(memberNodeType.prefix, "user", userId);
|
||||
}
|
||||
|
||||
function memberAddress(member: Model.GuildMember): NodeAddressT {
|
||||
return NodeAddress.append(
|
||||
memberNodeType.prefix,
|
||||
member.user.bot ? "bot" : "user",
|
||||
member.user.id
|
||||
);
|
||||
}
|
||||
|
||||
function messageAddress(message: Model.Message): NodeAddressT {
|
||||
return NodeAddress.append(
|
||||
messageNodeType.prefix,
|
||||
message.channelId,
|
||||
message.id
|
||||
);
|
||||
}
|
||||
|
||||
function reactionAddress(reaction: Model.Reaction): NodeAddressT {
|
||||
// Hacky order, so we can boost categories.
|
||||
return NodeAddress.append(
|
||||
reactionNodeType.prefix,
|
||||
reaction.channelId,
|
||||
Model.emojiToRef(reaction.emoji),
|
||||
reaction.authorId,
|
||||
reaction.messageId
|
||||
);
|
||||
}
|
||||
|
||||
function memberNode(member: Model.GuildMember): Node {
|
||||
const description = `${escape(member.user.username)}#${
|
||||
member.user.discriminator
|
||||
}`;
|
||||
return {
|
||||
address: memberAddress(member),
|
||||
description,
|
||||
timestampMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
function messageNode(
|
||||
message: Model.Message,
|
||||
guild: Model.Snowflake,
|
||||
channelName: string
|
||||
): Node {
|
||||
const url = messageUrl(guild, message.channelId, message.id);
|
||||
const partialMessage = escape(message.content.substring(0, MESSAGE_LENGTH));
|
||||
const description = `#${channelName} message ["${partialMessage}..."](${url})`;
|
||||
return {
|
||||
address: messageAddress(message),
|
||||
description,
|
||||
timestampMs: message.timestampMs,
|
||||
};
|
||||
}
|
||||
|
||||
function authorsMessageEdge(
|
||||
message: Model.Message,
|
||||
author: Model.GuildMember
|
||||
): Edge {
|
||||
const address: EdgeAddressT = EdgeAddress.append(
|
||||
authorsMessageEdgeType.prefix,
|
||||
author.user.bot ? "bot" : "user",
|
||||
author.user.id,
|
||||
message.channelId,
|
||||
message.id
|
||||
);
|
||||
return {
|
||||
address,
|
||||
timestampMs: message.timestampMs,
|
||||
src: memberAddress(author),
|
||||
dst: messageAddress(message),
|
||||
};
|
||||
}
|
||||
|
||||
function reactionNode(
|
||||
reaction: Model.Reaction,
|
||||
timestampMs: number,
|
||||
guild: Model.Snowflake
|
||||
): Node {
|
||||
const msgUrl = messageUrl(guild, reaction.channelId, reaction.messageId);
|
||||
const reactionStr = reaction.emoji.id
|
||||
? `:${reaction.emoji.name}:`
|
||||
: reaction.emoji.name;
|
||||
const description = `Reacted \`${reactionStr}\` to message [${reaction.messageId}](${msgUrl})`;
|
||||
return {
|
||||
address: reactionAddress(reaction),
|
||||
description,
|
||||
timestampMs,
|
||||
};
|
||||
}
|
||||
|
||||
function addsReactionEdge(
|
||||
reaction: Model.Reaction,
|
||||
member: Model.GuildMember,
|
||||
timestampMs: number
|
||||
): Edge {
|
||||
const address: EdgeAddressT = EdgeAddress.append(
|
||||
addsReactionEdgeType.prefix,
|
||||
member.user.bot ? "bot" : "user",
|
||||
member.user.id,
|
||||
Model.emojiToRef(reaction.emoji),
|
||||
reaction.channelId,
|
||||
reaction.messageId
|
||||
);
|
||||
return {
|
||||
address,
|
||||
// TODO: for now using timestamp of the message,
|
||||
// as reactions don't have timestamps.
|
||||
timestampMs,
|
||||
src: memberAddress(member),
|
||||
dst: reactionAddress(reaction),
|
||||
};
|
||||
}
|
||||
|
||||
function reactsToEdge(reaction: Model.Reaction, message: Model.Message): Edge {
|
||||
const address: EdgeAddressT = EdgeAddress.append(
|
||||
reactsToEdgeType.prefix,
|
||||
Model.emojiToRef(reaction.emoji),
|
||||
reaction.authorId,
|
||||
reaction.channelId,
|
||||
reaction.messageId
|
||||
);
|
||||
return {
|
||||
address,
|
||||
// TODO: for now using timestamp of the message,
|
||||
// as reactions don't have timestamps.
|
||||
timestampMs: message.timestampMs,
|
||||
src: reactionAddress(reaction),
|
||||
dst: messageAddress(message),
|
||||
};
|
||||
}
|
||||
|
||||
function mentionsEdge(message: Model.Message, member: Model.GuildMember): Edge {
|
||||
const address: EdgeAddressT = EdgeAddress.append(
|
||||
mentionsEdgeType.prefix,
|
||||
message.channelId,
|
||||
message.authorId,
|
||||
message.id,
|
||||
member.user.bot ? "bot" : "user",
|
||||
member.user.id
|
||||
);
|
||||
return {
|
||||
address,
|
||||
timestampMs: message.timestampMs,
|
||||
src: messageAddress(message),
|
||||
dst: memberAddress(member),
|
||||
};
|
||||
}
|
||||
|
||||
export type EmojiWeightMap = {[ref: Model.EmojiRef]: NodeWeight};
|
||||
|
||||
export function createGraph(
|
||||
guild: Model.Snowflake,
|
||||
repo: SqliteMirrorRepository,
|
||||
declarationWeights: Weights,
|
||||
emojiWeights: EmojiWeightMap
|
||||
): WeightedGraphT {
|
||||
const wg = {
|
||||
graph: new Graph(),
|
||||
weights: declarationWeights,
|
||||
};
|
||||
|
||||
const memberMap = new Map(repo.members().map((m) => [m.user.id, m]));
|
||||
const channels = repo.channels();
|
||||
for (const channel of channels) {
|
||||
const messages = repo.messages(channel.id);
|
||||
for (const message of messages) {
|
||||
const hasMentions = message.mentions.length > 0;
|
||||
if (!hasMentions && message.reactionEmoji.length === 0) continue;
|
||||
if (message.nonUserAuthor) continue;
|
||||
|
||||
let hasWeightedEmoji = false;
|
||||
const reactions = repo.reactions(channel.id, message.id);
|
||||
for (const reaction of reactions) {
|
||||
const emojiRef = Model.emojiToRef(reaction.emoji);
|
||||
const reactionWeight = emojiWeights[emojiRef];
|
||||
|
||||
// TODO: Skip all unweighted emoji in prototype.
|
||||
if (!reactionWeight) continue;
|
||||
|
||||
const reactingMember = memberMap.get(reaction.authorId);
|
||||
if (!reactingMember) {
|
||||
console.warn(
|
||||
`Reacting member not loaded ${reaction.authorId} (reacted ${emojiRef}), maybe a Deleted User?\n` +
|
||||
`${messageUrl(guild, channel.id, message.id)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
hasWeightedEmoji = true;
|
||||
const node = reactionNode(reaction, message.timestampMs, guild);
|
||||
wg.weights.nodeWeights.set(node.address, reactionWeight);
|
||||
wg.graph.addNode(node);
|
||||
wg.graph.addNode(memberNode(reactingMember));
|
||||
wg.graph.addEdge(reactsToEdge(reaction, message));
|
||||
wg.graph.addEdge(
|
||||
addsReactionEdge(reaction, reactingMember, message.timestampMs)
|
||||
);
|
||||
}
|
||||
|
||||
for (const userId of message.mentions) {
|
||||
const mentionedMember = memberMap.get(userId);
|
||||
if (!mentionedMember) {
|
||||
console.warn(
|
||||
`Mentioned member not loaded ${userId}, maybe a Deleted User?\n` +
|
||||
`${messageUrl(guild, channel.id, message.id)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
wg.graph.addNode(memberNode(mentionedMember));
|
||||
wg.graph.addEdge(mentionsEdge(message, mentionedMember));
|
||||
}
|
||||
|
||||
// Don't bloat the graph with no-weighted-reaction messages.
|
||||
if (hasWeightedEmoji || hasMentions) {
|
||||
const author = memberMap.get(message.authorId);
|
||||
if (!author) {
|
||||
console.warn(
|
||||
`Message author not loaded ${message.authorId}, maybe a Deleted User?\n` +
|
||||
`${messageUrl(guild, channel.id, message.id)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
wg.graph.addNode(memberNode(author));
|
||||
wg.graph.addNode(messageNode(message, guild, channel.name));
|
||||
wg.graph.addEdge(authorsMessageEdge(message, author));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wg;
|
||||
}
|
79
src/plugins/experimental-discord/declaration.js
Normal file
79
src/plugins/experimental-discord/declaration.js
Normal file
@ -0,0 +1,79 @@
|
||||
// @flow
|
||||
|
||||
import deepFreeze from "deep-freeze";
|
||||
import type {PluginDeclaration} from "../../analysis/pluginDeclaration";
|
||||
import type {NodeType, EdgeType} from "../../analysis/types";
|
||||
import {NodeAddress, EdgeAddress} from "../../core/graph";
|
||||
|
||||
export const nodePrefix = NodeAddress.fromParts(["sourcecred", "discord"]);
|
||||
export const edgePrefix = EdgeAddress.fromParts(["sourcecred", "discord"]);
|
||||
|
||||
export const memberNodeType: NodeType = deepFreeze({
|
||||
name: "Member",
|
||||
pluralName: "Members",
|
||||
prefix: NodeAddress.append(nodePrefix, "MEMBER"),
|
||||
defaultWeight: 0,
|
||||
description: "A member of the Discord server",
|
||||
});
|
||||
|
||||
export const messageNodeType: NodeType = deepFreeze({
|
||||
name: "Message",
|
||||
pluralName: "Messages",
|
||||
prefix: NodeAddress.append(nodePrefix, "MESSAGE"),
|
||||
defaultWeight: 0,
|
||||
description: "A Discord message, posted in a particular channel",
|
||||
});
|
||||
|
||||
export const reactionNodeType: NodeType = deepFreeze({
|
||||
name: "Reaction",
|
||||
pluralName: "Reactions",
|
||||
prefix: NodeAddress.append(nodePrefix, "REACTION"),
|
||||
defaultWeight: 1,
|
||||
description: "A reaction by some user, directed at some message",
|
||||
});
|
||||
|
||||
export const authorsMessageEdgeType: EdgeType = deepFreeze({
|
||||
forwardName: "authors message",
|
||||
backwardName: "message is authored by",
|
||||
prefix: EdgeAddress.append(edgePrefix, "AUTHORS", "MESSAGE"),
|
||||
defaultWeight: {forwards: 1 / 4, backwards: 1},
|
||||
description: "Connects an author to a message they've created.",
|
||||
});
|
||||
|
||||
export const addsReactionEdgeType: EdgeType = deepFreeze({
|
||||
forwardName: "adds reaction",
|
||||
backwardName: "reaction added by",
|
||||
prefix: EdgeAddress.append(edgePrefix, "ADDS_REACTION"),
|
||||
defaultWeight: {forwards: 1, backwards: 1 / 16},
|
||||
description: "Connects a member to a reaction that they added.",
|
||||
});
|
||||
|
||||
export const reactsToEdgeType: EdgeType = deepFreeze({
|
||||
forwardName: "reacts to",
|
||||
backwardName: "is reacted to by",
|
||||
prefix: EdgeAddress.append(edgePrefix, "REACTS_TO"),
|
||||
defaultWeight: {forwards: 1, backwards: 1 / 16},
|
||||
description: "Connects a reaction to a message that it reacts to.",
|
||||
});
|
||||
|
||||
export const mentionsEdgeType: EdgeType = deepFreeze({
|
||||
forwardName: "mentions",
|
||||
backwardName: "is mentioned by",
|
||||
prefix: EdgeAddress.append(edgePrefix, "MENTIONS"),
|
||||
defaultWeight: {forwards: 1, backwards: 1 / 16},
|
||||
description: "Connects a message to the member being mentioned.",
|
||||
});
|
||||
|
||||
export const declaration: PluginDeclaration = deepFreeze({
|
||||
name: "Discord",
|
||||
nodePrefix,
|
||||
edgePrefix,
|
||||
nodeTypes: [memberNodeType, messageNodeType, reactionNodeType],
|
||||
edgeTypes: [
|
||||
authorsMessageEdgeType,
|
||||
addsReactionEdgeType,
|
||||
reactsToEdgeType,
|
||||
mentionsEdgeType,
|
||||
],
|
||||
userTypes: [memberNodeType],
|
||||
});
|
189
src/plugins/experimental-discord/fetcher.js
Normal file
189
src/plugins/experimental-discord/fetcher.js
Normal file
@ -0,0 +1,189 @@
|
||||
// @flow
|
||||
|
||||
import fetch from "isomorphic-fetch";
|
||||
import * as Model from "./models";
|
||||
|
||||
export interface DiscordApi {
|
||||
guilds(): Promise<$ReadOnlyArray<Model.Guild>>;
|
||||
emojis(guild: Model.Snowflake): Promise<$ReadOnlyArray<Model.Emoji>>;
|
||||
channels(guild: Model.Snowflake): Promise<$ReadOnlyArray<Model.Channel>>;
|
||||
roles(guild: Model.Snowflake): Promise<$ReadOnlyArray<Model.Role>>;
|
||||
members(guild: Model.Snowflake): Promise<$ReadOnlyArray<Model.GuildMember>>;
|
||||
messages(
|
||||
channel: Model.Snowflake,
|
||||
after: Model.Snowflake,
|
||||
limit: number
|
||||
): Promise<$ReadOnlyArray<Model.Message>>;
|
||||
reactions(
|
||||
channel: Model.Snowflake,
|
||||
message: Model.Snowflake,
|
||||
emoji: Model.Emoji
|
||||
): Promise<$ReadOnlyArray<Model.Reaction>>;
|
||||
}
|
||||
|
||||
const fetcherDefaults: FetcherOptions = {
|
||||
apiUrl: "https://discordapp.com/api",
|
||||
token: null,
|
||||
fetch,
|
||||
};
|
||||
|
||||
type FetcherOptions = {|
|
||||
+apiUrl: string,
|
||||
+fetch: typeof fetch,
|
||||
+token: ?Model.BotToken,
|
||||
|};
|
||||
|
||||
export class Fetcher implements DiscordApi {
|
||||
+_options: FetcherOptions;
|
||||
|
||||
constructor(opts?: $Shape<FetcherOptions>) {
|
||||
this._options = {...fetcherDefaults, ...opts};
|
||||
if (!this._options.token) {
|
||||
throw new Error("A BotToken is required");
|
||||
}
|
||||
}
|
||||
|
||||
_fetch(endpoint: string): Promise<Response> {
|
||||
const {apiUrl, token} = this._options;
|
||||
if (!token) {
|
||||
throw new Error("A BotToken is required");
|
||||
}
|
||||
const requestOptions = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bot ${token}`,
|
||||
},
|
||||
};
|
||||
const url = new URL(`${apiUrl}${endpoint}`).href;
|
||||
return this._options.fetch(url, requestOptions);
|
||||
}
|
||||
|
||||
async _fetchJson(endpoint: string): Promise<any> {
|
||||
const res = await this._fetch(endpoint);
|
||||
failIfMissing(res);
|
||||
failForNotOk(res);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async guilds(): Promise<$ReadOnlyArray<Model.Guild>> {
|
||||
const guilds = await this._fetchJson("/users/@me/guilds");
|
||||
return guilds.map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
permissions: x.permissions,
|
||||
}));
|
||||
}
|
||||
|
||||
async emojis(guild: Model.Snowflake): Promise<$ReadOnlyArray<Model.Emoji>> {
|
||||
const emojis = await this._fetchJson(`/guilds/${guild}/emojis`);
|
||||
return emojis.map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async channels(
|
||||
guild: Model.Snowflake
|
||||
): Promise<$ReadOnlyArray<Model.Channel>> {
|
||||
const channels = await this._fetchJson(`/guilds/${guild}/channels`);
|
||||
return channels.map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
type: Model.channelTypeFromId(x.type),
|
||||
}));
|
||||
}
|
||||
|
||||
async roles(guild: Model.Snowflake): Promise<$ReadOnlyArray<Model.Role>> {
|
||||
const roles = await this._fetchJson(`/guilds/${guild}/roles`);
|
||||
return roles.map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async members(
|
||||
guild: Model.Snowflake
|
||||
): Promise<$ReadOnlyArray<Model.GuildMember>> {
|
||||
// TODO: hack, should have pagination.
|
||||
const members = await this._fetchJson(
|
||||
`/guilds/${guild}/members?limit=1000`
|
||||
);
|
||||
if (members.length === 1000) {
|
||||
throw new Error(
|
||||
"TODO: getting 1000 members, needs to implement pagination"
|
||||
);
|
||||
}
|
||||
return members.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,
|
||||
}));
|
||||
}
|
||||
|
||||
async messages(
|
||||
channel: Model.Snowflake,
|
||||
after: Model.Snowflake,
|
||||
limit: number
|
||||
): Promise<$ReadOnlyArray<Model.Message>> {
|
||||
const messages = await this._fetchJson(
|
||||
`/channels/${channel}/messages?after=${after}&limit=${limit}`
|
||||
);
|
||||
return messages.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: x.webhook_id != null || false,
|
||||
mentions: (x.mentions || []).map((user) => user.id),
|
||||
}));
|
||||
}
|
||||
|
||||
async reactions(
|
||||
channel: Model.Snowflake,
|
||||
message: Model.Snowflake,
|
||||
emoji: Model.Emoji
|
||||
): Promise<$ReadOnlyArray<Model.Reaction>> {
|
||||
// TODO: implement pagination.
|
||||
const after = "0";
|
||||
const limit = 100;
|
||||
const emojiRef = Model.emojiToRef(emoji);
|
||||
const reactingUsers = await this._fetchJson(
|
||||
`/channels/${channel}/messages/${message}/reactions/${emojiRef}?after=${after}&limit=${limit}`
|
||||
);
|
||||
if (reactingUsers.length === 100) {
|
||||
throw new Error("TODO: implement reactions pagination");
|
||||
}
|
||||
return reactingUsers.map((x) => ({
|
||||
channelId: channel,
|
||||
messageId: message,
|
||||
emoji,
|
||||
authorId: x.id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function failIfMissing(response: Response) {
|
||||
if (response.status === 404) {
|
||||
throw new Error(`404 Not Found on: ${response.url}; maybe bad serverUrl?`);
|
||||
}
|
||||
if (response.status === 403) {
|
||||
throw new Error(`403 Forbidden: bad API username or key?\n${response.url}`);
|
||||
}
|
||||
if (response.status === 410) {
|
||||
throw new Error(`410 Gone`);
|
||||
}
|
||||
}
|
||||
|
||||
function failForNotOk(response: Response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`not OK status ${response.status} on ${response.url}`);
|
||||
}
|
||||
}
|
41
src/plugins/experimental-discord/fetcher.test.js
Normal file
41
src/plugins/experimental-discord/fetcher.test.js
Normal file
@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
|
||||
import {snapshotFetcher} from "./mockSnapshotFetcher";
|
||||
|
||||
describe("plugins/experimental-discord/fetcher", () => {
|
||||
describe("snapshot testing", () => {
|
||||
const guildId = "678348980639498428";
|
||||
const channelId = "678394406507905129";
|
||||
const messageId = "678394436757094410";
|
||||
const emoji = {id: "678399364418502669", name: "sourcecred"};
|
||||
|
||||
it("loads guilds", async () => {
|
||||
expect(await snapshotFetcher().guilds()).toMatchSnapshot();
|
||||
});
|
||||
it("loads channels", async () => {
|
||||
expect(await snapshotFetcher().channels(guildId)).toMatchSnapshot();
|
||||
});
|
||||
it("loads emojis", async () => {
|
||||
expect(await snapshotFetcher().emojis(guildId)).toMatchSnapshot();
|
||||
});
|
||||
it("loads roles", async () => {
|
||||
expect(await snapshotFetcher().roles(guildId)).toMatchSnapshot();
|
||||
});
|
||||
it("loads members", async () => {
|
||||
expect(await snapshotFetcher().members(guildId)).toMatchSnapshot();
|
||||
});
|
||||
it("loads messages", async () => {
|
||||
expect(
|
||||
await snapshotFetcher().messages(channelId, "0", 10)
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
await snapshotFetcher().messages(channelId, "678394455849566208", 10)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
it("loads reactions", async () => {
|
||||
expect(
|
||||
await snapshotFetcher().reactions(channelId, messageId, emoji)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
56
src/plugins/experimental-discord/loader.js
Normal file
56
src/plugins/experimental-discord/loader.js
Normal file
@ -0,0 +1,56 @@
|
||||
// @flow
|
||||
|
||||
import {type PluginDeclaration} from "../../analysis/pluginDeclaration";
|
||||
import {type WeightedGraph} from "../../core/weightedGraph";
|
||||
import {TaskReporter} from "../../util/taskReporter";
|
||||
import {type CacheProvider} from "../../backend/cache";
|
||||
import {SqliteMirrorRepository} from "./mirrorRepository";
|
||||
import {weightsForDeclaration} from "../../analysis/pluginDeclaration";
|
||||
import {createGraph as _createGraph} from "./createGraph";
|
||||
import {type ProjectOptions} from "./params";
|
||||
import {declaration} from "./declaration";
|
||||
import * as Model from "./models";
|
||||
import {Fetcher} from "./fetcher";
|
||||
import {Mirror} from "./mirror";
|
||||
|
||||
export interface Loader {
|
||||
declaration(): PluginDeclaration;
|
||||
updateMirror: typeof updateMirror;
|
||||
createGraph: typeof createGraph;
|
||||
}
|
||||
|
||||
export default ({
|
||||
declaration: () => declaration,
|
||||
updateMirror,
|
||||
createGraph,
|
||||
}: Loader);
|
||||
|
||||
export async function updateMirror(
|
||||
{guildId}: ProjectOptions,
|
||||
token: Model.BotToken,
|
||||
cache: CacheProvider,
|
||||
reporter: TaskReporter
|
||||
): Promise<void> {
|
||||
const repo = await repository(cache, guildId);
|
||||
const fetcher = new Fetcher({token});
|
||||
const mirror = new Mirror(repo, fetcher, guildId);
|
||||
await mirror.update(reporter);
|
||||
}
|
||||
|
||||
export async function createGraph(
|
||||
{guildId, reactionWeights}: ProjectOptions,
|
||||
cache: CacheProvider
|
||||
): Promise<WeightedGraph> {
|
||||
const repo = await repository(cache, guildId);
|
||||
const declarationWeights = weightsForDeclaration(declaration);
|
||||
return await _createGraph(guildId, repo, declarationWeights, reactionWeights);
|
||||
}
|
||||
|
||||
async function repository(
|
||||
cache: CacheProvider,
|
||||
guild: Model.Snowflake
|
||||
): Promise<SqliteMirrorRepository> {
|
||||
// TODO: should replace base64url with hex, to be case insensitive.
|
||||
const db = await cache.database(`discord_${guild}`);
|
||||
return new SqliteMirrorRepository(db, guild);
|
||||
}
|
98
src/plugins/experimental-discord/mirror.js
Normal file
98
src/plugins/experimental-discord/mirror.js
Normal file
@ -0,0 +1,98 @@
|
||||
// @flow
|
||||
|
||||
import {TaskReporter} from "../../util/taskReporter";
|
||||
import {type DiscordApi} from "./fetcher";
|
||||
import {SqliteMirrorRepository} from "./mirrorRepository";
|
||||
import * as Model from "./models";
|
||||
|
||||
// How many messages for each channel to reload.
|
||||
const RELOAD_DEPTH = 50;
|
||||
|
||||
export class Mirror {
|
||||
+_repo: SqliteMirrorRepository;
|
||||
+_api: DiscordApi;
|
||||
+guild: Model.Snowflake;
|
||||
|
||||
constructor(
|
||||
repo: SqliteMirrorRepository,
|
||||
api: DiscordApi,
|
||||
guild: Model.Snowflake
|
||||
) {
|
||||
this._repo = repo;
|
||||
this._api = api;
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
async update(reporter: TaskReporter) {
|
||||
const guild = await this.validateGuildId();
|
||||
reporter.start(`discord/${guild.name}`);
|
||||
await this.addMembers();
|
||||
const channels = await this.addTextChannels();
|
||||
for (const channel of channels) {
|
||||
reporter.start(`discord/${guild.name}/#${channel.name}`);
|
||||
try {
|
||||
await this.addMessages(channel.id);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
reporter.finish(`discord/${guild.name}/#${channel.name}`);
|
||||
}
|
||||
reporter.finish(`discord/${guild.name}`);
|
||||
}
|
||||
|
||||
async validateGuildId() {
|
||||
const guilds = await this._api.guilds();
|
||||
const guild = guilds.find((g) => g.id === this.guild);
|
||||
if (!guild) {
|
||||
throw new Error(
|
||||
`Couldn't find guild with ID ${this.guild}\nMaybe the bot has no access to it?`
|
||||
);
|
||||
}
|
||||
// TODO: validate bot permissions
|
||||
return guild;
|
||||
}
|
||||
|
||||
async addMembers() {
|
||||
const members = await this._api.members(this.guild);
|
||||
for (const member of members) {
|
||||
this._repo.addMember(member);
|
||||
}
|
||||
return this._repo.members();
|
||||
}
|
||||
|
||||
async addTextChannels() {
|
||||
const channels = await this._api.channels(this.guild);
|
||||
for (const channel of channels) {
|
||||
if (channel.type !== "GUILD_TEXT") continue;
|
||||
this._repo.addChannel(channel);
|
||||
}
|
||||
return this._repo.channels();
|
||||
}
|
||||
|
||||
async addMessages(channel: Model.Snowflake, messageLimit?: number) {
|
||||
const loadStart = this._repo.nthMessageFromTail(channel, RELOAD_DEPTH);
|
||||
// console.log(channel, (loadStart || {}).id);
|
||||
|
||||
const limit = messageLimit || 100;
|
||||
let page: $ReadOnlyArray<Model.Message> = [];
|
||||
let after: Model.Snowflake = loadStart ? loadStart.id : "0";
|
||||
do {
|
||||
page = await this._api.messages(channel, after, limit);
|
||||
for (const message of page) {
|
||||
after = after < message.id ? message.id : after;
|
||||
this._repo.addMessage(message);
|
||||
for (const emoji of message.reactionEmoji) {
|
||||
const reactions = await this._api.reactions(
|
||||
channel,
|
||||
message.id,
|
||||
emoji
|
||||
);
|
||||
for (const reaction of reactions) {
|
||||
this._repo.addReaction(reaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (page.length >= limit);
|
||||
return this._repo.messages(channel);
|
||||
}
|
||||
}
|
28
src/plugins/experimental-discord/mirror.test.js
Normal file
28
src/plugins/experimental-discord/mirror.test.js
Normal file
@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
import {snapshotFetcher} from "./mockSnapshotFetcher";
|
||||
import {SqliteMirrorRepository} from "./mirrorRepository";
|
||||
import {Mirror} from "./mirror";
|
||||
|
||||
describe("plugins/experimental-discord/mirror", () => {
|
||||
describe("smoke test", () => {
|
||||
const guildId = "678348980639498428";
|
||||
// const channelId = "678394406507905129";
|
||||
|
||||
it("should print", async () => {
|
||||
// Given
|
||||
const repo = new SqliteMirrorRepository(
|
||||
new Database(":memory:"),
|
||||
guildId
|
||||
);
|
||||
const api = snapshotFetcher();
|
||||
|
||||
// When
|
||||
const mirror = new Mirror(repo, api, guildId);
|
||||
await mirror.addMembers();
|
||||
await mirror.addTextChannels();
|
||||
// await mirror.addMessages(channelId, 10);
|
||||
});
|
||||
});
|
||||
});
|
463
src/plugins/experimental-discord/mirrorRepository.js
Normal file
463
src/plugins/experimental-discord/mirrorRepository.js
Normal file
@ -0,0 +1,463 @@
|
||||
// @flow
|
||||
|
||||
import type Database from "better-sqlite3";
|
||||
import stringify from "json-stable-stringify";
|
||||
import * as Model from "./models";
|
||||
import dedent from "../../util/dedent";
|
||||
|
||||
const VERSION = "discord_mirror_v1";
|
||||
|
||||
export class SqliteMirrorRepository {
|
||||
+_db: Database;
|
||||
|
||||
constructor(db: Database, guild: Model.Snowflake) {
|
||||
if (db == null) throw new Error("db: " + String(db));
|
||||
this._db = db;
|
||||
this._transaction(() => {
|
||||
this._initialize(guild);
|
||||
});
|
||||
}
|
||||
|
||||
_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,
|
||||
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,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
`,
|
||||
dedent`\
|
||||
CREATE TABLE members (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
discriminator TEXT NOT NULL,
|
||||
bot INTEGER NOT NULL,
|
||||
nick TEXT
|
||||
)
|
||||
`,
|
||||
dedent`\
|
||||
CREATE TABLE member_roles (
|
||||
user_id TEXT,
|
||||
role TEXT NOT NULL,
|
||||
CONSTRAINT user_role PRIMARY KEY (user_id, role)
|
||||
)
|
||||
`,
|
||||
dedent`\
|
||||
CREATE TABLE messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
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
|
||||
)
|
||||
`,
|
||||
dedent`\
|
||||
CREATE TABLE message_reactions (
|
||||
channel_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
author_id TEXT NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
CONSTRAINT value_object PRIMARY KEY (channel_id, message_id, author_id, emoji)
|
||||
)
|
||||
`,
|
||||
dedent`\
|
||||
CREATE TABLE message_mentions (
|
||||
channel_id TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
CONSTRAINT value_object PRIMARY KEY (channel_id, message_id, user_id)
|
||||
)
|
||||
`,
|
||||
];
|
||||
for (const sql of tables) {
|
||||
db.prepare(sql).run();
|
||||
}
|
||||
}
|
||||
|
||||
members(): $ReadOnlyArray<Model.GuildMember> {
|
||||
return this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
discriminator,
|
||||
bot,
|
||||
nick
|
||||
FROM members`
|
||||
)
|
||||
.all()
|
||||
.map((x) => ({
|
||||
user: {
|
||||
id: x.user_id,
|
||||
username: x.username,
|
||||
discriminator: x.discriminator,
|
||||
bot: x.bot === 1,
|
||||
},
|
||||
nick: x.nick,
|
||||
roles: this.memberRoles(x.user_id),
|
||||
}));
|
||||
}
|
||||
|
||||
memberRoles(user: Model.Snowflake): $ReadOnlyArray<Model.Snowflake> {
|
||||
return this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
SELECT
|
||||
user_id,
|
||||
role
|
||||
FROM member_roles
|
||||
WHERE user_id = :user_id`
|
||||
)
|
||||
.all({user_id: user})
|
||||
.map((x) => x.role);
|
||||
}
|
||||
|
||||
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),
|
||||
}));
|
||||
}
|
||||
|
||||
nthMessageFromTail(channel: Model.Snowflake, n: number): ?Model.Message {
|
||||
const count = this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
SELECT count(*) as count
|
||||
FROM messages
|
||||
WHERE channel_id = :channel_id`
|
||||
)
|
||||
.get({channel_id: channel}).count;
|
||||
|
||||
if (count < n) return null;
|
||||
const offset = count - n;
|
||||
const m = this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
SELECT
|
||||
id,
|
||||
channel_id,
|
||||
author_id,
|
||||
non_user_author,
|
||||
timestamp_ms,
|
||||
content
|
||||
FROM messages
|
||||
WHERE channel_id = :channel_id
|
||||
ORDER BY timestamp_ms
|
||||
LIMIT 1
|
||||
OFFSET :offset
|
||||
`
|
||||
)
|
||||
.get({channel_id: channel, offset});
|
||||
|
||||
return {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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),
|
||||
}));
|
||||
}
|
||||
|
||||
addChannel(channel: Model.Channel) {
|
||||
this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
INSERT OR IGNORE 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`\
|
||||
INSERT OR IGNORE 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,
|
||||
});
|
||||
|
||||
for (const user of message.mentions) {
|
||||
this.addMention(message, user);
|
||||
}
|
||||
}
|
||||
|
||||
addReaction(reaction: Model.Reaction) {
|
||||
this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
INSERT OR IGNORE 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`\
|
||||
INSERT OR IGNORE 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,
|
||||
});
|
||||
}
|
||||
|
||||
addMember(member: Model.GuildMember) {
|
||||
this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
INSERT OR IGNORE INTO members (
|
||||
user_id,
|
||||
username,
|
||||
discriminator,
|
||||
bot,
|
||||
nick
|
||||
) VALUES (
|
||||
:user_id,
|
||||
:username,
|
||||
:discriminator,
|
||||
:bot,
|
||||
:nick
|
||||
)
|
||||
`
|
||||
)
|
||||
.run({
|
||||
user_id: member.user.id,
|
||||
username: member.user.username,
|
||||
discriminator: member.user.discriminator,
|
||||
bot: Number(member.user.bot),
|
||||
nick: member.nick,
|
||||
});
|
||||
|
||||
for (const role of member.roles) {
|
||||
this.addMemberRole(member.user.id, role);
|
||||
}
|
||||
}
|
||||
|
||||
addMemberRole(user: Model.Snowflake, role: Model.Snowflake) {
|
||||
this._db
|
||||
.prepare(
|
||||
dedent`\
|
||||
INSERT OR IGNORE INTO member_roles (
|
||||
user_id,
|
||||
role
|
||||
) VALUES (
|
||||
:user_id,
|
||||
:role
|
||||
)
|
||||
`
|
||||
)
|
||||
.run({
|
||||
user_id: user,
|
||||
role,
|
||||
});
|
||||
}
|
||||
}
|
24
src/plugins/experimental-discord/mockSnapshotFetcher.js
Normal file
24
src/plugins/experimental-discord/mockSnapshotFetcher.js
Normal file
@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import base64url from "base64url";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import {Fetcher} from "./fetcher";
|
||||
|
||||
async function snapshotFetch(url: string | Request | URL): Promise<Response> {
|
||||
const snapshotDir = "src/plugins/experimental-discord/snapshots";
|
||||
const filename = base64url(url);
|
||||
const file = path.join(snapshotDir, filename);
|
||||
if (await fs.exists(file)) {
|
||||
const contents = await fs.readFile(file);
|
||||
return new Response(contents, {status: 200});
|
||||
} else {
|
||||
throw new Error(`couldn't load snapshot for ${file} (${String(url)})`);
|
||||
}
|
||||
}
|
||||
|
||||
export const snapshotFetcher = () =>
|
||||
new Fetcher({
|
||||
fetch: snapshotFetch,
|
||||
token: "mock-token",
|
||||
});
|
112
src/plugins/experimental-discord/models.js
Normal file
112
src/plugins/experimental-discord/models.js
Normal file
@ -0,0 +1,112 @@
|
||||
// @flow
|
||||
|
||||
// https://discordapp.com/developers/docs/reference#snowflakes
|
||||
export type Snowflake = string;
|
||||
export const ZeroSnowflake: Snowflake = "0";
|
||||
|
||||
export type Guild = {|
|
||||
+id: Snowflake,
|
||||
+name: string,
|
||||
+permissions: number,
|
||||
|};
|
||||
|
||||
export type BotToken = string;
|
||||
|
||||
// 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";
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Channel = {|
|
||||
+id: Snowflake,
|
||||
+type: ChannelType,
|
||||
+name: string,
|
||||
|};
|
||||
|
||||
export type Role = {|
|
||||
+id: Snowflake,
|
||||
+name: string,
|
||||
|};
|
||||
|
||||
export type User = {|
|
||||
+id: Snowflake,
|
||||
+username: string,
|
||||
+discriminator: string,
|
||||
+bot: boolean,
|
||||
|};
|
||||
|
||||
export type GuildMember = {|
|
||||
+user: User,
|
||||
+nick: string | null,
|
||||
+roles: $ReadOnlyArray<Snowflake>,
|
||||
|};
|
||||
|
||||
export type Emoji = {|
|
||||
+id: ?Snowflake,
|
||||
+name: string,
|
||||
|};
|
||||
|
||||
export type EmojiRef = string;
|
||||
|
||||
export function emojiToRef({id, name}: Emoji): EmojiRef {
|
||||
// Built-in emoji, unicode names.
|
||||
if (!id) return name;
|
||||
|
||||
// Custom emoji.
|
||||
return `${name}:${id}`;
|
||||
}
|
||||
|
||||
export function refToEmoji(ref: EmojiRef): Emoji {
|
||||
const [name, id] = ref.split(":");
|
||||
if (!id) return {id: null, name};
|
||||
return {id, name};
|
||||
}
|
||||
|
||||
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>,
|
||||
|};
|
||||
|
||||
export type Reaction = {|
|
||||
+channelId: Snowflake,
|
||||
+messageId: Snowflake,
|
||||
+authorId: Snowflake,
|
||||
+emoji: Emoji,
|
||||
|};
|
11
src/plugins/experimental-discord/params.js
Normal file
11
src/plugins/experimental-discord/params.js
Normal file
@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import * as Model from "./models";
|
||||
import {type EmojiWeightMap} from "./createGraph";
|
||||
|
||||
export type {BotToken as DiscordToken} from "./models";
|
||||
|
||||
export type ProjectOptions = {|
|
||||
+guildId: Model.Snowflake,
|
||||
+reactionWeights: EmojiWeightMap,
|
||||
|};
|
@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
{
|
||||
"id": "439050857921904640",
|
||||
"username": "Brian Litwin",
|
||||
"avatar": "1cc439c3506ede04a77be8105d934df4",
|
||||
"discriminator": "2386",
|
||||
"public_flags": 0
|
||||
}
|
||||
]
|
@ -0,0 +1,224 @@
|
||||
[
|
||||
{
|
||||
"id": "678697350385893404",
|
||||
"type": 0,
|
||||
"content": "@here could each of you add one message to <#678696874869522446>? Content doesn't matter :]",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": true,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T20:20:57.919000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": "678399364418502669",
|
||||
"name": "sourcecred"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👍"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "🐙"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "🌱"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "🔥"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "❤️"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "💯"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678401508219420682",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "439050857921904640",
|
||||
"username": "Brian Litwin",
|
||||
"avatar": "1cc439c3506ede04a77be8105d934df4",
|
||||
"discriminator": "2386"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:45:23.650000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678370372390748199",
|
||||
"type": 0,
|
||||
"content": "Made <@!420341518948237331> and <@!432981598858903585> admins 😄",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [
|
||||
{
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
"avatar": "012988df3eab485878fd588696097802",
|
||||
"discriminator": "8658"
|
||||
},
|
||||
{
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
"avatar": "d54a9b690fd2e47abd4268c55cc25b8f",
|
||||
"discriminator": "8636"
|
||||
}
|
||||
],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T22:41:40.290000+00:00",
|
||||
"edited_timestamp": "2020-02-16T00:06:41.114000+00:00",
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👀"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👍"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678367818013737031",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
"avatar": "d54a9b690fd2e47abd4268c55cc25b8f",
|
||||
"discriminator": "8636"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T22:31:31.279000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678365403600257034",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
"avatar": "012988df3eab485878fd588696097802",
|
||||
"discriminator": "8658"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T22:21:55.638000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678359229685563412",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "678351352770068560",
|
||||
"username": "CredBot-Beanow",
|
||||
"avatar": "04fdfc8af8cca3c0b3155a946dccc435",
|
||||
"discriminator": "1705",
|
||||
"bot": true
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T21:57:23.662000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
}
|
||||
]
|
@ -0,0 +1,151 @@
|
||||
[
|
||||
{
|
||||
"id": "678401508219420682",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "439050857921904640",
|
||||
"username": "Brian Litwin",
|
||||
"avatar": "1cc439c3506ede04a77be8105d934df4",
|
||||
"discriminator": "2386",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:45:23.650000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678370372390748199",
|
||||
"type": 0,
|
||||
"content": "Made <@!420341518948237331> and <@!432981598858903585> admins 😄",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [
|
||||
{
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
"avatar": "012988df3eab485878fd588696097802",
|
||||
"discriminator": "8658",
|
||||
"public_flags": 0
|
||||
},
|
||||
{
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
"avatar": "a70b70bc0471e6a1efe5124c39c76020",
|
||||
"discriminator": "8636",
|
||||
"public_flags": 0
|
||||
}
|
||||
],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T22:41:40.290000+00:00",
|
||||
"edited_timestamp": "2020-02-16T00:06:41.114000+00:00",
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👀"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
},
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👍"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678367818013737031",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
"avatar": "a70b70bc0471e6a1efe5124c39c76020",
|
||||
"discriminator": "8636",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T22:31:31.279000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678365403600257034",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
"avatar": "012988df3eab485878fd588696097802",
|
||||
"discriminator": "8658",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T22:21:55.638000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678359229685563412",
|
||||
"type": 7,
|
||||
"content": "",
|
||||
"channel_id": "678348980849213472",
|
||||
"author": {
|
||||
"id": "678351352770068560",
|
||||
"username": "CredBot-Beanow",
|
||||
"avatar": "6f5b83041061a114c9f0fb26b863622c",
|
||||
"discriminator": "1705",
|
||||
"public_flags": 0,
|
||||
"bot": true
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-15T21:57:23.662000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
}
|
||||
]
|
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
}
|
||||
]
|
@ -0,0 +1,242 @@
|
||||
[
|
||||
{
|
||||
"id": "678394455849566208",
|
||||
"type": 0,
|
||||
"content": "10",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:22.234000+00:00",
|
||||
"edited_timestamp": "2020-02-16T00:17:38.520000+00:00",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394451462193154",
|
||||
"type": 0,
|
||||
"content": "9",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:21.188000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394448497082388",
|
||||
"type": 0,
|
||||
"content": "8",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:20.481000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394445351092275",
|
||||
"type": 0,
|
||||
"content": "7",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:19.731000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394442301833247",
|
||||
"type": 0,
|
||||
"content": "6",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:19.004000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394436757094410",
|
||||
"type": 0,
|
||||
"content": "5",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:17.682000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": "678399364418502669",
|
||||
"name": "sourcecred"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678394433233747978",
|
||||
"type": 0,
|
||||
"content": "4",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:16.842000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394431149178940",
|
||||
"type": 0,
|
||||
"content": "3",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:16.345000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394428569813013",
|
||||
"type": 0,
|
||||
"content": "2",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:15.730000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394426153893948",
|
||||
"type": 0,
|
||||
"content": "1",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:15.154000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👍"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,137 @@
|
||||
[
|
||||
{
|
||||
"id": "678394436757094410",
|
||||
"type": 0,
|
||||
"content": "5",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:17.682000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": "678399364418502669",
|
||||
"name": "sourcecred"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678394433233747978",
|
||||
"type": 0,
|
||||
"content": "4",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:16.842000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394431149178940",
|
||||
"type": 0,
|
||||
"content": "3",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:16.345000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394428569813013",
|
||||
"type": 0,
|
||||
"content": "2",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:15.730000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394426153893948",
|
||||
"type": 0,
|
||||
"content": "1",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:15.154000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "👍"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,127 @@
|
||||
[
|
||||
{
|
||||
"id": "678394480184918016",
|
||||
"type": 0,
|
||||
"content": "15",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:28.036000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "😆"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678394477106167818",
|
||||
"type": 0,
|
||||
"content": "14",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:27.302000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394473428025354",
|
||||
"type": 0,
|
||||
"content": "13",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:26.425000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394469048909841",
|
||||
"type": 0,
|
||||
"content": "12",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:25.381000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394468373626890",
|
||||
"type": 0,
|
||||
"content": "11",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:25.220000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
}
|
||||
]
|
@ -0,0 +1,232 @@
|
||||
[
|
||||
{
|
||||
"id": "678394498098659349",
|
||||
"type": 0,
|
||||
"content": "20",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:32.307000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394493124476939",
|
||||
"type": 0,
|
||||
"content": "19",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:31.121000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394489391415348",
|
||||
"type": 0,
|
||||
"content": "18",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:30.231000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394486644146187",
|
||||
"type": 0,
|
||||
"content": "17",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:29.576000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394484291141700",
|
||||
"type": 0,
|
||||
"content": "16",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:29.015000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394480184918016",
|
||||
"type": 0,
|
||||
"content": "15",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:28.036000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0,
|
||||
"reactions": [
|
||||
{
|
||||
"emoji": {
|
||||
"id": null,
|
||||
"name": "😆"
|
||||
},
|
||||
"count": 1,
|
||||
"me": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "678394477106167818",
|
||||
"type": 0,
|
||||
"content": "14",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:27.302000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394473428025354",
|
||||
"type": 0,
|
||||
"content": "13",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:26.425000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394469048909841",
|
||||
"type": 0,
|
||||
"content": "12",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:25.381000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394468373626890",
|
||||
"type": 0,
|
||||
"content": "11",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:25.220000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
}
|
||||
]
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1,117 @@
|
||||
[
|
||||
{
|
||||
"id": "678394455849566208",
|
||||
"type": 0,
|
||||
"content": "10",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:22.234000+00:00",
|
||||
"edited_timestamp": "2020-02-16T00:17:38.520000+00:00",
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394451462193154",
|
||||
"type": 0,
|
||||
"content": "9",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:21.188000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394448497082388",
|
||||
"type": 0,
|
||||
"content": "8",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:20.481000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394445351092275",
|
||||
"type": 0,
|
||||
"content": "7",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:19.731000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "678394442301833247",
|
||||
"type": 0,
|
||||
"content": "6",
|
||||
"channel_id": "678394406507905129",
|
||||
"author": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"attachments": [],
|
||||
"embeds": [],
|
||||
"mentions": [],
|
||||
"mention_roles": [],
|
||||
"pinned": false,
|
||||
"mention_everyone": false,
|
||||
"tts": false,
|
||||
"timestamp": "2020-02-16T00:17:19.004000+00:00",
|
||||
"edited_timestamp": null,
|
||||
"flags": 0
|
||||
}
|
||||
]
|
@ -0,0 +1,87 @@
|
||||
{
|
||||
"id": "678348980639498428",
|
||||
"name": "SourceCred Test Server",
|
||||
"icon": null,
|
||||
"description": null,
|
||||
"splash": null,
|
||||
"discovery_splash": null,
|
||||
"features": [],
|
||||
"emojis": [
|
||||
{
|
||||
"name": "sourcecred",
|
||||
"roles": [],
|
||||
"id": "678399364418502669",
|
||||
"require_colons": true,
|
||||
"managed": false,
|
||||
"animated": false,
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
"banner": null,
|
||||
"owner_id": "143776454050709505",
|
||||
"application_id": null,
|
||||
"region": "us-south",
|
||||
"afk_channel_id": null,
|
||||
"afk_timeout": 300,
|
||||
"system_channel_id": "678348980849213472",
|
||||
"widget_enabled": false,
|
||||
"widget_channel_id": null,
|
||||
"verification_level": 0,
|
||||
"roles": [
|
||||
{
|
||||
"id": "678348980639498428",
|
||||
"name": "@everyone",
|
||||
"permissions": 33620992,
|
||||
"position": 0,
|
||||
"color": 0,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
{
|
||||
"id": "678349848684003359",
|
||||
"name": "allow-adding-data",
|
||||
"permissions": 104324673,
|
||||
"position": 1,
|
||||
"color": 2123412,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
{
|
||||
"id": "678350026946117694",
|
||||
"name": "admins",
|
||||
"permissions": 104324681,
|
||||
"position": 2,
|
||||
"color": 15105570,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
{
|
||||
"id": "678359229433905152",
|
||||
"name": "CredBot-Beanow",
|
||||
"permissions": 66560,
|
||||
"position": 1,
|
||||
"color": 0,
|
||||
"hoist": false,
|
||||
"managed": true,
|
||||
"mentionable": false
|
||||
}
|
||||
],
|
||||
"default_message_notifications": 1,
|
||||
"mfa_level": 0,
|
||||
"explicit_content_filter": 0,
|
||||
"max_presences": null,
|
||||
"max_members": 250000,
|
||||
"max_video_channel_users": 25,
|
||||
"vanity_url_code": null,
|
||||
"premium_tier": 0,
|
||||
"premium_subscription_count": 0,
|
||||
"system_channel_flags": 0,
|
||||
"preferred_locale": "en-US",
|
||||
"rules_channel_id": null,
|
||||
"public_updates_channel_id": null,
|
||||
"embed_enabled": false,
|
||||
"embed_channel_id": null
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
[
|
||||
{
|
||||
"id": "678348980756938813",
|
||||
"type": 4,
|
||||
"name": "Text Channels",
|
||||
"position": 0,
|
||||
"parent_id": null,
|
||||
"guild_id": "678348980639498428",
|
||||
"permission_overwrites": [],
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"id": "678348980798881844",
|
||||
"type": 4,
|
||||
"name": "Voice Channels",
|
||||
"position": 0,
|
||||
"parent_id": null,
|
||||
"guild_id": "678348980639498428",
|
||||
"permission_overwrites": [],
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"id": "678348980849213472",
|
||||
"last_message_id": "678697350385893404",
|
||||
"type": 0,
|
||||
"name": "general",
|
||||
"position": 0,
|
||||
"parent_id": "678348980756938813",
|
||||
"topic": null,
|
||||
"guild_id": "678348980639498428",
|
||||
"permission_overwrites": [],
|
||||
"nsfw": false,
|
||||
"rate_limit_per_user": 0
|
||||
},
|
||||
{
|
||||
"id": "678348980878573661",
|
||||
"type": 2,
|
||||
"name": "General",
|
||||
"position": 0,
|
||||
"parent_id": "678348980798881844",
|
||||
"bitrate": 64000,
|
||||
"user_limit": 0,
|
||||
"guild_id": "678348980639498428",
|
||||
"permission_overwrites": [],
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"id": "678394406507905129",
|
||||
"last_message_id": "678394498098659349",
|
||||
"type": 0,
|
||||
"name": "pagination",
|
||||
"position": 1,
|
||||
"parent_id": "678348980756938813",
|
||||
"topic": "A known length of messages to test pagination.",
|
||||
"guild_id": "678348980639498428",
|
||||
"permission_overwrites": [],
|
||||
"nsfw": false,
|
||||
"rate_limit_per_user": 0
|
||||
},
|
||||
{
|
||||
"id": "678696874869522446",
|
||||
"last_message_id": "678698977100824587",
|
||||
"type": 0,
|
||||
"name": "notmymessage",
|
||||
"position": 2,
|
||||
"parent_id": "678348980756938813",
|
||||
"topic": "Reactions to messages which aren't our own.",
|
||||
"guild_id": "678348980639498428",
|
||||
"permission_overwrites": [],
|
||||
"nsfw": false,
|
||||
"rate_limit_per_user": 0
|
||||
}
|
||||
]
|
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "sourcecred",
|
||||
"roles": [],
|
||||
"id": "678399364418502669",
|
||||
"require_colons": true,
|
||||
"managed": false,
|
||||
"animated": false,
|
||||
"available": true
|
||||
}
|
||||
]
|
@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"user": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887",
|
||||
"public_flags": 0
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T21:16:40.215000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
"avatar": "a70b70bc0471e6a1efe5124c39c76020",
|
||||
"discriminator": "8636",
|
||||
"public_flags": 0
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T22:31:31.243000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
"avatar": "012988df3eab485878fd588696097802",
|
||||
"discriminator": "8658",
|
||||
"public_flags": 0
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T22:21:55.613000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "439050857921904640",
|
||||
"username": "Brian Litwin",
|
||||
"avatar": "1cc439c3506ede04a77be8105d934df4",
|
||||
"discriminator": "2386",
|
||||
"public_flags": 0
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-16T00:45:23.478000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "678351352770068560",
|
||||
"username": "CredBot-Beanow",
|
||||
"avatar": "6f5b83041061a114c9f0fb26b863622c",
|
||||
"discriminator": "1705",
|
||||
"public_flags": 0,
|
||||
"bot": true
|
||||
},
|
||||
"roles": [
|
||||
"678359229433905152"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T21:57:23.609000+00:00"
|
||||
}
|
||||
]
|
@ -0,0 +1,87 @@
|
||||
[
|
||||
{
|
||||
"user": {
|
||||
"id": "143776454050709505",
|
||||
"username": "Beanow",
|
||||
"avatar": "6496446fe1455f31f9536b132dcc4ac8",
|
||||
"discriminator": "5887"
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T21:16:40.215000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "420341518948237331",
|
||||
"username": "decentralion",
|
||||
"avatar": "d54a9b690fd2e47abd4268c55cc25b8f",
|
||||
"discriminator": "8636"
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T22:31:31.243000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "432981598858903585",
|
||||
"username": "wchargin",
|
||||
"avatar": "012988df3eab485878fd588696097802",
|
||||
"discriminator": "8658"
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T22:21:55.613000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "439050857921904640",
|
||||
"username": "Brian Litwin",
|
||||
"avatar": "1cc439c3506ede04a77be8105d934df4",
|
||||
"discriminator": "2386"
|
||||
},
|
||||
"roles": [
|
||||
"678349848684003359",
|
||||
"678350026946117694"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-16T00:45:23.478000+00:00"
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "678351352770068560",
|
||||
"username": "CredBot-Beanow",
|
||||
"avatar": "04fdfc8af8cca3c0b3155a946dccc435",
|
||||
"discriminator": "1705",
|
||||
"bot": true
|
||||
},
|
||||
"roles": [
|
||||
"678359229433905152"
|
||||
],
|
||||
"nick": null,
|
||||
"premium_since": null,
|
||||
"mute": false,
|
||||
"deaf": false,
|
||||
"joined_at": "2020-02-15T21:57:23.609000+00:00"
|
||||
}
|
||||
]
|
@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"id": "678348980639498428",
|
||||
"name": "@everyone",
|
||||
"permissions": 33620992,
|
||||
"position": 0,
|
||||
"color": 0,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
{
|
||||
"id": "678349848684003359",
|
||||
"name": "allow-adding-data",
|
||||
"permissions": 104324673,
|
||||
"position": 1,
|
||||
"color": 2123412,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
{
|
||||
"id": "678350026946117694",
|
||||
"name": "admins",
|
||||
"permissions": 104324681,
|
||||
"position": 2,
|
||||
"color": 15105570,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
{
|
||||
"id": "678359229433905152",
|
||||
"name": "CredBot-Beanow",
|
||||
"permissions": 66560,
|
||||
"position": 1,
|
||||
"color": 0,
|
||||
"hoist": false,
|
||||
"managed": true,
|
||||
"mentionable": false
|
||||
}
|
||||
]
|
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "453243919774253079",
|
||||
"name": "sourcecred",
|
||||
"icon": "934f3f7c5d6df1d014c55c1760090d7d",
|
||||
"owner": false,
|
||||
"permissions": 104193601,
|
||||
"features": []
|
||||
},
|
||||
{
|
||||
"id": "629411177947987986",
|
||||
"name": "MetaGame",
|
||||
"icon": "e3602d4e34004a5053e4d43d81cf8291",
|
||||
"owner": false,
|
||||
"permissions": 104324673,
|
||||
"features": []
|
||||
},
|
||||
{
|
||||
"id": "678348980639498428",
|
||||
"name": "SourceCred Test Server",
|
||||
"icon": null,
|
||||
"owner": false,
|
||||
"permissions": 33620992,
|
||||
"features": []
|
||||
}
|
||||
]
|
40
src/plugins/experimental-discord/update_discord_api_snapshots.sh
Executable file
40
src/plugins/experimental-discord/update_discord_api_snapshots.sh
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
snapshots_dir=src/plugins/experimental-discord/snapshots
|
||||
test_instance_url="https://discordapp.com/api"
|
||||
|
||||
if [ ! "$(jq --version)" ]; then
|
||||
printf >&2 'This script depends on jq. Please install it.\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SOURCECRED_DISCORD_BOT_TOKEN:-}" ]; then
|
||||
printf >&2 "Please set the SOURCECRED_DISCORD_TOKEN environment variable.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
toplevel="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
|
||||
cd "${toplevel}"
|
||||
|
||||
fetch() {
|
||||
url="${test_instance_url}$1"
|
||||
filename="$(printf '%s' "${url}" | base64 -w 0 | tr -d '=' | tr '/+' '_-')"
|
||||
path="${snapshots_dir}/${filename}"
|
||||
curl -sfL "$url" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bot ${SOURCECRED_DISCORD_BOT_TOKEN}" \
|
||||
| jq '.' > "${path}"
|
||||
}
|
||||
|
||||
rm -r "${snapshots_dir}"
|
||||
mkdir "${snapshots_dir}"
|
||||
fetch "/guilds/678348980639498428"
|
||||
fetch "/guilds/678348980639498428/channels"
|
||||
fetch "/guilds/678348980639498428/members?after=0&limit=5"
|
||||
fetch "/channels/678348980849213472/messages?after=0&limit=5"
|
||||
fetch "/channels/678394406507905129/messages?after=0&limit=5"
|
||||
fetch "/channels/678394406507905129/messages?after=678394436757094410&limit=5"
|
||||
fetch "/channels/678394406507905129/messages?after=678394455849566208&limit=5"
|
||||
fetch "/channels/678348980849213472/messages/678697350385893404/reactions/sourcecred:678399364418502669?after=0&limit=5"
|
Loading…
x
Reference in New Issue
Block a user