Add views/mastodon/accounts.ts, views/mastodon/emojis.ts

This commit is contained in:
Alex Gleason 2023-10-06 15:28:02 -05:00
parent cb1141784e
commit 0b77e7d888
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
6 changed files with 138 additions and 121 deletions

View file

@ -11,7 +11,8 @@ import { isFollowing, lookupAccount, nostrNow, Time } from '@/utils.ts';
import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; import { paginated, paginationSchema, parseBody } from '@/utils/web.ts';
import { createEvent } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts';
import { renderEventAccounts } from '@/views.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 const usernameSchema = z
.string().min(1).max(30) .string().min(1).max(30)
@ -60,7 +61,7 @@ const verifyCredentialsController: AppController = async (c) => {
const event = await getAuthor(pubkey); const event = await getAuthor(pubkey);
if (event) { if (event) {
return c.json(await toAccount(event, { withSource: true })); return c.json(await renderAccount(event, { withSource: true }));
} else { } else {
return c.json(await accountFromPubkey(pubkey, { withSource: true })); return c.json(await accountFromPubkey(pubkey, { withSource: true }));
} }
@ -71,7 +72,7 @@ const accountController: AppController = async (c) => {
const event = await getAuthor(pubkey); const event = await getAuthor(pubkey);
if (event) { if (event) {
return c.json(await toAccount(event)); return c.json(await renderAccount(event));
} }
return c.json({ error: 'Could not find user.' }, 404); return c.json({ error: 'Could not find user.' }, 404);
@ -86,7 +87,7 @@ const accountLookupController: AppController = async (c) => {
const event = await lookupAccount(decodeURIComponent(acct)); const event = await lookupAccount(decodeURIComponent(acct));
if (event) { if (event) {
return c.json(await toAccount(event)); return c.json(await renderAccount(event));
} }
return c.json({ error: 'Could not find user.' }, 404); return c.json({ error: 'Could not find user.' }, 404);
@ -101,7 +102,7 @@ const accountSearchController: AppController = async (c) => {
const event = await lookupAccount(decodeURIComponent(q)); const event = await lookupAccount(decodeURIComponent(q));
if (event) { if (event) {
return c.json([await toAccount(event)]); return c.json([await renderAccount(event)]);
} }
return c.json([]); return c.json([]);
@ -199,7 +200,7 @@ const updateCredentialsController: AppController = async (c) => {
tags: [], tags: [],
}, c); }, c);
const account = await toAccount(event); const account = await renderAccount(event);
return c.json(account); return c.json(account);
}; };
@ -237,7 +238,7 @@ const followingController: AppController = async (c) => {
// TODO: pagination by offset. // TODO: pagination by offset.
const accounts = await Promise.all(pubkeys.map(async (pubkey) => { const accounts = await Promise.all(pubkeys.map(async (pubkey) => {
const event = await getAuthor(pubkey); const event = await getAuthor(pubkey);
return event ? await toAccount(event) : undefined; return event ? await renderAccount(event) : undefined;
})); }));
return c.json(accounts.filter(Boolean)); return c.json(accounts.filter(Boolean));

View file

@ -6,7 +6,8 @@ import { booleanParamSchema } from '@/schema.ts';
import { nostrIdSchema } from '@/schemas/nostr.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts';
import { dedupeEvents, Time } from '@/utils.ts'; import { dedupeEvents, Time } from '@/utils.ts';
import { lookupNip05Cached } from '@/utils/nip05.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. */ /** Matches NIP-05 names with or without an @ in front. */
const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/; const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/;
@ -44,7 +45,7 @@ const searchController: AppController = async (c) => {
Promise.all( Promise.all(
results results
.filter((event): event is Event<0> => event.kind === 0) .filter((event): event is Event<0> => event.kind === 0)
.map((event) => toAccount(event)), .map((event) => renderAccount(event)),
), ),
Promise.all( Promise.all(
results results

View file

@ -2,7 +2,7 @@ import { AppContext } from '@/app.ts';
import { type Filter } from '@/deps.ts'; import { type Filter } from '@/deps.ts';
import * as mixer from '@/mixer.ts'; import * as mixer from '@/mixer.ts';
import { getAuthor } from '@/queries.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'; import { paginated } from '@/utils/web.ts';
/** Render account objects for the author of each event. */ /** 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 accounts = await Promise.all([...pubkeys].map(async (pubkey) => {
const author = await getAuthor(pubkey); const author = await getAuthor(pubkey);
if (author) { if (author) {
return toAccount(author); return renderAccount(author);
} }
})); }));

View file

@ -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<Nip05 | undefined> {
if (nip05 && await verifyNip05Cached(nip05, pubkey)) {
return parseNip05(nip05);
}
}
export { accountFromPubkey, renderAccount };

View file

@ -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 };

View file

@ -2,108 +2,19 @@ import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ad
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import * as eventsDB from '@/db/events.ts'; import * as eventsDB from '@/db/events.ts';
import { findUser } from '@/db/users.ts'; import { type Event, findReplyTag, nip19 } from '@/deps.ts';
import { type Event, findReplyTag, lodash, nip19, type UnsignedEvent } from '@/deps.ts';
import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts';
import { getAuthor, getFollowedPubkeys, getFollows } from '@/queries.ts'; import { getAuthor, getFollows } from '@/queries.ts';
import { filteredArray } from '@/schema.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts';
import { emojiTagSchema, jsonMediaDataSchema, jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { isFollowing, nostrDate } from '@/utils.ts';
import { isFollowing, type Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
import { verifyNip05Cached } from '@/utils/nip05.ts';
import { unfurlCardCached } from '@/utils/unfurl.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts';
import { renderEmojis } from '@/views/mastodon/emojis.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<Nip05 | undefined> {
if (nip05 && await verifyNip05Cached(nip05, pubkey)) {
return parseNip05(nip05);
}
}
async function toMention(pubkey: string) { async function toMention(pubkey: string) {
const profile = await getAuthor(pubkey); const profile = await getAuthor(pubkey);
const account = profile ? await toAccount(profile) : undefined; const account = profile ? await renderAccount(profile) : undefined;
if (account) { if (account) {
return { return {
@ -125,7 +36,7 @@ async function toMention(pubkey: string) {
async function toStatus(event: Event<1>, viewerPubkey?: string) { async function toStatus(event: Event<1>, viewerPubkey?: string) {
const profile = await getAuthor(event.pubkey); 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); const replyTag = findReplyTag(event);
@ -191,7 +102,7 @@ async function toStatus(event: Event<1>, viewerPubkey?: string) {
media_attachments: media.map(renderAttachment), media_attachments: media.map(renderAttachment),
mentions, mentions,
tags: [], tags: [],
emojis: toEmojis(event), emojis: renderEmojis(event),
poll: null, poll: null,
uri: Conf.local(`/posts/${event.id}`), uri: Conf.local(`/posts/${event.id}`),
url: Conf.local(`/posts/${event.id}`), url: Conf.local(`/posts/${event.id}`),
@ -212,17 +123,6 @@ function buildInlineRecipients(mentions: Mention[]): string {
return `<span class="recipients-inline">${elements.join(' ')} </span>`; return `<span class="recipients-inline">${elements.join(' ')} </span>`;
} }
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) { async function toRelationship(sourcePubkey: string, targetPubkey: string) {
const [source, target] = await Promise.all([ const [source, target] = await Promise.all([
getFollows(sourcePubkey), 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 };