kopia lustrzana https://github.com/cloudflare/wildebeest
Merge pull request #132 from cloudflare/replies
fetch status context instead of providing an empty onepull/142/head
commit
f1ade0233d
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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$(() => {
|
||||
|
|
|
@ -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!')
|
||||
})
|
||||
})
|
||||
|
|
Ładowanie…
Reference in New Issue