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 { 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));

View File

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

View File

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

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 * 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<Nip05 | undefined> {
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 `<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) {
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 };