From eea4599addd5713d94b462804b79f9619c7233d7 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 18 Jan 2023 11:30:34 +0000 Subject: [PATCH 1/2] fetch status context instead of providing an empty one --- .../[accountId]/[statusId]/index.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx index 66a5636..0b58778 100644 --- a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx +++ b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx @@ -4,6 +4,7 @@ import Status from '~/components/Status' import { formatDateTime } from '~/utils/dateTime' import { formatRoundedNumber } from '~/utils/numbers' import * as statusAPI from 'wildebeest/functions/api/v1/statuses/[id]' +import * as contextAPI from 'wildebeest/functions/api/v1/statuses/[id]/context' import { Link, loader$ } from '@builder.io/qwik-city' import StickyHeader from '~/components/StickyHeader/StickyHeader' import { Avatar } from '~/components/avatar' @@ -11,13 +12,20 @@ import { Avatar } from '~/components/avatar' export const statusLoader = loader$< { DATABASE: D1Database; domain: string }, Promise<{ status: MastodonStatus; context: StatusContext }> ->(async ({ redirect, platform, params }) => { - const response = await statusAPI.handleRequest(platform.DATABASE, params.statusId) - const results = await response.text() - if (!results) { +>(async ({ request, redirect, platform, params }) => { + const domain = new URL(request.url).hostname + const statusResponse = await statusAPI.handleRequest(platform.DATABASE, params.statusId, domain) + const statusText = await statusResponse.text() + if (!statusText) { throw redirect(303, '/not-found') } - return { status: JSON.parse(results), context: { ancestors: [], descendants: [] } } + const contextResponse = await contextAPI.handleRequest(domain, platform.DATABASE, params.statusId) + const contextText = await contextResponse.text() + const context = JSON.parse(contextText ?? null) as StatusContext | null + if (!context) { + throw new Error(`No context present for status with ${params.statusId}`) + } + return { status: JSON.parse(statusText), context } }) export default component$(() => { From e464fe2bcdc86d86cf3fd2f81953cff313b46f13 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 18 Jan 2023 13:40:09 +0000 Subject: [PATCH 2/2] add replies to dummy data and test them in the ui tests as part of this change, refactor the ui tests so that they use lower level backend apis instead of simulating http requests (this allows us to mock replies as well) --- frontend/.eslintrc.cjs | 2 +- frontend/mock-db/init.ts | 108 +++++++++++++++++----------- frontend/src/dummyData.tsx | 116 +++++++++++++++++++++++++++++++ frontend/test/posts-page.spec.ts | 4 ++ 4 files changed, 189 insertions(+), 41 deletions(-) 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!') }) })