From 0b77e7d8883f70b725031f7f23ec376e6e17c819 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 Oct 2023 15:28:02 -0500 Subject: [PATCH] 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 };