Merge pull request #132 from cloudflare/replies

fetch status context instead of providing an empty one
pull/142/head
Dario Piotrowicz 2023-01-19 11:31:33 +00:00 zatwierdzone przez GitHub
commit f1ade0233d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
5 zmienionych plików z 202 dodań i 46 usunięć

Wyświetl plik

@ -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',
},

Wyświetl plik

@ -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<Person> {
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 }
}

Wyświetl plik

@ -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',

Wyświetl plik

@ -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$(() => {

Wyświetl plik

@ -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!')
})
})