From 178a3c4d0e488dba74ab21c2b5686f598dedcc4d Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 5 Apr 2024 19:44:12 -0300 Subject: [PATCH 1/8] feat: repost functionality --- src/app.ts | 2 ++ src/controllers/api/statuses.ts | 28 +++++++++++++++++++++++++++- src/controllers/api/timelines.ts | 13 +++++++++---- src/views/mastodon/statuses.ts | 14 +++++++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index 3866760..743620a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -62,6 +62,7 @@ import { favouritedByController, pinController, rebloggedByController, + reblogStatusController, statusController, unbookmarkController, unpinController, @@ -167,6 +168,7 @@ app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unbookmark', requirePubkey, unbookm app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/pin', requirePubkey, pinController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/unpin', requirePubkey, unpinController); app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/zap', requirePubkey, zapController); +app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/reblog', requirePubkey, reblogStatusController); app.post('/api/v1/statuses', requirePubkey, createStatusController); app.delete('/api/v1/statuses/:id{[0-9a-f]{64}}', requirePubkey, deleteStatusController); diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 0b4216e..7abd3e5 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -7,7 +7,7 @@ import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { addTag, deleteTag } from '@/tags.ts'; import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; import { renderEventAccounts } from '@/views.ts'; -import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { getLnurl } from '@/utils/lnurl.ts'; const createStatusSchema = z.object({ @@ -173,6 +173,31 @@ const favouritedByController: AppController = (c) => { return renderEventAccounts(c, [{ kinds: [7], '#e': [id], ...params }]); }; +/** https://docs.joinmastodon.org/methods/statuses/#boost */ +const reblogStatusController: AppController = async (c) => { + const eventId = c.req.param('id'); + + const event = await getEvent(eventId, { + kind: 1, + }); + + if (event == undefined) { + return c.json({ error: 'Event not found.' }, 404); + } + + const tags: string[][] = [['e', event.id], ['p', event.pubkey]]; + + const reblogEvent = await createEvent({ + kind: 6, + content: JSON.stringify(event), + tags, + }, c); + + const status = await renderReblog(reblogEvent, reblogEvent.pubkey); + + return c.json(status); +}; + const rebloggedByController: AppController = (c) => { const id = c.req.param('id'); const params = paginationSchema.parse(c.req.query()); @@ -339,6 +364,7 @@ export { favouriteController, favouritedByController, pinController, + reblogStatusController, rebloggedByController, statusController, unbookmarkController, diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index e633ce3..9dd0739 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -7,13 +7,13 @@ import { booleanParamSchema } from '@/schema.ts'; import { eventsDB } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; -import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const homeTimelineController: AppController = async (c) => { const params = paginationSchema.parse(c.req.query()); const pubkey = c.get('pubkey')!; const authors = await getFeedPubkeys(pubkey); - return renderStatuses(c, [{ authors, kinds: [1], ...params }]); + return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]); }; const publicQuerySchema = z.object({ @@ -25,7 +25,7 @@ const publicTimelineController: AppController = (c) => { const params = paginationSchema.parse(c.req.query()); const { local, instance } = publicQuerySchema.parse(c.req.query()); - const filter: NostrFilter = { kinds: [1], ...params }; + const filter: NostrFilter = { kinds: [1, 6], ...params }; if (local) { filter.search = `domain:${Conf.url.host}`; @@ -56,7 +56,12 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { return c.json([]); } - const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events.map((event) => { + if (event.kind == 6) { + return renderReblog(event, c.get('pubkey')); + } + return renderStatus(event, c.get('pubkey')); + })); return paginated(c, events, statuses); } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 172a2b6..408d360 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -98,6 +98,18 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) { }; } +async function renderReblog(event: DittoEvent, viewerPubkey?: string) { + if (event.author == undefined) return; + + const reblog = await renderStatus(JSON.parse(event.content), viewerPubkey); + return { + id: event.id, + account: await renderAccount(event.author), + reblogged: true, + reblog, + }; +} + async function toMention(pubkey: string) { const author = await getAuthor(pubkey); const account = author ? await renderAccount(author) : undefined; @@ -134,4 +146,4 @@ function buildInlineRecipients(mentions: Mention[]): string { return `${elements.join(' ')} `; } -export { renderStatus }; +export { renderStatus, renderReblog }; From bd5899029e597de1e45818126c990688519889ab Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 5 Apr 2024 19:52:29 -0300 Subject: [PATCH 2/8] refactor: deno fmt --- src/controllers/api/statuses.ts | 2 +- src/views/mastodon/statuses.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 7abd3e5..c81dedd 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -364,8 +364,8 @@ export { favouriteController, favouritedByController, pinController, - reblogStatusController, rebloggedByController, + reblogStatusController, statusController, unbookmarkController, unpinController, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 408d360..cafd71e 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -146,4 +146,4 @@ function buildInlineRecipients(mentions: Mention[]): string { return `${elements.join(' ')} `; } -export { renderStatus, renderReblog }; +export { renderReblog, renderStatus }; From ba08958b30136885a2854fec0c3488fe4ae88a2a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 6 Apr 2024 22:32:39 -0300 Subject: [PATCH 3/8] feat: attempt to delete repost if original post is deleted --- src/controllers/api/statuses.ts | 5 ++--- src/controllers/api/timelines.ts | 4 ++-- src/interfaces/DittoEvent.ts | 1 + src/views/mastodon/statuses.ts | 19 ++++++++++++++----- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index c81dedd..be8dbd9 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -181,7 +181,7 @@ const reblogStatusController: AppController = async (c) => { kind: 1, }); - if (event == undefined) { + if (!event) { return c.json({ error: 'Event not found.' }, 404); } @@ -189,11 +189,10 @@ const reblogStatusController: AppController = async (c) => { const reblogEvent = await createEvent({ kind: 6, - content: JSON.stringify(event), tags, }, c); - const status = await renderReblog(reblogEvent, reblogEvent.pubkey); + const status = await renderReblog(reblogEvent); return c.json(status); }; diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 9dd0739..78a1fa9 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -57,8 +57,8 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { } const statuses = await Promise.all(events.map((event) => { - if (event.kind == 6) { - return renderReblog(event, c.get('pubkey')); + if (event.kind === 6) { + return renderReblog(event) } return renderStatus(event, c.get('pubkey')); })); diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index cdd4002..ca38a42 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -22,4 +22,5 @@ export interface DittoEvent extends NostrEvent { event_stats?: EventStats; d_author?: DittoEvent; user?: DittoEvent; + repost?: NostrEvent; } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index cafd71e..b4126e1 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -4,7 +4,7 @@ import { Conf } from '@/config.ts'; import { nip19 } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; -import { getAuthor } from '@/queries.ts'; +import { getAuthor, getEvent } from '@/queries.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; import { eventsDB } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; @@ -98,15 +98,24 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) { }; } -async function renderReblog(event: DittoEvent, viewerPubkey?: string) { - if (event.author == undefined) return; +async function renderReblog(event: DittoEvent) { + if (!event.author) return; - const reblog = await renderStatus(JSON.parse(event.content), viewerPubkey); + for (let i = 0; i < event.tags.length; i++) { + if (event.tags[i][0] === 'e') { + event.repost = await getEvent(event.tags[i][1], {kind:1}) + break + } + } + + if (!event.repost) return; + + const reblog = await renderStatus(event.repost); return { id: event.id, account: await renderAccount(event.author), reblogged: true, - reblog, + reblog }; } From 25fcd04d9f2619036165cf6ab68f69e07f908b80 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 6 Apr 2024 23:12:30 -0300 Subject: [PATCH 4/8] fix: deno fmt --- src/controllers/api/timelines.ts | 2 +- src/views/mastodon/statuses.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 78a1fa9..a2c4367 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -58,7 +58,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { const statuses = await Promise.all(events.map((event) => { if (event.kind === 6) { - return renderReblog(event) + return renderReblog(event); } return renderStatus(event, c.get('pubkey')); })); diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index b4126e1..e01e955 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -101,12 +101,12 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) { async function renderReblog(event: DittoEvent) { if (!event.author) return; - for (let i = 0; i < event.tags.length; i++) { - if (event.tags[i][0] === 'e') { - event.repost = await getEvent(event.tags[i][1], {kind:1}) - break - } - } + for (let i = 0; i < event.tags.length; i++) { + if (event.tags[i][0] === 'e') { + event.repost = await getEvent(event.tags[i][1], { kind: 1 }); + break; + } + } if (!event.repost) return; @@ -115,7 +115,7 @@ async function renderReblog(event: DittoEvent) { id: event.id, account: await renderAccount(event.author), reblogged: true, - reblog + reblog, }; } From 25b1e7229cc5289b550e59b0fd9c50d16383e449 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sun, 7 Apr 2024 20:26:47 -0300 Subject: [PATCH 5/8] fix: stop home timeline hanging/loading when no posts are available --- src/controllers/api/timelines.ts | 9 +++++++-- src/views/mastodon/statuses.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index a2c4367..677bc60 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -56,12 +56,17 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { return c.json([]); } - const statuses = await Promise.all(events.map((event) => { + const statuses = (await Promise.all(events.map((event) => { if (event.kind === 6) { return renderReblog(event); } return renderStatus(event, c.get('pubkey')); - })); + }))).filter((boolean) => boolean); + + if (!statuses.length) { + return c.json([]); + } + return paginated(c, events, statuses); } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index e01e955..cac79ef 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -111,6 +111,7 @@ async function renderReblog(event: DittoEvent) { if (!event.repost) return; const reblog = await renderStatus(event.repost); + reblog.reblogged = true; return { id: event.id, account: await renderAccount(event.author), From d8a471272ddbf57072c35ebb7ff082793cdaa9cd Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sun, 7 Apr 2024 20:37:54 -0300 Subject: [PATCH 6/8] feat: update home timeline in realtime when reposting --- src/controllers/api/streaming.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index c864962..05eaf20 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -5,7 +5,7 @@ import { Debug, z } from '@/deps.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { Sub } from '@/subs.ts'; import { bech32ToPubkey } from '@/utils.ts'; -import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const debug = Debug('ditto:streaming'); @@ -63,6 +63,13 @@ const streamingController: AppController = (c) => { if (filter) { for await (const event of Sub.sub(socket, '1', [filter])) { + if (event.kind == 6) { + const status = await renderReblog(event); + if (status) { + send('update', status); + } + return; + } const status = await renderStatus(event, pubkey); if (status) { send('update', status); @@ -87,20 +94,20 @@ async function topicToFilter( switch (topic) { case 'public': - return { kinds: [1] }; + return { kinds: [1, 6] }; case 'public:local': - return { kinds: [1], search: `domain:${host}` }; + return { kinds: [1, 6], search: `domain:${host}` }; case 'hashtag': - if (query.tag) return { kinds: [1], '#t': [query.tag] }; + if (query.tag) return { kinds: [1, 6], '#t': [query.tag] }; break; case 'hashtag:local': - if (query.tag) return { kinds: [1], '#t': [query.tag], search: `domain:${host}` }; + if (query.tag) return { kinds: [1, 6], '#t': [query.tag], search: `domain:${host}` }; break; case 'user': // HACK: this puts the user's entire contacts list into RAM, // and then calls `matchFilters` over it. Refreshing the page // is required after following a new user. - return pubkey ? { kinds: [1], authors: await getFeedPubkeys(pubkey) } : undefined; + return pubkey ? { kinds: [1, 6], authors: await getFeedPubkeys(pubkey) } : undefined; } } From 1f7a4664ecf3d30fc5e3f5891ac311b0dbc3656b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Apr 2024 15:49:35 +0000 Subject: [PATCH 7/8] Apply 2 suggestion(s) to 2 file(s) --- src/controllers/api/statuses.ts | 4 +--- src/controllers/api/streaming.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index be8dbd9..91c1092 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -185,11 +185,9 @@ const reblogStatusController: AppController = async (c) => { return c.json({ error: 'Event not found.' }, 404); } - const tags: string[][] = [['e', event.id], ['p', event.pubkey]]; - const reblogEvent = await createEvent({ kind: 6, - tags, + tags: [['e', event.id], ['p', event.pubkey]], }, c); const status = await renderReblog(reblogEvent); diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 05eaf20..2ad9eda 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -63,7 +63,7 @@ const streamingController: AppController = (c) => { if (filter) { for await (const event of Sub.sub(socket, '1', [filter])) { - if (event.kind == 6) { + if (event.kind === 6) { const status = await renderReblog(event); if (status) { send('update', status); From d7d52bbdee7686c7e2e8d347ecb9557c881783fa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Apr 2024 15:53:08 +0000 Subject: [PATCH 8/8] Apply 1 suggestion(s) to 1 file(s) --- src/views/mastodon/statuses.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index cac79ef..69b086f 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -101,12 +101,8 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) { async function renderReblog(event: DittoEvent) { if (!event.author) return; - for (let i = 0; i < event.tags.length; i++) { - if (event.tags[i][0] === 'e') { - event.repost = await getEvent(event.tags[i][1], { kind: 1 }); - break; - } - } + const repostId = event.tags.find(([name]) => name === 'p')?.[1]; + event.repost = await getEvent(repostId, { kind: 1 }); if (!event.repost) return;