Remove Discourse admin API key and user. (#1431)

This removes all usage of and reference to the admin API key and username. Instead relying on anonymous access of the Discourse API.

This enables anyone to deploy an instance with discourse support, and is much safer, since the admin API key isn't used for this purpose anymore. Once merged I would encourage revoking any admin API keys used in the past.

The only notable remaining reference of the discourse username is in the project file.
Which goes from 0.3.0 to 0.3.1 in a backwards-compatible way here, simply ignoring the username if present. For #1426 I'm expecting a 0.4.0 version, so this is to prevent having to change project files twice.

Test plan: updated the snapshots to their latest anonymous versions. Ran yarn test and anonymous discourse loading from CLI numerous times.
This commit is contained in:
Robin van Boven 2019-11-08 02:34:00 +01:00 committed by Dandelion Mané
parent ada9140663
commit 64834c6874
23 changed files with 188 additions and 753 deletions

View File

@ -63,23 +63,15 @@ Finally, we can navigate a browser window to `localhost:8080` to view generated
SourceCred can also run on Discourse instances!
To do so, you'll first need admin access on the Discourse server in question. Generate
an admin API key, available at the `/admin/api/keys`. You should also create a user account
on the instance that will be the nominal user for the API requests. You shouldn't use an admin
user identity for this, because then SourceCred could pick up private or deleted posts. Instead,
we recommend making a user called "credbot" with no special permissions.
Once you have the key and user ready, prepare SourceCred using the same steps as above,
and then use the `sourcecred discourse` command, providing the server url, and then the username.
Below is an example for loading the cred for SourceCred's [own discourse instance][forum].
Prepare SourceCred using the same steps as above, then use the `sourcecred discourse` command,
providing the server url. Below is an example for loading the cred for SourceCred's [own discourse instance][forum].
```Bash
git clone https://github.com/sourcecred/sourcecred.git
cd sourcecred
yarn install
yarn backend
export SOURCECRED_DISCOURSE_KEY=$YOUR_KEY
node bin/sourcecred.js discourse https://discourse.sourcecred.io credbot
node bin/sourcecred.js discourse https://discourse.sourcecred.io
```
### Running with Docker

View File

@ -23,7 +23,6 @@ export type LoadOptions = {|
+plugins: $ReadOnlyArray<PluginDeclaration>,
+sourcecredDirectory: string,
+githubToken: string | null,
+discourseKey: string | null,
|};
/**
@ -54,12 +53,9 @@ export async function load(
function discourseGraph(): ?Promise<Graph> {
const discourseServer = project.discourseServer;
if (discourseServer != null) {
const {serverUrl, apiUsername} = discourseServer;
if (options.discourseKey == null) {
throw new Error("Tried to load Discourse, but no Discourse key set");
}
const {serverUrl} = discourseServer;
const discourseOptions = {
fetchOptions: {apiKey: options.discourseKey, serverUrl, apiUsername},
fetchOptions: {serverUrl},
cacheDirectory,
};
return loadDiscourse(discourseOptions, taskReporter);

View File

@ -59,19 +59,16 @@ describe("api/load", () => {
timelineCredCompute.mockResolvedValue(fakeTimelineCred);
});
const discourseServerUrl = "https://example.com";
const discourseApiUsername = "credbot";
const project: Project = {
id: "foo",
repoIds: [makeRepoId("foo", "bar")],
discourseServer: {
serverUrl: discourseServerUrl,
apiUsername: discourseApiUsername,
},
identities: [],
};
deepFreeze(project);
const githubToken = "EXAMPLE_TOKEN";
const discourseKey = "EXAMPLE_KEY";
const weights = defaultWeights();
// Tweaks the weights so that we can ensure we aren't overriding with default weights
weights.nodeManualWeights.set(NodeAddress.empty, 33);
@ -87,7 +84,6 @@ describe("api/load", () => {
params,
plugins,
project,
discourseKey,
};
return {options, taskReporter, sourcecredDirectory};
};
@ -120,8 +116,6 @@ describe("api/load", () => {
const cacheDirectory = path.join(sourcecredDirectory, "cache");
const expectedOptions: LoadDiscourseOptions = {
fetchOptions: {
apiUsername: discourseApiUsername,
apiKey: discourseKey,
serverUrl: discourseServerUrl,
},
cacheDirectory,
@ -175,15 +169,6 @@ describe("api/load", () => {
]);
});
it("errors if a discourse server is provided without a discourse key", () => {
const {options, taskReporter} = example();
const optionsWithoutKey = {...options, discourseKey: null};
expect.assertions(1);
return load(optionsWithoutKey, taskReporter).catch((e) =>
expect(e.message).toMatch("no Discourse key")
);
});
it("errors if GitHub repoIds are provided without a GitHub token", () => {
const {options, taskReporter} = example();
const optionsWithoutToken = {...options, githubToken: null};
@ -196,7 +181,7 @@ describe("api/load", () => {
it("only loads GitHub if no Discourse server set", async () => {
const {options, taskReporter, sourcecredDirectory} = example();
const newProject = {...options.project, discourseServer: null};
const newOptions = {...options, project: newProject, discourseKey: null};
const newOptions = {...options, project: newProject};
await load(newOptions, taskReporter);
expect(loadDiscourse).not.toHaveBeenCalled();
const projectDirectory = directoryForProjectId(

View File

@ -26,10 +26,6 @@ export function githubToken(): string | null {
return NullUtil.orElse(process.env.SOURCECRED_GITHUB_TOKEN, null);
}
export function discourseKey(): string | null {
return NullUtil.orElse(process.env.SOURCECRED_DISCOURSE_KEY, null);
}
export async function loadWeights(path: string): Promise<Weights> {
if (!(await fs.exists(path))) {
throw new Error("Could not find the weights file");

View File

@ -11,7 +11,6 @@ import {
defaultSourcecredDirectory,
sourcecredDirectory,
githubToken,
discourseKey,
loadWeights,
} from "./common";
@ -61,17 +60,6 @@ describe("cli/common", () => {
});
});
describe("discourseKey", () => {
it("uses the environment variable when available", () => {
process.env.SOURCECRED_DISCOURSE_KEY = "010101";
expect(discourseKey()).toEqual("010101");
});
it("returns `null` if the environment variable is not set", () => {
delete process.env.SOURCECRED_DISCOURSE_KEY;
expect(discourseKey()).toBe(null);
});
});
describe("loadWeights", () => {
function tmpWithContents(contents: mixed) {
const name = tmp.tmpNameSync();

View File

@ -15,7 +15,7 @@ import {type Project} from "../core/project";
function usage(print: (string) => void): void {
print(
dedent`\
usage: sourcecred discourse DISCOURSE_URL DISCOURSE_USERNAME
usage: sourcecred discourse DISCOURSE_URL
[--weights WEIGHTS_FILE]
sourcecred discourse --help
@ -26,13 +26,6 @@ function usage(print: (string) => void): void {
The url to the Discourse server in question, for example
https://discourse.sourcecred.io
DISCOURSE_USERNAME
A user account on the Discourse server, to be used as the
"perspective" for the Discourse API calls. This user should not be
a privileged or admin user, otherwise hidden or deleted topics may
be included in the results. We recommend making a new user called
"credbot" on the server, with no special roles or permissions.
--weights WEIGHTS_FILE
Path to a json file which contains a weights configuration.
This will be used instead of the default weights and persisted.
@ -41,13 +34,6 @@ function usage(print: (string) => void): void {
Show this help message and exit, as 'sourcecred help discourse'.
Environment variables:
SOURCECRED_DISCOURSE_KEY
A Discourse admin API key generated from discourse server in
question.
To generate a key, use the /admin/api/keys route on your Discourse
server, e.g. https://discourse.example.com/admin/api/keys
SOURCECRED_DIRECTORY
Directory owned by SourceCred, in which data, caches,
registries, etc. are stored. Optional: defaults to a
@ -88,10 +74,10 @@ const command: Command = async (args, std) => {
}
}
}
if (positionalArgs.length !== 2) {
return die(std, "Expected two positional arguments (or --help).");
if (positionalArgs.length !== 1) {
return die(std, "Expected one positional arguments (or --help).");
}
const [serverUrl, apiUsername] = positionalArgs;
const [serverUrl] = positionalArgs;
let projectId = serverUrl;
if (projectId.startsWith("https://")) {
projectId = projectId.slice("https://".length);
@ -104,7 +90,7 @@ const command: Command = async (args, std) => {
const project: Project = {
id: projectId,
repoIds: [],
discourseServer: {serverUrl, apiUsername},
discourseServer: {serverUrl},
identities: [],
};
const taskReporter = new LoggingTaskReporter();
@ -121,7 +107,6 @@ const command: Command = async (args, std) => {
plugins,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: null,
discourseKey: Common.discourseKey(),
},
taskReporter
);

View File

@ -54,10 +54,6 @@ function usage(print: (string) => void): void {
--discourse-url DISCOURSE_URL
The url of a Discourse server to load.
--discourse-username DISCOURSE_USERNAME
The username of a Discourse account to scan from. It's recommended
to make an account called "credbot".
--help
Show this help message and exit, as 'sourcecred help scores'.
@ -83,7 +79,6 @@ function die(std, message) {
export const genProject: Command = async (args, std) => {
let projectId: string | null = null;
let discourseUrl: string | null = null;
let discourseUsername: string | null = null;
const githubSpecs: string[] = [];
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
@ -105,14 +100,6 @@ export const genProject: Command = async (args, std) => {
discourseUrl = args[i];
break;
}
case "--discourse-username": {
if (discourseUsername != undefined)
return die(std, "'--discourse-username' given multiple times");
if (++i >= args.length)
return die(std, "'--discourse-username' given without value");
discourseUsername = args[i];
break;
}
default: {
if (projectId != null) return die(std, "multiple project IDs provided");
projectId = args[i];
@ -129,7 +116,6 @@ export const genProject: Command = async (args, std) => {
const project: Project = await createProject({
projectId,
githubSpecs,
discourseUsername,
discourseUrl,
githubToken,
});
@ -141,28 +127,14 @@ export const genProject: Command = async (args, std) => {
export async function createProject(opts: {|
+projectId: string,
+githubSpecs: $ReadOnlyArray<string>,
+discourseUsername: string | null,
+discourseUrl: string | null,
+githubToken: string | null,
|}): Promise<Project> {
const {
projectId,
githubSpecs,
discourseUsername,
discourseUrl,
githubToken,
} = opts;
const {projectId, githubSpecs, discourseUrl, githubToken} = opts;
let repoIds: RepoId[] = [];
let discourseServer = null;
if (discourseUrl && discourseUsername) {
discourseServer = {serverUrl: discourseUrl, apiUsername: discourseUsername};
} else if (
(!discourseUrl && discourseUsername) ||
(discourseUrl && !discourseUsername)
) {
throw new Error(
"If either of discourseUrl and discourseUsername are provided, then both must be."
);
if (discourseUrl) {
discourseServer = {serverUrl: discourseUrl};
}
if (githubSpecs.length && githubToken == null) {
throw new Error("Provided GitHub specs without GitHub token.");

View File

@ -142,7 +142,6 @@ const loadCommand: Command = async (args, std) => {
plugins,
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken,
discourseKey: Common.discourseKey(),
};
});
// Deliberately load in serial because GitHub requests that their API not

View File

@ -29,12 +29,10 @@ describe("cli/load", () => {
});
const fakeGithubToken = "....".replace(/./g, "0123456789");
const fakeDiscourseKey = "abcdefg";
function newSourcecredDirectory() {
const dirname = tmp.dirSync().name;
process.env.SOURCECRED_DIRECTORY = dirname;
process.env.SOURCECRED_GITHUB_TOKEN = fakeGithubToken;
process.env.SOURCECRED_DISCOURSE_KEY = fakeDiscourseKey;
return dirname;
}
@ -83,7 +81,6 @@ describe("cli/load", () => {
plugins: [githubDeclaration],
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey,
};
expect(await invocation).toEqual({
exitCode: 0,
@ -109,7 +106,6 @@ describe("cli/load", () => {
plugins: [githubDeclaration],
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey,
});
expect(await invocation).toEqual({
exitCode: 0,
@ -148,7 +144,6 @@ describe("cli/load", () => {
plugins: [githubDeclaration],
sourcecredDirectory: Common.sourcecredDirectory(),
githubToken: fakeGithubToken,
discourseKey: fakeDiscourseKey,
};
expect(await invocation).toEqual({
exitCode: 0,

View File

@ -28,12 +28,14 @@ export type Project = {|
+repoIds: $ReadOnlyArray<RepoId>,
+discourseServer: {|
+serverUrl: string,
+apiUsername: string,
+apiUsername?: string,
|} | null,
+identities: $ReadOnlyArray<Identity>,
|};
const COMPAT_INFO = {type: "sourcecred/project", version: "0.3.0"};
const COMPAT_INFO = {type: "sourcecred/project", version: "0.3.1"};
const upgrades = {"0.3.0": (p) => p};
export type ProjectJSON = Compatible<Project>;
@ -42,7 +44,7 @@ export function projectToJSON(p: Project): ProjectJSON {
}
export function projectFromJSON(j: ProjectJSON): Project {
return fromCompat(COMPAT_INFO, j);
return fromCompat(COMPAT_INFO, j, upgrades);
}
/**

View File

@ -23,7 +23,7 @@ describe("core/project", () => {
const p2: Project = deepFreeze({
id: "@foo",
repoIds: [foobar, foozod],
discourseServer: {serverUrl: "https://example.com", apiUsername: "credbot"},
discourseServer: {serverUrl: "https://example.com"},
identities: [
{
username: "example",

View File

@ -28,7 +28,7 @@ describe("core/project_io", () => {
const p2: Project = deepFreeze({
id: "@foo",
repoIds: [foobar, foozod],
discourseServer: {serverUrl: "https://example.com", apiUsername: "credbot"},
discourseServer: {serverUrl: "https://example.com"},
identities: [{username: "foo", aliases: ["github/foo", "discourse/foo"]}],
});

View File

@ -3,7 +3,8 @@
exports[`plugins/discourse/fetch snapshot testing loads a particular post from snapshot 1`] = `
Object {
"authorUsername": "d11",
"cooked": "<p>This is a test post.</p>",
"cooked": "<p>This is a test post.</p>
<p>EDIT: This test post was edited on October 24, 2019.</p>",
"id": 14,
"indexWithinTopic": 1,
"replyToPostIndex": null,
@ -17,7 +18,8 @@ Object {
"posts": Array [
Object {
"authorUsername": "d11",
"cooked": "<p>This is a test post.</p>",
"cooked": "<p>This is a test post.</p>
<p>EDIT: This test post was edited on October 24, 2019.</p>",
"id": 14,
"indexWithinTopic": 1,
"replyToPostIndex": null,
@ -169,7 +171,8 @@ Here is a post reference with a slug: <a href=\\"https://sourcecred-test.discour
},
Object {
"authorUsername": "d11",
"cooked": "<p>This is a test post.</p>",
"cooked": "<p>This is a test post.</p>
<p>EDIT: This test post was edited on October 24, 2019.</p>",
"id": 14,
"indexWithinTopic": 1,
"replyToPostIndex": null,

View File

@ -131,7 +131,7 @@ export class Fetcher implements Discourse {
}
_fetch(endpoint: string): Promise<Response> {
const {serverUrl, apiKey, apiUsername} = this.options;
const {serverUrl} = this.options;
if (!endpoint.startsWith("/")) {
throw new Error(`invalid endpoint: ${endpoint}`);
}
@ -141,8 +141,6 @@ export class Fetcher implements Discourse {
const fetchOptions = {
method: "GET",
headers: {
"Api-Key": apiKey,
"Api-Username": apiUsername,
Accept: "application/json",
},
};
@ -251,12 +249,5 @@ function parseLike(json: any): LikeAction {
}
export type DiscourseFetchOptions = {|
apiKey: string,
// We'll use the view permissions for this user. It needs to be a real user
// on the server. I recommend making a new user called "credbot" with no
// special permissions for this purpose. If you use a permissioned user (e.g.
// "system") then SourceCred will pick up hidden and deleted posts,
// potentially leaking private information.
apiUsername: string,
serverUrl: string,
|};

View File

@ -77,8 +77,6 @@ describe("plugins/discourse/fetch", () => {
throw new Error("fetchOptions == null");
}
expect(fetchOptions.method).toEqual("GET");
expect(fetchOptions.headers["Api-Key"]).toEqual(options.apiKey);
expect(fetchOptions.headers["Api-Username"]).toEqual(options.apiUsername);
expect(fetchOptions.headers["Accept"]).toEqual("application/json");
});
});

View File

@ -7,8 +7,6 @@ import fs from "fs-extra";
import {Fetcher, type DiscourseFetchOptions} from "./fetch";
export const options: DiscourseFetchOptions = deepFreeze({
apiKey: "FAKE_KEY",
apiUsername: "credbot",
serverUrl: "https://sourcecred-test.discourse.group",
});

View File

@ -21,10 +21,10 @@
],
"primary_groups": [],
"topic_list": {
"can_create_topic": true,
"can_create_topic": false,
"draft": null,
"draft_key": "new_topic",
"draft_sequence": 0,
"draft_sequence": null,
"per_page": 30,
"topics": [
{
@ -40,7 +40,7 @@
"last_posted_at": "2019-10-23T18:26:31.281Z",
"bumped": true,
"bumped_at": "2019-10-23T18:26:31.281Z",
"unseen": true,
"unseen": false,
"pinned": false,
"unpinned": null,
"visible": true,
@ -48,7 +48,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 1,
"views": 9,
"like_count": 0,
"has_summary": false,
"archetype": "regular",
@ -87,7 +87,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 5,
"views": 8,
"like_count": 0,
"has_summary": false,
"archetype": "regular",
@ -126,7 +126,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 6,
"views": 9,
"like_count": 0,
"has_summary": false,
"archetype": "regular",
@ -165,7 +165,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 5,
"views": 8,
"like_count": 0,
"has_summary": false,
"archetype": "regular",
@ -204,7 +204,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 10,
"views": 13,
"like_count": 0,
"has_summary": false,
"archetype": "regular",
@ -249,7 +249,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 25,
"views": 34,
"like_count": 1,
"has_summary": false,
"archetype": "regular",
@ -295,7 +295,7 @@
"archived": false,
"bookmarked": null,
"liked": null,
"views": 5,
"views": 8,
"like_count": 0,
"has_summary": false,
"archetype": "regular",

View File

@ -13,10 +13,10 @@
"reply_count": 0,
"reply_to_post_number": null,
"quote_count": 0,
"incoming_link_count": 0,
"incoming_link_count": 1,
"reads": 1,
"readers_count": 0,
"score": 0,
"score": 5.2,
"yours": false,
"topic_id": 21,
"topic_slug": "a-post-with-references",
@ -35,32 +35,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Here is a mention: @dl-proto\nHere is a topic reference: https://sourcecred-test.discourse.group/t/123-a-post-with-numbers-in-slug/20/\nHere is a post reference with a slug: https://sourcecred-test.discourse.group/t/my-first-test-post/11/4",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -72,8 +47,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -113,32 +86,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Oh the post won't be empty I assure you.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -150,8 +98,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -191,32 +137,7 @@
"can_wiki": false,
"user_title": null,
"raw": "I'm replying to a wiki.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -228,8 +149,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -263,38 +182,13 @@
"primary_group_flair_bg_color": null,
"primary_group_flair_color": null,
"version": 3,
"can_edit": true,
"can_edit": false,
"can_delete": false,
"can_recover": false,
"can_wiki": false,
"user_title": null,
"raw": "So I'm starting the wiki content.\n\nNo remove that line.\n\nAh now I've added another line.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -307,8 +201,6 @@
"can_view_edit_history": true,
"wiki": true,
"last_wiki_edit": "2019-10-02T20:53:28.194Z",
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -348,32 +240,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Testing a poll. Let's see how it works.\n[poll type=multiple results=always min=1 max=5 public=true]\n* One\n* Two\n* Three\n* Four\n* Five\n[/poll]",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -385,8 +252,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false,
@ -501,32 +366,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Adding another post, after having deleted a whole thread.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -538,8 +378,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -579,32 +417,7 @@
"can_wiki": false,
"user_title": null,
"raw": "[quote=\"dl-proto, post:1, topic:13\"]\nHere is a link to another discourse thread:\n[/quote]\n\nExcellent link. I've quoted you.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -616,8 +429,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -657,32 +468,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Here is a link to another discourse thread: https://sourcecred-test.discourse.group/t/my-first-test-post/11/3\n\nHere is a link to a GitHub issue: https://github.com/sourcecred-test/example-github/issues/1",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": false,
"staff": false,
@ -694,8 +480,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-08-02T11:14:13.800Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -735,32 +519,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Ah, an excellent post. I will like it and reply.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": false,
"staff": false,
@ -772,8 +531,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-08-02T11:14:13.800Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -784,17 +541,17 @@
"username": "d11",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png",
"created_at": "2019-08-02T11:12:29.476Z",
"cooked": "<p>This is a test post.</p>",
"cooked": "<p>This is a test post.</p>\n<p>EDIT: This test post was edited on October 24, 2019.</p>",
"post_number": 1,
"post_type": 1,
"updated_at": "2019-08-02T11:12:29.476Z",
"updated_at": "2019-10-24T16:51:12.095Z",
"reply_count": 0,
"reply_to_post_number": null,
"quote_count": 0,
"incoming_link_count": 0,
"incoming_link_count": 1,
"reads": 2,
"readers_count": 1,
"score": 15.4,
"score": 20.4,
"yours": false,
"topic_id": 11,
"topic_slug": "my-first-test-post",
@ -806,38 +563,17 @@
"primary_group_flair_url": null,
"primary_group_flair_bg_color": null,
"primary_group_flair_color": null,
"version": 1,
"version": 2,
"can_edit": false,
"can_delete": false,
"can_recover": false,
"can_wiki": false,
"user_title": null,
"raw": "This is a test post.",
"raw": "This is a test post.\n\nEDIT: This test post was edited on October 24, 2019.",
"actions_summary": [
{
"id": 2,
"count": 1,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
"count": 1
}
],
"moderator": false,
@ -851,8 +587,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -873,7 +607,7 @@
"incoming_link_count": 0,
"reads": 2,
"readers_count": 1,
"score": 0.2,
"score": 0.4,
"yours": false,
"topic_id": 7,
"topic_slug": "welcome-to-discourse",
@ -892,28 +626,7 @@
"can_wiki": false,
"user_title": null,
"raw": "This is a test instance.\nThe first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important!\n\n**Edit this** into a brief description of your community:\n\n- Who is it for?\n- What can they find here?\n- Why should they come here?\n- Where can they read more (links, resources, etc)?\n\n<img src=\"/images/welcome/discourse-edit-post-animated.gif\" width=\"508\" height=\"106\">\n\nYou may want to close this topic via the admin :wrench: (at the upper right and bottom), so that replies don't pile up on an announcement.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": true,
"admin": true,
"staff": true,
@ -925,8 +638,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:20:06.287Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -966,28 +677,7 @@
"can_wiki": false,
"user_title": null,
"raw": "Discussion about this site, its organization, how it works, and how we can improve it.",
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": true,
"admin": true,
"staff": true,
@ -999,8 +689,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:20:06.287Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false

View File

@ -4,17 +4,17 @@
"username": "d11",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png",
"created_at": "2019-08-02T11:12:29.476Z",
"cooked": "<p>This is a test post.</p>",
"cooked": "<p>This is a test post.</p>\n<p>EDIT: This test post was edited on October 24, 2019.</p>",
"post_number": 1,
"post_type": 1,
"updated_at": "2019-08-02T11:12:29.476Z",
"updated_at": "2019-10-24T16:51:12.095Z",
"reply_count": 0,
"reply_to_post_number": null,
"quote_count": 0,
"incoming_link_count": 0,
"incoming_link_count": 1,
"reads": 2,
"readers_count": 1,
"score": 15.4,
"score": 20.4,
"yours": false,
"topic_id": 11,
"topic_slug": "my-first-test-post",
@ -23,38 +23,17 @@
"primary_group_flair_url": null,
"primary_group_flair_bg_color": null,
"primary_group_flair_color": null,
"version": 1,
"version": 2,
"can_edit": false,
"can_delete": false,
"can_recover": false,
"can_wiki": false,
"user_title": null,
"raw": "This is a test post.",
"raw": "This is a test post.\n\nEDIT: This test post was edited on October 24, 2019.",
"actions_summary": [
{
"id": 2,
"count": 1,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
"count": 1
}
],
"moderator": false,
@ -68,8 +47,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false

View File

@ -7,17 +7,17 @@
"username": "d11",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png",
"created_at": "2019-08-02T11:12:29.476Z",
"cooked": "<p>This is a test post.</p>",
"cooked": "<p>This is a test post.</p>\n<p>EDIT: This test post was edited on October 24, 2019.</p>",
"post_number": 1,
"post_type": 1,
"updated_at": "2019-08-02T11:12:29.476Z",
"updated_at": "2019-10-24T16:51:12.095Z",
"reply_count": 0,
"reply_to_post_number": null,
"quote_count": 0,
"incoming_link_count": 0,
"incoming_link_count": 1,
"reads": 2,
"readers_count": 1,
"score": 15.4,
"score": 20.4,
"yours": false,
"topic_id": 11,
"topic_slug": "my-first-test-post",
@ -26,38 +26,17 @@
"primary_group_flair_url": null,
"primary_group_flair_bg_color": null,
"primary_group_flair_color": null,
"version": 1,
"version": 2,
"can_edit": false,
"can_delete": false,
"can_recover": false,
"can_wiki": false,
"read": false,
"read": true,
"user_title": null,
"actions_summary": [
{
"id": 2,
"count": 1,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
"count": 1
}
],
"moderator": false,
@ -71,8 +50,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -107,34 +84,9 @@
"can_delete": false,
"can_recover": false,
"can_wiki": false,
"read": false,
"read": true,
"user_title": null,
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": false,
"staff": false,
@ -146,8 +98,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-08-02T11:14:13.800Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -182,34 +132,9 @@
"can_delete": false,
"can_recover": false,
"can_wiki": false,
"read": false,
"read": true,
"user_title": null,
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -221,8 +146,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -237,11 +160,11 @@
"timeline_lookup": [
[
1,
82
89
],
[
3,
79
86
]
],
"suggested_topics": [
@ -258,7 +181,7 @@
"last_posted_at": "2019-10-23T18:26:31.281Z",
"bumped": true,
"bumped_at": "2019-10-23T18:26:31.281Z",
"unseen": true,
"unseen": false,
"pinned": false,
"unpinned": null,
"visible": true,
@ -268,7 +191,7 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 1,
"views": 9,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -308,7 +231,47 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 5,
"views": 8,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
{
"extras": "latest single",
"description": "Original Poster, Most Recent Poster",
"user": {
"id": 2,
"username": "d11",
"name": "D",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png"
}
}
]
},
{
"id": 18,
"title": "Test Poll Please Ignore",
"fancy_title": "Test Poll Please Ignore",
"slug": "test-poll-please-ignore",
"posts_count": 1,
"reply_count": 0,
"highest_post_number": 1,
"image_url": null,
"created_at": "2019-10-02T20:50:10.568Z",
"last_posted_at": "2019-10-02T20:50:10.625Z",
"bumped": true,
"bumped_at": "2019-10-02T20:50:10.625Z",
"unseen": false,
"pinned": false,
"unpinned": null,
"visible": true,
"closed": false,
"archived": false,
"bookmarked": null,
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 8,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -348,7 +311,7 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 6,
"views": 9,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -389,7 +352,7 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 5,
"views": 8,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -405,56 +368,6 @@
}
}
]
},
{
"id": 13,
"title": "A Thread With Links",
"fancy_title": "A Thread With Links",
"slug": "a-thread-with-links",
"posts_count": 2,
"reply_count": 0,
"highest_post_number": 2,
"image_url": null,
"created_at": "2019-08-02T11:15:53.843Z",
"last_posted_at": "2019-08-02T11:16:31.313Z",
"bumped": true,
"bumped_at": "2019-08-02T11:17:12.755Z",
"unseen": false,
"pinned": false,
"unpinned": null,
"visible": true,
"closed": false,
"archived": false,
"bookmarked": null,
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 10,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
{
"extras": "latest",
"description": "Original Poster, Most Recent Poster",
"user": {
"id": 3,
"username": "dl-proto",
"name": "dandelion protocol acct",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/8797f3/{size}.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
"id": 2,
"username": "d11",
"name": "D",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png"
}
}
]
}
],
"id": 11,
@ -462,7 +375,7 @@
"fancy_title": "My First Test Post",
"posts_count": 3,
"created_at": "2019-08-02T11:12:29.408Z",
"views": 25,
"views": 34,
"reply_count": 0,
"like_count": 1,
"last_posted_at": "2019-08-04T20:50:39.667Z",
@ -473,7 +386,7 @@
"archetype": "regular",
"slug": "my-first-test-post",
"category_id": 1,
"word_count": 24,
"word_count": 34,
"deleted_at": null,
"user_id": 2,
"featured_link": null,
@ -483,7 +396,7 @@
"image_url": null,
"draft": null,
"draft_key": "topic_11",
"draft_sequence": 0,
"draft_sequence": null,
"unpinned": null,
"pinned": false,
"current_post_number": 1,
@ -494,34 +407,30 @@
"id": 4,
"count": 0,
"hidden": false,
"can_act": true
"can_act": false
},
{
"id": 8,
"count": 0,
"hidden": false,
"can_act": true
"can_act": false
},
{
"id": 7,
"count": 0,
"hidden": false,
"can_act": true
"can_act": false
}
],
"chunk_size": 20,
"bookmarked": null,
"topic_timer": null,
"private_topic_timer": null,
"message_bus_last_id": 0,
"message_bus_last_id": 1,
"participant_count": 2,
"show_read_indicator": false,
"tags_disable_ads": false,
"details": {
"notification_level": 1,
"can_create_post": true,
"can_reply_as_new_topic": true,
"can_flag_topic": true,
"participants": [
{
"id": 2,

View File

@ -14,10 +14,10 @@
"reply_count": 0,
"reply_to_post_number": null,
"quote_count": 0,
"incoming_link_count": 0,
"incoming_link_count": 1,
"reads": 1,
"readers_count": 0,
"score": 0,
"score": 5.2,
"yours": false,
"topic_id": 21,
"topic_slug": "a-post-with-references",
@ -47,34 +47,9 @@
"clicks": 0
}
],
"read": false,
"read": true,
"user_title": null,
"actions_summary": [
{
"id": 2,
"can_act": true
},
{
"id": 3,
"can_act": true
},
{
"id": 4,
"can_act": true
},
{
"id": 8,
"can_act": true
},
{
"id": 6,
"can_act": true
},
{
"id": 7,
"can_act": true
}
],
"actions_summary": [],
"moderator": false,
"admin": true,
"staff": true,
@ -86,8 +61,6 @@
"edit_reason": null,
"can_view_edit_history": true,
"wiki": false,
"user_created_at": "2019-04-02T20:22:14.312Z",
"user_date_of_birth": null,
"can_accept_answer": false,
"can_unaccept_answer": false,
"accepted_answer": false
@ -100,7 +73,7 @@
"timeline_lookup": [
[
1,
0
7
]
],
"suggested_topics": [
@ -127,7 +100,7 @@
"liked": null,
"archetype": "regular",
"like_count": 1,
"views": 26,
"views": 34,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -177,7 +150,47 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 5,
"views": 8,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
{
"extras": "latest single",
"description": "Original Poster, Most Recent Poster",
"user": {
"id": 2,
"username": "d11",
"name": "D",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png"
}
}
]
},
{
"id": 18,
"title": "Test Poll Please Ignore",
"fancy_title": "Test Poll Please Ignore",
"slug": "test-poll-please-ignore",
"posts_count": 1,
"reply_count": 0,
"highest_post_number": 1,
"image_url": null,
"created_at": "2019-10-02T20:50:10.568Z",
"last_posted_at": "2019-10-02T20:50:10.625Z",
"bumped": true,
"bumped_at": "2019-10-02T20:50:10.625Z",
"unseen": false,
"pinned": false,
"unpinned": null,
"visible": true,
"closed": false,
"archived": false,
"bookmarked": null,
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 8,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -217,7 +230,7 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 6,
"views": 9,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -258,7 +271,7 @@
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 5,
"views": 8,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
@ -274,46 +287,6 @@
}
}
]
},
{
"id": 18,
"title": "Test Poll Please Ignore",
"fancy_title": "Test Poll Please Ignore",
"slug": "test-poll-please-ignore",
"posts_count": 1,
"reply_count": 0,
"highest_post_number": 1,
"image_url": null,
"created_at": "2019-10-02T20:50:10.568Z",
"last_posted_at": "2019-10-02T20:50:10.625Z",
"bumped": true,
"bumped_at": "2019-10-02T20:50:10.625Z",
"unseen": false,
"pinned": false,
"unpinned": null,
"visible": true,
"closed": false,
"archived": false,
"bookmarked": null,
"liked": null,
"archetype": "regular",
"like_count": 0,
"views": 5,
"category_id": 1,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
{
"extras": "latest single",
"description": "Original Poster, Most Recent Poster",
"user": {
"id": 2,
"username": "d11",
"name": "D",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png"
}
}
]
}
],
"id": 21,
@ -321,7 +294,7 @@
"fancy_title": "A Post With References",
"posts_count": 1,
"created_at": "2019-10-23T18:26:31.140Z",
"views": 1,
"views": 9,
"reply_count": 0,
"like_count": 0,
"last_posted_at": "2019-10-23T18:26:31.281Z",
@ -342,7 +315,7 @@
"image_url": null,
"draft": null,
"draft_key": "topic_21",
"draft_sequence": 0,
"draft_sequence": null,
"unpinned": null,
"pinned": false,
"current_post_number": 1,
@ -353,34 +326,30 @@
"id": 4,
"count": 0,
"hidden": false,
"can_act": true
"can_act": false
},
{
"id": 8,
"count": 0,
"hidden": false,
"can_act": true
"can_act": false
},
{
"id": 7,
"count": 0,
"hidden": false,
"can_act": true
"can_act": false
}
],
"chunk_size": 20,
"bookmarked": null,
"topic_timer": null,
"private_topic_timer": null,
"message_bus_last_id": 2,
"message_bus_last_id": 0,
"participant_count": 1,
"show_read_indicator": false,
"tags_disable_ads": false,
"details": {
"notification_level": 1,
"can_create_post": true,
"can_reply_as_new_topic": true,
"can_flag_topic": true,
"participants": [
{
"id": 2,

View File

@ -1,7 +1,7 @@
{
"user_actions": [
{
"excerpt": "This is a test post.",
"excerpt": "This is a test post. \nEDIT: This test post was edited on October 24, 2019.",
"action_type": 1,
"created_at": "2019-08-02T11:14:58.189Z",
"avatar_template": "https://avatars.discourse.org/v4/letter/d/47e85d/{size}.png",

View File

@ -6,12 +6,6 @@ snapshots_dir=src/plugins/discourse/snapshots
test_instance_url="https://sourcecred-test.discourse.group"
test_instance_username="credbot"
if [ -z "${DISCOURSE_TEST_API_KEY:-}" ]; then
printf >&2 'Please set the DISCOURSE_TEST_API_KEY environment variable.\n'
printf >&2 'Contact the SourceCred maintainers to get the key.\n'
exit 1
fi
if [ ! "$(jq --version)" ]; then
printf >&2 'This script depends on jq. Please install it.\n'
exit 1
@ -25,8 +19,6 @@ fetch() {
filename="$(printf '%s' "${url}" | base64 -w 0 | tr -d '=' | tr '/+' '_-')"
path="${snapshots_dir}/${filename}"
curl -sfL "$url" \
-H "Api-Key: ${DISCOURSE_TEST_API_KEY}" \
-H "Api-Username: ${test_instance_username}" \
-H "Accept: application/json" \
| jq '.' > "${path}"
}