diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 51fc5ee..eca306a 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -30,7 +30,7 @@ module.exports = { '@typescript-eslint/ban-ts-comment': 'error', 'prefer-spread': 'error', 'no-case-declarations': 'error', - 'no-console': 'error', + 'no-console': ['error', { allow: ['warn', 'error']} ], '@typescript-eslint/no-unused-vars': ['error'], 'prefer-const': 'error', }, diff --git a/frontend/mock-db/init.ts b/frontend/mock-db/init.ts index c938fa9..489df7b 100644 --- a/frontend/mock-db/init.ts +++ b/frontend/mock-db/init.ts @@ -1,57 +1,71 @@ import { createPerson, getPersonByEmail, type Person } from 'wildebeest/backend/src/activitypub/actors' -import * as statusesAPI from 'wildebeest/functions/api/v1/statuses' -import * as reblogAPI from 'wildebeest/functions/api/v1/statuses/[id]/reblog' -import { statuses } from 'wildebeest/frontend/src/dummyData' +import { replies, statuses } from 'wildebeest/frontend/src/dummyData' import type { Account, MastodonStatus } from 'wildebeest/frontend/src/types' - -const kek = 'test-kek' -/* eslint-disable @typescript-eslint/no-empty-function */ -const queue: unknown = { - send() {}, - sendBatch() {}, -} -/* eslint-disable @typescript-eslint/no-empty-function */ -const cache: unknown = { - get() {}, - put() {}, -} +import { createPublicNote, Note } from 'wildebeest/backend/src/activitypub/objects/note' +import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' +import { insertReblog } from 'wildebeest/backend/src/mastodon/reblog' +import { insertReply } from 'wildebeest/backend/src/mastodon/reply' /** * Run helper commands to initialize the database with actors, statuses, etc. */ export async function init(domain: string, db: D1Database) { - const loadedStatuses: MastodonStatus[] = [] + const loadedStatuses: { status: MastodonStatus; note: Note }[] = [] for (const status of statuses) { const actor = await getOrCreatePerson(domain, db, status.account) - loadedStatuses.push(await createStatus(db, actor, status.content)) + const note = await createStatus(domain, db, actor, status.content) + loadedStatuses.push({ status, note }) } - // Grab the account from an arbitrary status to use as the reblogger - const rebloggerAccount = loadedStatuses[1].account - const reblogger = await getOrCreatePerson(domain, db, rebloggerAccount) - // Reblog an arbitrary status with this reblogger - const statusToReblog = loadedStatuses[2] - await reblogStatus(db, reblogger, statusToReblog, domain) + const { reblogger, noteToReblog } = await pickReblogDetails(loadedStatuses, domain, db) + reblogNote(db, reblogger, noteToReblog) + + for (const reply of replies) { + await createReply(domain, db, reply, loadedStatuses) + } } /** - * Create a status object in the given actors outbox. + * Create a status object in the given actor's outbox. */ -async function createStatus(db: D1Database, actor: Person, status: string, visibility = 'public') { - const body = { - status, - visibility, +async function createStatus(domain: string, db: D1Database, actor: Person, content: string) { + const note = await createPublicNote(domain, db, content, actor) + await addObjectInOutbox(db, actor, note) + return note +} + +/** + * Reblogs a note (representing a status) + */ +async function reblogNote(db: D1Database, reblogger: Person, noteToReblog: Note) { + await addObjectInOutbox(db, reblogger, noteToReblog) + await insertReblog(db, reblogger, noteToReblog) +} + +/** + * Creates a reply for a note (representing a status) + */ +async function createReply( + domain: string, + db: D1Database, + reply: MastodonStatus, + loadedStatuses: { status: MastodonStatus; note: Note }[] +) { + if (!reply.in_reply_to_id) { + console.warn(`Ignoring reply with id ${reply.id} since it doesn't have a in_reply_to_id field`) + return } - const headers = { - 'content-type': 'application/json', + + const originalStatus = loadedStatuses.find(({ status: { id } }) => id === reply.in_reply_to_id) + if (!originalStatus) { + console.warn(`Ignoring reply since no status matching the in_reply_to_id ${reply.id} has been found`) + return } - const req = new Request('https://example.com', { - method: 'POST', - headers, - body: JSON.stringify(body), - }) - const resp = await statusesAPI.handleRequest(req, db, actor, kek, queue, cache) - return (await resp.json()) as MastodonStatus + + const inReplyTo = originalStatus.note.mastodonId + const actor = await getOrCreatePerson(domain, db, reply.account) + const replyNote = await createPublicNote(domain, db, reply.content, actor, [], { inReplyTo }) + await insertReply(db, actor, replyNote, originalStatus.note) } async function getOrCreatePerson( @@ -61,7 +75,7 @@ async function getOrCreatePerson( ): Promise { const person = await getPersonByEmail(db, username) if (person) return person - const newPerson = await createPerson(domain, db, kek, username, { + const newPerson = await createPerson(domain, db, 'test-kek', username, { icon: { url: avatar }, name: display_name, }) @@ -71,6 +85,20 @@ async function getOrCreatePerson( return newPerson } -async function reblogStatus(db: D1Database, actor: Person, status: MastodonStatus, domain: string) { - await reblogAPI.handleRequest(db, status.id, actor, kek, queue, domain) +/** + * Picks the details to use to reblog an arbitrary note/status. + * + * Both the note/status and the reblogger are picked arbitrarily + * form a list of available notes/states (respectively from the first + * and second entries). + */ +async function pickReblogDetails( + loadedStatuses: { status: MastodonStatus; note: Note }[], + domain: string, + db: D1Database +) { + const rebloggerAccount = loadedStatuses[1].status.account + const reblogger = await getOrCreatePerson(domain, db, rebloggerAccount) + const noteToReblog = loadedStatuses[2].note + return { reblogger, noteToReblog } } diff --git a/frontend/src/dummyData.tsx b/frontend/src/dummyData.tsx index 44035e6..b0b7f06 100644 --- a/frontend/src/dummyData.tsx +++ b/frontend/src/dummyData.tsx @@ -1557,6 +1557,122 @@ export const statuses: MastodonStatus[] = [ }, ] +export const replies: MastodonStatus[] = [ + { + id: '209630407170172986', + created_at: '2023-01-04T17:14:17.855Z', + in_reply_to_id: '109630407170172986', + in_reply_to_account_id: null, + sensitive: false, + spoiler_text: '', + visibility: 'public', + language: 'en', + uri: 'https://mastodon.social/users/ZachWeinersmith/statuses/209630407170172986', + url: 'https://mastodon.social/@ZachWeinersmith/209630407170172986', + replies_count: 52, + reblogs_count: 630, + favourites_count: 1434, + edited_at: '2023-01-04T10:15:59.795Z', + content: '\u003cp\u003e Yes we did! \u003c/p\u003e', + reblog: null, + application: { name: 'Web', website: null }, + account: { + id: '109521032613939160', + username: 'ZachWeinersmith', + acct: 'ZachWeinersmith', + display_name: 'Zach Weinersmith', + locked: false, + bot: false, + discoverable: true, + group: false, + created_at: '2022-12-16T00:00:00.000Z', + note: '\u003cp\u003eThe SMBC guy. Co-author of Soonish, illustrator of Open Borders, scop of Bea Wolf.\u003c/p\u003e', + url: 'https://mastodon.social/@ZachWeinersmith', + avatar: 'https://files.mastodon.social/accounts/avatars/109/521/032/613/939/160/original/6d6588cad807dfbc.png', + avatar_static: + 'https://files.mastodon.social/accounts/avatars/109/521/032/613/939/160/original/6d6588cad807dfbc.png', + header: 'https://files.mastodon.social/accounts/headers/109/521/032/613/939/160/original/8368e47ebdb7f00f.jpg', + header_static: + 'https://files.mastodon.social/accounts/headers/109/521/032/613/939/160/original/8368e47ebdb7f00f.jpg', + followers_count: 4112, + following_count: 514, + statuses_count: 142, + last_status_at: '2023-01-04', + noindex: false, + emojis: [], + fields: [], + }, + media_attachments: [], + mentions: [], + tags: [], + emojis: [], + card: null, + poll: null, + }, + { + id: '309630407170172986', + created_at: '2023-01-04T22:14:17.855Z', + in_reply_to_id: '109630407170172986', + in_reply_to_account_id: null, + sensitive: false, + spoiler_text: '', + visibility: 'public', + language: 'en', + uri: 'https://mastodon.social/users/ZachWeinersmith/statuses/209630407170172986', + url: 'https://mastodon.social/@ZachWeinersmith/209630407170172986', + replies_count: 52, + reblogs_count: 630, + favourites_count: 1434, + edited_at: '2023-01-04T10:15:59.795Z', + content: '\u003cp\u003e Yes you guys did it! \u003c/p\u003e', + reblog: null, + application: { name: 'Web', website: null }, + account: { + id: '38659', + username: 'nixCraft', + acct: 'nixCraft', + display_name: 'nixCraft 🐧', + locked: false, + bot: false, + discoverable: true, + group: false, + created_at: '2017-04-03T00:00:00.000Z', + note: '\u003cp\u003eEnjoy \u003ca href="https://mastodon.social/tags/Linux" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eLinux\u003c/span\u003e\u003c/a\u003e, \u003ca href="https://mastodon.social/tags/macOS" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003emacOS\u003c/span\u003e\u003c/a\u003e, \u003ca href="https://mastodon.social/tags/FreeBSD" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eFreeBSD\u003c/span\u003e\u003c/a\u003e \u0026amp; \u003ca href="https://mastodon.social/tags/Unix" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eUnix\u003c/span\u003e\u003c/a\u003e like systems? \u003ca href="https://mastodon.social/tags/Opensource" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eOpensource\u003c/span\u003e\u003c/a\u003e software \u0026amp; \u003ca href="https://mastodon.social/tags/programming" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eprogramming\u003c/span\u003e\u003c/a\u003e? Enjoy \u003ca href="https://mastodon.social/tags/Sysadmin" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eSysadmin\u003c/span\u003e\u003c/a\u003e \u0026amp; \u003ca href="https://mastodon.social/tags/DevOps" class="mention hashtag" rel="tag"\u003e#\u003cspan\u003eDevOps\u003c/span\u003e\u003c/a\u003e work? Follow us to make the most of your geeky IT career.\u003c/p\u003e', + url: 'https://mastodon.social/@nixCraft', + avatar: 'https://files.mastodon.social/accounts/avatars/000/038/659/original/b0de8058da52f2aa.jpg', + avatar_static: 'https://files.mastodon.social/accounts/avatars/000/038/659/original/b0de8058da52f2aa.jpg', + header: 'https://files.mastodon.social/accounts/headers/000/038/659/original/6c4d08cfd6c28163.png', + header_static: 'https://files.mastodon.social/accounts/headers/000/038/659/original/6c4d08cfd6c28163.png', + followers_count: 14151, + following_count: 13, + statuses_count: 824, + last_status_at: '2023-01-04', + noindex: false, + emojis: [], + fields: [ + { + name: 'Linux blog', + value: + '\u003ca href="https://www.cyberciti.biz" target="_blank" rel="nofollow noopener noreferrer me"\u003e\u003cspan class="invisible"\u003ehttps://www.\u003c/span\u003e\u003cspan class=""\u003ecyberciti.biz\u003c/span\u003e\u003cspan class="invisible"\u003e\u003c/span\u003e\u003c/a\u003e', + verified_at: '2022-11-21T11:10:53.864+00:00', + }, + { + name: 'Q \u0026 A forum', + value: + '\u003ca href="https://www.nixcraft.com" target="_blank" rel="nofollow noopener noreferrer me"\u003e\u003cspan class="invisible"\u003ehttps://www.\u003c/span\u003e\u003cspan class=""\u003enixcraft.com\u003c/span\u003e\u003cspan class="invisible"\u003e\u003c/span\u003e\u003c/a\u003e', + verified_at: '2022-12-18T08:41:11.287+00:00', + }, + ], + }, + media_attachments: [], + mentions: [], + tags: [], + emojis: [], + card: null, + poll: null, + }, +] + export const tags: TagDetails[] = [ { name: 'wintersolstice', diff --git a/frontend/test/posts-page.spec.ts b/frontend/test/posts-page.spec.ts index 622f56d..45abf5f 100644 --- a/frontend/test/posts-page.spec.ts +++ b/frontend/test/posts-page.spec.ts @@ -29,6 +29,10 @@ describe('Toot details', () => { const response = await fetch(`http://0.0.0.0:6868/${tootPath}`) expect(response.status).toBe(200) const body = await response.text() + // validate the toot content itself expect(body).toContain('We did it!') + // validate replies + expect(body).toContain('Yes we did!') + expect(body).toContain('Yes you guys did it!') }) })