mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Add views/mastodon/accounts.ts, views/mastodon/emojis.ts
This commit is contained in:
parent
cb1141784e
commit
0b77e7d888
6 changed files with 138 additions and 121 deletions
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
96
src/views/mastodon/accounts.ts
Normal file
96
src/views/mastodon/accounts.ts
Normal 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 };
|
||||||
19
src/views/mastodon/emojis.ts
Normal file
19
src/views/mastodon/emojis.ts
Normal 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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue