From 9ca3ec08a3bd81c6d2e6e2e15fd4f581721564a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 12:02:24 -0500 Subject: [PATCH 1/9] Delete `transformers` directory, rename `views` --- src/controllers/activitypub/actor.ts | 2 +- src/controllers/api/accounts.ts | 6 +++--- src/controllers/api/notifications.ts | 4 ++-- src/controllers/api/search.ts | 2 +- src/controllers/api/statuses.ts | 4 ++-- src/controllers/api/streaming.ts | 2 +- src/controllers/api/timelines.ts | 4 ++-- src/{transformers => views}/nostr-to-activitypub.ts | 0 src/{transformers => views}/nostr-to-mastoapi.ts | 0 9 files changed, 12 insertions(+), 12 deletions(-) rename src/{transformers => views}/nostr-to-activitypub.ts (100%) rename src/{transformers => views}/nostr-to-mastoapi.ts (100%) diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts index 6c9ed024..4f051c09 100644 --- a/src/controllers/activitypub/actor.ts +++ b/src/controllers/activitypub/actor.ts @@ -1,7 +1,7 @@ import { findUser } from '@/db/users.ts'; import { getAuthor } from '@/queries.ts'; -import { toActor } from '@/transformers/nostr-to-activitypub.ts'; import { activityJson } from '@/utils/web.ts'; +import { toActor } from '@/views/nostr-to-activitypub.ts'; import type { AppContext, AppController } from '@/app.ts'; diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index f14efafc..d86a410e 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -1,17 +1,17 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { insertUser } from '@/db/users.ts'; import { type Filter, findReplyTag, nip19, z } from '@/deps.ts'; import * as mixer from '@/mixer.ts'; import { getAuthor, getFollowedPubkeys, getFollows, syncUser } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; -import { accountFromPubkey, toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; +import { uploadFile } from '@/upload.ts'; import { isFollowing, lookupAccount, nostrNow, Time } from '@/utils.ts'; import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; -import { insertUser } from '@/db/users.ts'; -import { uploadFile } from '@/upload.ts'; +import { accountFromPubkey, toAccount, toRelationship, toStatus } from '@/views/nostr-to-mastoapi.ts'; const usernameSchema = z .string().min(1).max(30) diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index bd8fe4d8..c5f3e7ea 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -1,8 +1,8 @@ import { type AppController } from '@/app.ts'; import * as mixer from '@/mixer.ts'; -import { paginated, paginationSchema } from '@/utils/web.ts'; -import { toNotification } from '@/transformers/nostr-to-mastoapi.ts'; import { Time } from '@/utils.ts'; +import { paginated, paginationSchema } from '@/utils/web.ts'; +import { toNotification } from '@/views/nostr-to-mastoapi.ts'; const notificationsController: AppController = async (c) => { const pubkey = c.get('pubkey')!; diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index ee8497ea..4c711353 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -4,9 +4,9 @@ import { type Event, type Filter, nip19, z } from '@/deps.ts'; import * as mixer from '@/mixer.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; -import { toAccount, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { dedupeEvents, Time } from '@/utils.ts'; import { lookupNip05Cached } from '@/utils/nip05.ts'; +import { toAccount, toStatus } from '@/views/nostr-to-mastoapi.ts'; /** Matches NIP-05 names with or without an @ in front. */ const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/; diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 4756df05..73fb9821 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,10 +1,10 @@ import { type AppController } from '@/app.ts'; +import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; import { type Event, ISO6391, z } from '@/deps.ts'; import { getAncestors, getDescendants, getEvent } from '@/queries.ts'; -import { toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { createEvent, paginationSchema, parseBody } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; -import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; +import { toStatus } from '@/views/nostr-to-mastoapi.ts'; const createStatusSchema = z.object({ in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(), diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index acbc545e..1e495b81 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -3,8 +3,8 @@ import { z } from '@/deps.ts'; import { type DittoFilter } from '@/filter.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { Sub } from '@/subs.ts'; -import { toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { bech32ToPubkey } from '@/utils.ts'; +import { toStatus } from '@/views/nostr-to-mastoapi.ts'; /** * Streaming timelines/categories. diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 4b1825f4..56b8ddff 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -3,9 +3,9 @@ import { type DittoFilter } from '@/filter.ts'; import * as mixer from '@/mixer.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { booleanParamSchema } from '@/schema.ts'; -import { toStatus } from '@/transformers/nostr-to-mastoapi.ts'; -import { paginated, paginationSchema } from '@/utils/web.ts'; import { Time } from '@/utils.ts'; +import { paginated, paginationSchema } from '@/utils/web.ts'; +import { toStatus } from '@/views/nostr-to-mastoapi.ts'; import type { AppContext, AppController } from '@/app.ts'; diff --git a/src/transformers/nostr-to-activitypub.ts b/src/views/nostr-to-activitypub.ts similarity index 100% rename from src/transformers/nostr-to-activitypub.ts rename to src/views/nostr-to-activitypub.ts diff --git a/src/transformers/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts similarity index 100% rename from src/transformers/nostr-to-mastoapi.ts rename to src/views/nostr-to-mastoapi.ts From db17f84937b43022d07a8a26543cd31feaf26863 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 12:39:22 -0500 Subject: [PATCH 2/9] Move emojiTagSchema into schemas/nostr.ts --- src/schema.ts | 14 +------------- src/schemas/nostr.ts | 8 ++++++++ src/views/nostr-to-mastoapi.ts | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index a29191f4..8868cca2 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -21,9 +21,6 @@ const jsonSchema = z.string().transform((value, ctx) => { } }); -/** Parses a Nostr emoji tag. */ -const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]); - /** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ const decode64Schema = z.string().transform((value, ctx) => { try { @@ -51,13 +48,4 @@ const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value /** Schema for `File` objects. */ const fileSchema = z.custom((value) => value instanceof File); -export { - booleanParamSchema, - decode64Schema, - emojiTagSchema, - fileSchema, - filteredArray, - hashtagSchema, - jsonSchema, - safeUrlSchema, -}; +export { booleanParamSchema, decode64Schema, fileSchema, filteredArray, hashtagSchema, jsonSchema, safeUrlSchema }; diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index c097935c..7fb888c7 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -111,6 +111,12 @@ const connectResponseSchema = z.object({ result: signedEventSchema, }); +/** Parses a Nostr emoji tag. */ +const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]); + +/** NIP-30 custom emoji tag. */ +type EmojiTag = z.infer; + export { type ClientCLOSE, type ClientCOUNT, @@ -119,6 +125,8 @@ export { clientMsgSchema, type ClientREQ, connectResponseSchema, + type EmojiTag, + emojiTagSchema, filterSchema, jsonMediaDataSchema, jsonMetaContentSchema, diff --git a/src/views/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts index 1e3b887d..e7af4684 100644 --- a/src/views/nostr-to-mastoapi.ts +++ b/src/views/nostr-to-mastoapi.ts @@ -5,8 +5,8 @@ import * as eventsDB from '@/db/events.ts'; import { type Event, findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, type UnsignedEvent } from '@/deps.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { getAuthor, getFollowedPubkeys, getFollows } from '@/queries.ts'; -import { emojiTagSchema, filteredArray } from '@/schema.ts'; -import { jsonMediaDataSchema, jsonMetaContentSchema } from '@/schemas/nostr.ts'; +import { filteredArray } from '@/schema.ts'; +import { emojiTagSchema, jsonMediaDataSchema, jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { isFollowing, type Nip05, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; import { verifyNip05Cached } from '@/utils/nip05.ts'; import { findUser } from '@/db/users.ts'; From 1662f616ef3c0b2b9f7903a64d04cce86e495e7b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 13:29:25 -0500 Subject: [PATCH 3/9] Move unfurl code to a separate module --- src/utils/unfurl.ts | 73 ++++++++++++++++++++++++++++++++++ src/views/nostr-to-mastoapi.ts | 71 ++------------------------------- 2 files changed, 77 insertions(+), 67 deletions(-) create mode 100644 src/utils/unfurl.ts diff --git a/src/utils/unfurl.ts b/src/utils/unfurl.ts new file mode 100644 index 00000000..80977cf9 --- /dev/null +++ b/src/utils/unfurl.ts @@ -0,0 +1,73 @@ +import { TTLCache, unfurl } from '@/deps.ts'; +import { Time } from '@/utils/time.ts'; + +interface PreviewCard { + url: string; + title: string; + description: string; + type: 'link' | 'photo' | 'video' | 'rich'; + author_name: string; + author_url: string; + provider_name: string; + provider_url: string; + html: string; + width: number; + height: number; + image: string | null; + embed_url: string; + blurhash: string | null; +} + +async function unfurlCard(url: string, signal: AbortSignal): Promise { + console.log(`Unfurling ${url}...`); + try { + const result = await unfurl(url, { + fetch: (url) => fetch(url, { signal }), + }); + + return { + type: result.oEmbed?.type || 'link', + url: result.canonical_url || url, + title: result.oEmbed?.title || result.title || '', + description: result.open_graph.description || result.description || '', + author_name: result.oEmbed?.author_name || '', + author_url: result.oEmbed?.author_url || '', + provider_name: result.oEmbed?.provider_name || '', + provider_url: result.oEmbed?.provider_url || '', + // @ts-expect-error `html` does in fact exist on oEmbed. + html: sanitizeHtml(result.oEmbed?.html || '', { + allowedTags: ['iframe'], + allowedAttributes: { + iframe: ['width', 'height', 'src', 'frameborder', 'allowfullscreen'], + }, + }), + width: result.oEmbed?.width || 0, + height: result.oEmbed?.height || 0, + image: result.oEmbed?.thumbnails?.[0].url || result.open_graph.images?.[0].url || null, + embed_url: '', + blurhash: null, + }; + } catch (_e) { + return null; + } +} + +/** TTL cache for preview cards. */ +const previewCardCache = new TTLCache>({ + ttl: Time.hours(12), + max: 500, +}); + +/** Unfurl card from cache if available, otherwise fetch it. */ +function unfurlCardCached(url: string, timeout = Time.seconds(1)): Promise { + const cached = previewCardCache.get(url); + if (cached !== undefined) { + return cached; + } else { + const card = unfurlCard(url, AbortSignal.timeout(timeout)); + previewCardCache.set(url, card); + return card; + } +} + +export { type PreviewCard, unfurlCardCached }; diff --git a/src/views/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts index e7af4684..633df9a2 100644 --- a/src/views/nostr-to-mastoapi.ts +++ b/src/views/nostr-to-mastoapi.ts @@ -2,14 +2,15 @@ import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ad import { Conf } from '@/config.ts'; import * as eventsDB from '@/db/events.ts'; -import { type Event, findReplyTag, lodash, nip19, sanitizeHtml, TTLCache, unfurl, type UnsignedEvent } from '@/deps.ts'; +import { findUser } from '@/db/users.ts'; +import { type Event, findReplyTag, lodash, nip19, type UnsignedEvent } from '@/deps.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { getAuthor, getFollowedPubkeys, getFollows } from '@/queries.ts'; import { filteredArray } from '@/schema.ts'; import { emojiTagSchema, jsonMediaDataSchema, jsonMetaContentSchema } from '@/schemas/nostr.ts'; -import { isFollowing, type Nip05, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; +import { isFollowing, type Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; import { verifyNip05Cached } from '@/utils/nip05.ts'; -import { findUser } from '@/db/users.ts'; +import { unfurlCardCached } from '@/utils/unfurl.ts'; import { DittoAttachment, renderAttachment } from '@/views/attachment.ts'; const defaultAvatar = () => Conf.local('/images/avi.png'); @@ -211,70 +212,6 @@ function buildInlineRecipients(mentions: Mention[]): string { return `${elements.join(' ')} `; } -interface PreviewCard { - url: string; - title: string; - description: string; - type: 'link' | 'photo' | 'video' | 'rich'; - author_name: string; - author_url: string; - provider_name: string; - provider_url: string; - html: string; - width: number; - height: number; - image: string | null; - embed_url: string; - blurhash: string | null; -} - -async function unfurlCard(url: string): Promise { - console.log(`Unfurling ${url}...`); - try { - const result = await unfurl(url, { - fetch: (url) => fetch(url, { signal: AbortSignal.timeout(Time.seconds(1)) }), - }); - - return { - type: result.oEmbed?.type || 'link', - url: result.canonical_url || url, - title: result.oEmbed?.title || result.title || '', - description: result.open_graph.description || result.description || '', - author_name: result.oEmbed?.author_name || '', - author_url: result.oEmbed?.author_url || '', - provider_name: result.oEmbed?.provider_name || '', - provider_url: result.oEmbed?.provider_url || '', - // @ts-expect-error `html` does in fact exist on oEmbed. - html: sanitizeHtml(result.oEmbed?.html || '', { - allowedTags: ['iframe'], - allowedAttributes: { - iframe: ['width', 'height', 'src', 'frameborder', 'allowfullscreen'], - }, - }), - width: result.oEmbed?.width || 0, - height: result.oEmbed?.height || 0, - image: result.oEmbed?.thumbnails?.[0].url || result.open_graph.images?.[0].url || null, - embed_url: '', - blurhash: null, - }; - } catch (_e) { - return null; - } -} - -const previewCardCache = new TTLCache>({ ttl: Time.hours(12), max: 500 }); - -/** Unfurl card from cache if available, otherwise fetch it. */ -function unfurlCardCached(url: string): Promise { - const cached = previewCardCache.get(url); - if (cached !== undefined) return cached; - - const card = unfurlCard(url); - previewCardCache.set(url, card); - - return card; -} - function toEmojis(event: UnsignedEvent) { const emojiTags = event.tags.filter((tag) => tag[0] === 'emoji'); From 180fdbd1c9599d469e7de0b2b413fa00bac1e7cc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 15:09:01 -0500 Subject: [PATCH 4/9] nostr-to-activitypub.ts --> activitypub/actor.ts --- src/controllers/activitypub/actor.ts | 4 ++-- src/views/{nostr-to-activitypub.ts => activitypub/actor.ts} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/views/{nostr-to-activitypub.ts => activitypub/actor.ts} (91%) diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts index 4f051c09..49fda36f 100644 --- a/src/controllers/activitypub/actor.ts +++ b/src/controllers/activitypub/actor.ts @@ -1,7 +1,7 @@ import { findUser } from '@/db/users.ts'; import { getAuthor } from '@/queries.ts'; import { activityJson } from '@/utils/web.ts'; -import { toActor } from '@/views/nostr-to-activitypub.ts'; +import { renderActor } from '@/views/activitypub/actor.ts'; import type { AppContext, AppController } from '@/app.ts'; @@ -14,7 +14,7 @@ const actorController: AppController = async (c) => { const event = await getAuthor(user.pubkey); if (!event) return notFound(c); - const actor = await toActor(event, user.username); + const actor = await renderActor(event, user.username); if (!actor) return notFound(c); return activityJson(c, actor); diff --git a/src/views/nostr-to-activitypub.ts b/src/views/activitypub/actor.ts similarity index 91% rename from src/views/nostr-to-activitypub.ts rename to src/views/activitypub/actor.ts index ca90625f..1f2f6795 100644 --- a/src/views/nostr-to-activitypub.ts +++ b/src/views/activitypub/actor.ts @@ -6,7 +6,7 @@ import type { Event } from '@/deps.ts'; import type { Actor } from '@/schemas/activitypub.ts'; /** Nostr metadata event to ActivityPub actor. */ -async function toActor(event: Event<0>, username: string): Promise { +async function renderActor(event: Event<0>, username: string): Promise { const content = jsonMetaContentSchema.parse(event.content); return { @@ -44,4 +44,4 @@ async function toActor(event: Event<0>, username: string): Promise Date: Fri, 6 Oct 2023 15:19:13 -0500 Subject: [PATCH 5/9] views/attachment.ts -> views/mastodon/attachments.ts --- src/controllers/api/media.ts | 2 +- src/note.ts | 2 +- src/views/{attachment.ts => mastodon/attachments.ts} | 0 src/views/nostr-to-mastoapi.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/views/{attachment.ts => mastodon/attachments.ts} (100%) diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index a7edef15..b35da9f1 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -2,7 +2,7 @@ import { AppController } from '@/app.ts'; import { z } from '@/deps.ts'; import { fileSchema } from '@/schema.ts'; import { parseBody } from '@/utils/web.ts'; -import { renderAttachment } from '@/views/attachment.ts'; +import { renderAttachment } from '@/views/mastodon/attachments.ts'; import { uploadFile } from '@/upload.ts'; const mediaBodySchema = z.object({ diff --git a/src/note.ts b/src/note.ts index 92baf521..93689e94 100644 --- a/src/note.ts +++ b/src/note.ts @@ -1,6 +1,6 @@ import { Conf } from '@/config.ts'; import { linkify, linkifyStr, mime, nip19, nip21 } from '@/deps.ts'; -import { type DittoAttachment } from '@/views/attachment.ts'; +import { type DittoAttachment } from '@/views/mastodon/attachments.ts'; linkify.registerCustomProtocol('nostr', true); linkify.registerCustomProtocol('wss'); diff --git a/src/views/attachment.ts b/src/views/mastodon/attachments.ts similarity index 100% rename from src/views/attachment.ts rename to src/views/mastodon/attachments.ts diff --git a/src/views/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts index 633df9a2..107d4699 100644 --- a/src/views/nostr-to-mastoapi.ts +++ b/src/views/nostr-to-mastoapi.ts @@ -11,7 +11,7 @@ import { emojiTagSchema, jsonMediaDataSchema, jsonMetaContentSchema } from '@/sc import { isFollowing, type Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; import { verifyNip05Cached } from '@/utils/nip05.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; -import { DittoAttachment, renderAttachment } from '@/views/attachment.ts'; +import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; const defaultAvatar = () => Conf.local('/images/avi.png'); const defaultBanner = () => Conf.local('/images/banner.png'); From cb1141784e4ef99a34e047bc3e23dbca30cb7771 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 15:19:27 -0500 Subject: [PATCH 6/9] views.ts: fix wrong import of toAccount --- src/views.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views.ts b/src/views.ts index 2765c6de..db7a51d0 100644 --- a/src/views.ts +++ b/src/views.ts @@ -2,7 +2,7 @@ import { AppContext } from '@/app.ts'; import { type Filter } from '@/deps.ts'; import * as mixer from '@/mixer.ts'; import { getAuthor } from '@/queries.ts'; -import { toAccount } from '@/transformers/nostr-to-mastoapi.ts'; +import { toAccount } from '@/views/nostr-to-mastoapi.ts'; import { paginated } from '@/utils/web.ts'; /** Render account objects for the author of each event. */ From 0b77e7d8883f70b725031f7f23ec376e6e17c819 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 15:28:02 -0500 Subject: [PATCH 7/9] Add views/mastodon/accounts.ts, views/mastodon/emojis.ts --- src/controllers/api/accounts.ts | 15 ++-- src/controllers/api/search.ts | 5 +- src/views.ts | 4 +- src/views/mastodon/accounts.ts | 96 +++++++++++++++++++++++++ src/views/mastodon/emojis.ts | 19 +++++ src/views/nostr-to-mastoapi.ts | 120 +++----------------------------- 6 files changed, 138 insertions(+), 121 deletions(-) create mode 100644 src/views/mastodon/accounts.ts create mode 100644 src/views/mastodon/emojis.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index d86a410e..c43a4247 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -11,7 +11,8 @@ import { isFollowing, lookupAccount, nostrNow, Time } from '@/utils.ts'; import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; -import { accountFromPubkey, toAccount, toRelationship, toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderAccount } from '@/views/mastodon/accounts.ts'; +import { accountFromPubkey, toRelationship, toStatus } from '@/views/nostr-to-mastoapi.ts'; const usernameSchema = z .string().min(1).max(30) @@ -60,7 +61,7 @@ const verifyCredentialsController: AppController = async (c) => { const event = await getAuthor(pubkey); if (event) { - return c.json(await toAccount(event, { withSource: true })); + return c.json(await renderAccount(event, { withSource: true })); } else { return c.json(await accountFromPubkey(pubkey, { withSource: true })); } @@ -71,7 +72,7 @@ const accountController: AppController = async (c) => { const event = await getAuthor(pubkey); if (event) { - return c.json(await toAccount(event)); + return c.json(await renderAccount(event)); } return c.json({ error: 'Could not find user.' }, 404); @@ -86,7 +87,7 @@ const accountLookupController: AppController = async (c) => { const event = await lookupAccount(decodeURIComponent(acct)); if (event) { - return c.json(await toAccount(event)); + return c.json(await renderAccount(event)); } return c.json({ error: 'Could not find user.' }, 404); @@ -101,7 +102,7 @@ const accountSearchController: AppController = async (c) => { const event = await lookupAccount(decodeURIComponent(q)); if (event) { - return c.json([await toAccount(event)]); + return c.json([await renderAccount(event)]); } return c.json([]); @@ -199,7 +200,7 @@ const updateCredentialsController: AppController = async (c) => { tags: [], }, c); - const account = await toAccount(event); + const account = await renderAccount(event); return c.json(account); }; @@ -237,7 +238,7 @@ const followingController: AppController = async (c) => { // TODO: pagination by offset. const accounts = await Promise.all(pubkeys.map(async (pubkey) => { const event = await getAuthor(pubkey); - return event ? await toAccount(event) : undefined; + return event ? await renderAccount(event) : undefined; })); return c.json(accounts.filter(Boolean)); diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 4c711353..8cc17e60 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -6,7 +6,8 @@ import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; import { dedupeEvents, Time } from '@/utils.ts'; import { lookupNip05Cached } from '@/utils/nip05.ts'; -import { toAccount, toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderAccount } from '@/views/mastodon/accounts.ts'; +import { toStatus } from '@/views/nostr-to-mastoapi.ts'; /** Matches NIP-05 names with or without an @ in front. */ const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/; @@ -44,7 +45,7 @@ const searchController: AppController = async (c) => { Promise.all( results .filter((event): event is Event<0> => event.kind === 0) - .map((event) => toAccount(event)), + .map((event) => renderAccount(event)), ), Promise.all( results diff --git a/src/views.ts b/src/views.ts index db7a51d0..30497108 100644 --- a/src/views.ts +++ b/src/views.ts @@ -2,7 +2,7 @@ import { AppContext } from '@/app.ts'; import { type Filter } from '@/deps.ts'; import * as mixer from '@/mixer.ts'; import { getAuthor } from '@/queries.ts'; -import { toAccount } from '@/views/nostr-to-mastoapi.ts'; +import { renderAccount } from '@/views/mastodon/accounts.ts'; import { paginated } from '@/utils/web.ts'; /** Render account objects for the author of each event. */ @@ -17,7 +17,7 @@ async function renderEventAccounts(c: AppContext, filters: Filter[]) { const accounts = await Promise.all([...pubkeys].map(async (pubkey) => { const author = await getAuthor(pubkey); if (author) { - return toAccount(author); + return renderAccount(author); } })); diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts new file mode 100644 index 00000000..3a55fb75 --- /dev/null +++ b/src/views/mastodon/accounts.ts @@ -0,0 +1,96 @@ +import { Conf } from '@/config.ts'; +import * as eventsDB from '@/db/events.ts'; +import { findUser } from '@/db/users.ts'; +import { lodash, nip19, type UnsignedEvent } from '@/deps.ts'; +import { getFollowedPubkeys } from '@/queries.ts'; +import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; +import { verifyNip05Cached } from '@/utils/nip05.ts'; +import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; +import { renderEmojis } from '@/views/mastodon/emojis.ts'; + +interface ToAccountOpts { + withSource?: boolean; +} + +async function renderAccount(event: UnsignedEvent<0>, opts: ToAccountOpts = {}) { + const { withSource = false } = opts; + const { pubkey } = event; + + const { + name, + nip05, + picture = Conf.local('/images/avi.png'), + banner = Conf.local('/images/banner.png'), + about, + } = jsonMetaContentSchema.parse(event.content); + + const npub = nip19.npubEncode(pubkey); + + const [user, parsed05, followersCount, followingCount, statusesCount] = await Promise.all([ + findUser({ pubkey }), + parseAndVerifyNip05(nip05, pubkey), + eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]), + getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length), + eventsDB.countFilters([{ kinds: [1], authors: [pubkey] }]), + ]); + + return { + id: pubkey, + acct: parsed05?.handle || npub, + avatar: picture, + avatar_static: picture, + bot: false, + created_at: event ? nostrDate(event.created_at).toISOString() : new Date().toISOString(), + discoverable: true, + display_name: name, + emojis: renderEmojis(event), + fields: [], + follow_requests_count: 0, + followers_count: followersCount, + following_count: followingCount, + fqn: parsed05?.handle || npub, + header: banner, + header_static: banner, + last_status_at: null, + locked: false, + note: lodash.escape(about), + roles: [], + source: withSource + ? { + fields: [], + language: '', + note: about || '', + privacy: 'public', + sensitive: false, + follow_requests_count: 0, + } + : undefined, + statuses_count: statusesCount, + url: Conf.local(`/users/${pubkey}`), + username: parsed05?.nickname || npub.substring(0, 8), + pleroma: { + is_admin: user?.admin || false, + is_moderator: user?.admin || false, + }, + }; +} + +function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) { + const event: UnsignedEvent<0> = { + kind: 0, + pubkey, + content: '', + tags: [], + created_at: nostrNow(), + }; + + return renderAccount(event, opts); +} + +async function parseAndVerifyNip05(nip05: string | undefined, pubkey: string): Promise { + if (nip05 && await verifyNip05Cached(nip05, pubkey)) { + return parseNip05(nip05); + } +} + +export { accountFromPubkey, renderAccount }; diff --git a/src/views/mastodon/emojis.ts b/src/views/mastodon/emojis.ts new file mode 100644 index 00000000..0ba28956 --- /dev/null +++ b/src/views/mastodon/emojis.ts @@ -0,0 +1,19 @@ +import { UnsignedEvent } from '@/deps.ts'; +import { EmojiTag, emojiTagSchema } from '@/schemas/nostr.ts'; +import { filteredArray } from '@/schema.ts'; + +function renderEmoji([_, shortcode, url]: EmojiTag) { + return { + shortcode, + static_url: url, + url, + }; +} + +function renderEmojis({ tags }: UnsignedEvent) { + return filteredArray(emojiTagSchema) + .parse(tags) + .map(renderEmoji); +} + +export { renderEmojis }; diff --git a/src/views/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts index 107d4699..21d5f01c 100644 --- a/src/views/nostr-to-mastoapi.ts +++ b/src/views/nostr-to-mastoapi.ts @@ -2,108 +2,19 @@ import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ad import { Conf } from '@/config.ts'; import * as eventsDB from '@/db/events.ts'; -import { findUser } from '@/db/users.ts'; -import { type Event, findReplyTag, lodash, nip19, type UnsignedEvent } from '@/deps.ts'; +import { type Event, findReplyTag, nip19 } from '@/deps.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; -import { getAuthor, getFollowedPubkeys, getFollows } from '@/queries.ts'; -import { filteredArray } from '@/schema.ts'; -import { emojiTagSchema, jsonMediaDataSchema, jsonMetaContentSchema } from '@/schemas/nostr.ts'; -import { isFollowing, type Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; -import { verifyNip05Cached } from '@/utils/nip05.ts'; +import { getAuthor, getFollows } from '@/queries.ts'; +import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; +import { isFollowing, nostrDate } from '@/utils.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; - -const defaultAvatar = () => Conf.local('/images/avi.png'); -const defaultBanner = () => Conf.local('/images/banner.png'); - -interface ToAccountOpts { - withSource?: boolean; -} - -async function toAccount(event: UnsignedEvent<0>, opts: ToAccountOpts = {}) { - const { withSource = false } = opts; - const { pubkey } = event; - - const { - name, - nip05, - picture = defaultAvatar(), - banner = defaultBanner(), - about, - } = jsonMetaContentSchema.parse(event.content); - - const npub = nip19.npubEncode(pubkey); - - const [user, parsed05, followersCount, followingCount, statusesCount] = await Promise.all([ - findUser({ pubkey }), - parseAndVerifyNip05(nip05, pubkey), - eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]), - getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length), - eventsDB.countFilters([{ kinds: [1], authors: [pubkey] }]), - ]); - - return { - id: pubkey, - acct: parsed05?.handle || npub, - avatar: picture, - avatar_static: picture, - bot: false, - created_at: event ? nostrDate(event.created_at).toISOString() : new Date().toISOString(), - discoverable: true, - display_name: name, - emojis: toEmojis(event), - fields: [], - follow_requests_count: 0, - followers_count: followersCount, - following_count: followingCount, - fqn: parsed05?.handle || npub, - header: banner, - header_static: banner, - last_status_at: null, - locked: false, - note: lodash.escape(about), - roles: [], - source: withSource - ? { - fields: [], - language: '', - note: about || '', - privacy: 'public', - sensitive: false, - follow_requests_count: 0, - } - : undefined, - statuses_count: statusesCount, - url: Conf.local(`/users/${pubkey}`), - username: parsed05?.nickname || npub.substring(0, 8), - pleroma: { - is_admin: user?.admin || false, - is_moderator: user?.admin || false, - }, - }; -} - -function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) { - const event: UnsignedEvent<0> = { - kind: 0, - pubkey, - content: '', - tags: [], - created_at: nostrNow(), - }; - - return toAccount(event, opts); -} - -async function parseAndVerifyNip05(nip05: string | undefined, pubkey: string): Promise { - if (nip05 && await verifyNip05Cached(nip05, pubkey)) { - return parseNip05(nip05); - } -} +import { renderEmojis } from '@/views/mastodon/emojis.ts'; async function toMention(pubkey: string) { const profile = await getAuthor(pubkey); - const account = profile ? await toAccount(profile) : undefined; + const account = profile ? await renderAccount(profile) : undefined; if (account) { return { @@ -125,7 +36,7 @@ async function toMention(pubkey: string) { async function toStatus(event: Event<1>, viewerPubkey?: string) { const profile = await getAuthor(event.pubkey); - const account = profile ? await toAccount(profile) : await accountFromPubkey(event.pubkey); + const account = profile ? await renderAccount(profile) : await accountFromPubkey(event.pubkey); const replyTag = findReplyTag(event); @@ -191,7 +102,7 @@ async function toStatus(event: Event<1>, viewerPubkey?: string) { media_attachments: media.map(renderAttachment), mentions, tags: [], - emojis: toEmojis(event), + emojis: renderEmojis(event), poll: null, uri: Conf.local(`/posts/${event.id}`), url: Conf.local(`/posts/${event.id}`), @@ -212,17 +123,6 @@ function buildInlineRecipients(mentions: Mention[]): string { return `${elements.join(' ')} `; } -function toEmojis(event: UnsignedEvent) { - const emojiTags = event.tags.filter((tag) => tag[0] === 'emoji'); - - return filteredArray(emojiTagSchema).parse(emojiTags) - .map((tag) => ({ - shortcode: tag[1], - static_url: tag[2], - url: tag[2], - })); -} - async function toRelationship(sourcePubkey: string, targetPubkey: string) { const [source, target] = await Promise.all([ getFollows(sourcePubkey), @@ -265,4 +165,4 @@ async function toNotificationMention(event: Event<1>, viewerPubkey?: string) { }; } -export { accountFromPubkey, toAccount, toNotification, toRelationship, toStatus }; +export { accountFromPubkey, toNotification, toRelationship, toStatus }; From d49c63bb1a3c4fdb3ee886239b0c47f666964f98 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 15:37:31 -0500 Subject: [PATCH 8/9] Move statuses view into its own file --- src/controllers/api/accounts.ts | 7 +- src/controllers/api/search.ts | 4 +- src/controllers/api/statuses.ts | 10 +-- src/controllers/api/streaming.ts | 4 +- src/controllers/api/timelines.ts | 4 +- src/views/mastodon/statuses.ts | 126 +++++++++++++++++++++++++++++ src/views/nostr-to-mastoapi.ts | 131 ++----------------------------- 7 files changed, 147 insertions(+), 139 deletions(-) create mode 100644 src/views/mastodon/statuses.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index c43a4247..feb6dfe6 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -12,7 +12,8 @@ import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; -import { accountFromPubkey, toRelationship, toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; +import { accountFromPubkey, toRelationship } from '@/views/nostr-to-mastoapi.ts'; const usernameSchema = z .string().min(1).max(30) @@ -149,7 +150,7 @@ const accountStatusesController: AppController = async (c) => { events = events.filter((event) => !findReplyTag(event)); } - const statuses = await Promise.all(events.map((event) => toStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); return paginated(c, events, statuses); }; @@ -259,7 +260,7 @@ const favouritesController: AppController = async (c) => { const events1 = await mixer.getFilters([{ kinds: [1], ids }], { timeout: Time.seconds(1) }); - const statuses = await Promise.all(events1.map((event) => toStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events1.map((event) => renderStatus(event, c.get('pubkey')))); return paginated(c, events1, statuses); }; diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 8cc17e60..15417c88 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -7,7 +7,7 @@ import { nostrIdSchema } from '@/schemas/nostr.ts'; import { dedupeEvents, Time } from '@/utils.ts'; import { lookupNip05Cached } from '@/utils/nip05.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; -import { toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; /** Matches NIP-05 names with or without an @ in front. */ const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/; @@ -50,7 +50,7 @@ const searchController: AppController = async (c) => { Promise.all( results .filter((event): event is Event<1> => event.kind === 1) - .map((event) => toStatus(event, c.get('pubkey'))), + .map((event) => renderStatus(event, c.get('pubkey'))), ), ]); diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 73fb9821..0f6be9cd 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -4,7 +4,7 @@ import { type Event, ISO6391, z } from '@/deps.ts'; import { getAncestors, getDescendants, getEvent } from '@/queries.ts'; import { createEvent, paginationSchema, parseBody } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; -import { toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; const createStatusSchema = z.object({ in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(), @@ -31,7 +31,7 @@ const statusController: AppController = async (c) => { const event = await getEvent(id, { kind: 1 }); if (event) { - return c.json(await toStatus(event, c.get('pubkey'))); + return c.json(await renderStatus(event, c.get('pubkey'))); } return c.json({ error: 'Event not found.' }, 404); @@ -83,7 +83,7 @@ const createStatusController: AppController = async (c) => { tags, }, c); - return c.json(await toStatus(event, c.get('pubkey'))); + return c.json(await renderStatus(event, c.get('pubkey'))); }; const contextController: AppController = async (c) => { @@ -91,7 +91,7 @@ const contextController: AppController = async (c) => { const event = await getEvent(id, { kind: 1 }); async function renderStatuses(events: Event<1>[]) { - const statuses = await Promise.all(events.map((event) => toStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); return statuses.filter(Boolean); } @@ -121,7 +121,7 @@ const favouriteController: AppController = async (c) => { ], }, c); - const status = await toStatus(target, c.get('pubkey')); + const status = await renderStatus(target, c.get('pubkey')); if (status) { status.favourited = true; diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 1e495b81..6a9e535b 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -4,7 +4,7 @@ import { type DittoFilter } from '@/filter.ts'; import { getFeedPubkeys } from '@/queries.ts'; import { Sub } from '@/subs.ts'; import { bech32ToPubkey } from '@/utils.ts'; -import { toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; /** * Streaming timelines/categories. @@ -63,7 +63,7 @@ const streamingController: AppController = (c) => { if (filter) { for await (const event of Sub.sub(socket, '1', [filter])) { - const status = await toStatus(event, pubkey); + const status = await renderStatus(event, pubkey); if (status) { send('update', status); } diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 56b8ddff..712f7415 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -5,7 +5,7 @@ import { getFeedPubkeys } from '@/queries.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Time } from '@/utils.ts'; import { paginated, paginationSchema } from '@/utils/web.ts'; -import { toStatus } from '@/views/nostr-to-mastoapi.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; import type { AppContext, AppController } from '@/app.ts'; @@ -40,7 +40,7 @@ async function renderStatuses(c: AppContext, filters: DittoFilter<1>[]) { return c.json([]); } - const statuses = await Promise.all(events.map((event) => toStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); return paginated(c, events, statuses); } diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts new file mode 100644 index 00000000..131dcd6c --- /dev/null +++ b/src/views/mastodon/statuses.ts @@ -0,0 +1,126 @@ +import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; + +import { Conf } from '@/config.ts'; +import * as eventsDB from '@/db/events.ts'; +import { type Event, findReplyTag, nip19 } from '@/deps.ts'; +import { getMediaLinks, parseNoteContent } from '@/note.ts'; +import { getAuthor } from '@/queries.ts'; +import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; +import { nostrDate } from '@/utils.ts'; +import { unfurlCardCached } from '@/utils/unfurl.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; +import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; +import { renderEmojis } from '@/views/mastodon/emojis.ts'; + +async function renderStatus(event: Event<1>, viewerPubkey?: string) { + const profile = await getAuthor(event.pubkey); + const account = profile ? await renderAccount(profile) : await accountFromPubkey(event.pubkey); + + const replyTag = findReplyTag(event); + + const mentionedPubkeys = [ + ...new Set( + event.tags + .filter((tag) => tag[0] === 'p') + .map((tag) => tag[1]), + ), + ]; + + const { html, links, firstUrl } = parseNoteContent(event.content); + + const [mentions, card, repliesCount, reblogsCount, favouritesCount, [repostEvent], [reactionEvent]] = await Promise + .all([ + Promise.all(mentionedPubkeys.map(toMention)), + firstUrl ? unfurlCardCached(firstUrl) : null, + eventsDB.countFilters([{ kinds: [1], '#e': [event.id] }]), + eventsDB.countFilters([{ kinds: [6], '#e': [event.id] }]), + eventsDB.countFilters([{ kinds: [7], '#e': [event.id] }]), + viewerPubkey + ? eventsDB.getFilters([{ kinds: [6], '#e': [event.id], authors: [viewerPubkey] }], { limit: 1 }) + : [], + viewerPubkey + ? eventsDB.getFilters([{ kinds: [7], '#e': [event.id], authors: [viewerPubkey] }], { limit: 1 }) + : [], + ]); + + const content = buildInlineRecipients(mentions) + html; + + const cw = event.tags.find(isCWTag); + const subject = event.tags.find((tag) => tag[0] === 'subject'); + + const mediaLinks = getMediaLinks(links); + + const mediaTags: DittoAttachment[] = event.tags + .filter((tag) => tag[0] === 'media') + .map(([_, url, json]) => ({ url, data: jsonMediaDataSchema.parse(json) })); + + const media = [...mediaLinks, ...mediaTags]; + + return { + id: event.id, + account, + card, + content, + created_at: nostrDate(event.created_at).toISOString(), + in_reply_to_id: replyTag ? replyTag[1] : null, + in_reply_to_account_id: null, + sensitive: !!cw, + spoiler_text: (cw ? cw[1] : subject?.[1]) || '', + visibility: 'public', + language: event.tags.find((tag) => tag[0] === 'lang')?.[1] || null, + replies_count: repliesCount, + reblogs_count: reblogsCount, + favourites_count: favouritesCount, + favourited: reactionEvent?.content === '+', + reblogged: Boolean(repostEvent), + muted: false, + bookmarked: false, + reblog: null, + application: null, + media_attachments: media.map(renderAttachment), + mentions, + tags: [], + emojis: renderEmojis(event), + poll: null, + uri: Conf.local(`/posts/${event.id}`), + url: Conf.local(`/posts/${event.id}`), + }; +} + +async function toMention(pubkey: string) { + const profile = await getAuthor(pubkey); + const account = profile ? await renderAccount(profile) : undefined; + + if (account) { + return { + id: account.id, + acct: account.acct, + username: account.username, + url: account.url, + }; + } else { + const npub = nip19.npubEncode(pubkey); + return { + id: pubkey, + acct: npub, + username: npub.substring(0, 8), + url: Conf.local(`/users/${pubkey}`), + }; + } +} + +type Mention = Awaited>; + +function buildInlineRecipients(mentions: Mention[]): string { + if (!mentions.length) return ''; + + const elements = mentions.reduce((acc, { url, username }) => { + const name = nip19.BECH32_REGEX.test(username) ? username.substring(0, 8) : username; + acc.push(`@${name}`); + return acc; + }, []); + + return `${elements.join(' ')} `; +} + +export { renderStatus }; diff --git a/src/views/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts index 21d5f01c..bdad7396 100644 --- a/src/views/nostr-to-mastoapi.ts +++ b/src/views/nostr-to-mastoapi.ts @@ -1,127 +1,8 @@ -import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; - -import { Conf } from '@/config.ts'; -import * as eventsDB from '@/db/events.ts'; -import { type Event, findReplyTag, nip19 } from '@/deps.ts'; -import { getMediaLinks, parseNoteContent } from '@/note.ts'; -import { getAuthor, getFollows } from '@/queries.ts'; -import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; +import { type Event } from '@/deps.ts'; +import { getFollows } from '@/queries.ts'; import { isFollowing, nostrDate } from '@/utils.ts'; -import { unfurlCardCached } from '@/utils/unfurl.ts'; -import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; -import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; -import { renderEmojis } from '@/views/mastodon/emojis.ts'; - -async function toMention(pubkey: string) { - const profile = await getAuthor(pubkey); - const account = profile ? await renderAccount(profile) : undefined; - - if (account) { - return { - id: account.id, - acct: account.acct, - username: account.username, - url: account.url, - }; - } else { - const npub = nip19.npubEncode(pubkey); - return { - id: pubkey, - acct: npub, - username: npub.substring(0, 8), - url: Conf.local(`/users/${pubkey}`), - }; - } -} - -async function toStatus(event: Event<1>, viewerPubkey?: string) { - const profile = await getAuthor(event.pubkey); - const account = profile ? await renderAccount(profile) : await accountFromPubkey(event.pubkey); - - const replyTag = findReplyTag(event); - - const mentionedPubkeys = [ - ...new Set( - event.tags - .filter((tag) => tag[0] === 'p') - .map((tag) => tag[1]), - ), - ]; - - const { html, links, firstUrl } = parseNoteContent(event.content); - - const [mentions, card, repliesCount, reblogsCount, favouritesCount, [repostEvent], [reactionEvent]] = await Promise - .all([ - Promise.all(mentionedPubkeys.map(toMention)), - firstUrl ? unfurlCardCached(firstUrl) : null, - eventsDB.countFilters([{ kinds: [1], '#e': [event.id] }]), - eventsDB.countFilters([{ kinds: [6], '#e': [event.id] }]), - eventsDB.countFilters([{ kinds: [7], '#e': [event.id] }]), - viewerPubkey - ? eventsDB.getFilters([{ kinds: [6], '#e': [event.id], authors: [viewerPubkey] }], { limit: 1 }) - : [], - viewerPubkey - ? eventsDB.getFilters([{ kinds: [7], '#e': [event.id], authors: [viewerPubkey] }], { limit: 1 }) - : [], - ]); - - const content = buildInlineRecipients(mentions) + html; - - const cw = event.tags.find(isCWTag); - const subject = event.tags.find((tag) => tag[0] === 'subject'); - - const mediaLinks = getMediaLinks(links); - - const mediaTags: DittoAttachment[] = event.tags - .filter((tag) => tag[0] === 'media') - .map(([_, url, json]) => ({ url, data: jsonMediaDataSchema.parse(json) })); - - const media = [...mediaLinks, ...mediaTags]; - - return { - id: event.id, - account, - card, - content, - created_at: nostrDate(event.created_at).toISOString(), - in_reply_to_id: replyTag ? replyTag[1] : null, - in_reply_to_account_id: null, - sensitive: !!cw, - spoiler_text: (cw ? cw[1] : subject?.[1]) || '', - visibility: 'public', - language: event.tags.find((tag) => tag[0] === 'lang')?.[1] || null, - replies_count: repliesCount, - reblogs_count: reblogsCount, - favourites_count: favouritesCount, - favourited: reactionEvent?.content === '+', - reblogged: Boolean(repostEvent), - muted: false, - bookmarked: false, - reblog: null, - application: null, - media_attachments: media.map(renderAttachment), - mentions, - tags: [], - emojis: renderEmojis(event), - poll: null, - uri: Conf.local(`/posts/${event.id}`), - url: Conf.local(`/posts/${event.id}`), - }; -} - -type Mention = Awaited>; - -function buildInlineRecipients(mentions: Mention[]): string { - if (!mentions.length) return ''; - - const elements = mentions.reduce((acc, { url, username }) => { - const name = nip19.BECH32_REGEX.test(username) ? username.substring(0, 8) : username; - acc.push(`@${name}`); - return acc; - }, []); - - return `${elements.join(' ')} `; -} +import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; async function toRelationship(sourcePubkey: string, targetPubkey: string) { const [source, target] = await Promise.all([ @@ -153,7 +34,7 @@ function toNotification(event: Event, viewerPubkey?: string) { } async function toNotificationMention(event: Event<1>, viewerPubkey?: string) { - const status = await toStatus(event, viewerPubkey); + const status = await renderStatus(event, viewerPubkey); if (!status) return; return { @@ -165,4 +46,4 @@ async function toNotificationMention(event: Event<1>, viewerPubkey?: string) { }; } -export { accountFromPubkey, toNotification, toRelationship, toStatus }; +export { accountFromPubkey, toNotification, toRelationship }; From 45d42f7ea4119f22a5560ed2b82093db81a956a1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 15:40:34 -0500 Subject: [PATCH 9/9] Delete nostr-to-mastoapi.ts, add notifications.ts and relationships.ts --- src/controllers/api/accounts.ts | 8 ++--- src/controllers/api/notifications.ts | 4 +-- src/views/mastodon/notifications.ts | 26 +++++++++++++++ src/views/mastodon/relationships.ts | 26 +++++++++++++++ src/views/nostr-to-mastoapi.ts | 49 ---------------------------- 5 files changed, 58 insertions(+), 55 deletions(-) create mode 100644 src/views/mastodon/notifications.ts create mode 100644 src/views/mastodon/relationships.ts delete mode 100644 src/views/nostr-to-mastoapi.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index feb6dfe6..9ac9f575 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -11,9 +11,9 @@ import { isFollowing, lookupAccount, nostrNow, Time } from '@/utils.ts'; import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; -import { renderAccount } from '@/views/mastodon/accounts.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; +import { renderRelationship } from '@/views/mastodon/relationships.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -import { accountFromPubkey, toRelationship } from '@/views/nostr-to-mastoapi.ts'; const usernameSchema = z .string().min(1).max(30) @@ -117,7 +117,7 @@ const relationshipsController: AppController = async (c) => { return c.json({ error: 'Missing `id[]` query parameters.' }, 422); } - const result = await Promise.all(ids.data.map((id) => toRelationship(pubkey, id))); + const result = await Promise.all(ids.data.map((id) => renderRelationship(pubkey, id))); return c.json(result); }; @@ -222,7 +222,7 @@ const followController: AppController = async (c) => { }, c); } - const relationship = await toRelationship(sourcePubkey, targetPubkey); + const relationship = await renderRelationship(sourcePubkey, targetPubkey); return c.json(relationship); }; diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index c5f3e7ea..89f59f79 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -2,7 +2,7 @@ import { type AppController } from '@/app.ts'; import * as mixer from '@/mixer.ts'; import { Time } from '@/utils.ts'; import { paginated, paginationSchema } from '@/utils/web.ts'; -import { toNotification } from '@/views/nostr-to-mastoapi.ts'; +import { renderNotification } from '@/views/mastodon/notifications.ts'; const notificationsController: AppController = async (c) => { const pubkey = c.get('pubkey')!; @@ -13,7 +13,7 @@ const notificationsController: AppController = async (c) => { { timeout: Time.seconds(3) }, ); - const statuses = await Promise.all(events.map((event) => toNotification(event, pubkey))); + const statuses = await Promise.all(events.map((event) => renderNotification(event, pubkey))); return paginated(c, events, statuses); }; diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts new file mode 100644 index 00000000..59e56653 --- /dev/null +++ b/src/views/mastodon/notifications.ts @@ -0,0 +1,26 @@ +import { type Event } from '@/deps.ts'; +import { nostrDate } from '@/utils.ts'; +import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; + +function renderNotification(event: Event, viewerPubkey?: string) { + switch (event.kind) { + case 1: + return renderNotificationMention(event as Event<1>, viewerPubkey); + } +} + +async function renderNotificationMention(event: Event<1>, viewerPubkey?: string) { + const status = await renderStatus(event, viewerPubkey); + if (!status) return; + + return { + id: event.id, + type: 'mention', + created_at: nostrDate(event.created_at).toISOString(), + account: status.account, + status: status, + }; +} + +export { accountFromPubkey, renderNotification }; diff --git a/src/views/mastodon/relationships.ts b/src/views/mastodon/relationships.ts new file mode 100644 index 00000000..91d33ea7 --- /dev/null +++ b/src/views/mastodon/relationships.ts @@ -0,0 +1,26 @@ +import { getFollows } from '@/queries.ts'; +import { isFollowing } from '@/utils.ts'; + +async function renderRelationship(sourcePubkey: string, targetPubkey: string) { + const [source, target] = await Promise.all([ + getFollows(sourcePubkey), + getFollows(targetPubkey), + ]); + + return { + id: targetPubkey, + following: source ? isFollowing(source, targetPubkey) : false, + showing_reblogs: true, + notifying: false, + followed_by: target ? isFollowing(target, sourcePubkey) : false, + blocking: false, + blocked_by: false, + muting: false, + muting_notifications: false, + requested: false, + domain_blocking: false, + endorsed: false, + }; +} + +export { renderRelationship }; diff --git a/src/views/nostr-to-mastoapi.ts b/src/views/nostr-to-mastoapi.ts deleted file mode 100644 index bdad7396..00000000 --- a/src/views/nostr-to-mastoapi.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type Event } from '@/deps.ts'; -import { getFollows } from '@/queries.ts'; -import { isFollowing, nostrDate } from '@/utils.ts'; -import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; -import { renderStatus } from '@/views/mastodon/statuses.ts'; - -async function toRelationship(sourcePubkey: string, targetPubkey: string) { - const [source, target] = await Promise.all([ - getFollows(sourcePubkey), - getFollows(targetPubkey), - ]); - - return { - id: targetPubkey, - following: source ? isFollowing(source, targetPubkey) : false, - showing_reblogs: true, - notifying: false, - followed_by: target ? isFollowing(target, sourcePubkey) : false, - blocking: false, - blocked_by: false, - muting: false, - muting_notifications: false, - requested: false, - domain_blocking: false, - endorsed: false, - }; -} - -function toNotification(event: Event, viewerPubkey?: string) { - switch (event.kind) { - case 1: - return toNotificationMention(event as Event<1>, viewerPubkey); - } -} - -async function toNotificationMention(event: Event<1>, viewerPubkey?: string) { - const status = await renderStatus(event, viewerPubkey); - if (!status) return; - - return { - id: event.id, - type: 'mention', - created_at: nostrDate(event.created_at).toISOString(), - account: status.account, - status: status, - }; -} - -export { accountFromPubkey, toNotification, toRelationship };