diff --git a/src/entities/MastodonAccount.ts b/src/entities/MastodonAccount.ts new file mode 100644 index 00000000..27cad244 --- /dev/null +++ b/src/entities/MastodonAccount.ts @@ -0,0 +1,58 @@ +/** Mastodon account entity, including supported extensions from Pleroma, etc. */ +export interface MastodonAccount { + id: string; + acct: string; + avatar: string; + avatar_static: string; + bot: boolean; + created_at: string; + discoverable: boolean; + display_name: string; + emojis: { + shortcode: string; + static_url: string; + url: string; + }[]; + fields: unknown[]; + follow_requests_count: number; + followers_count: number; + following_count: number; + fqn: string; + header: string; + header_static: string; + last_status_at: string | null; + locked: boolean; + note: string; + roles: unknown[]; + source?: { + fields: unknown[]; + language: string; + note: string; + privacy: string; + sensitive: boolean; + follow_requests_count: number; + nostr: { + nip05?: string; + }; + }; + statuses_count: number; + url: string; + username: string; + ditto: { + accepts_zaps: boolean; + }; + pleroma: { + deactivated: boolean; + is_admin: boolean; + is_moderator: boolean; + is_suggested: boolean; + is_local: boolean; + settings_store: unknown; + tags: string[]; + }; + nostr: { + pubkey: string; + lud16?: string; + }; + website?: string; +} diff --git a/src/entities/MastodonMention.ts b/src/entities/MastodonMention.ts new file mode 100644 index 00000000..e6140024 --- /dev/null +++ b/src/entities/MastodonMention.ts @@ -0,0 +1,6 @@ +export interface MastodonMention { + acct: string; + id: string; + url: string; + username: string; +} diff --git a/src/entities/MastodonStatus.ts b/src/entities/MastodonStatus.ts new file mode 100644 index 00000000..1fcbcacb --- /dev/null +++ b/src/entities/MastodonStatus.ts @@ -0,0 +1,42 @@ +import { MastodonAccount } from '@/entities/MastodonAccount.ts'; +import { PreviewCard } from '@/entities/PreviewCard.ts'; + +export interface MastodonStatus { + id: string; + account: MastodonAccount; + card: PreviewCard | null; + content: string; + created_at: string; + in_reply_to_id: string | null; + in_reply_to_account_id: string | null; + sensitive: boolean; + spoiler_text: string; + visibility: string; + language: string | null; + replies_count: number; + reblogs_count: number; + favourites_count: number; + zaps_amount: number; + favourited: boolean; + reblogged: boolean; + muted: boolean; + bookmarked: boolean; + pinned: boolean; + reblog: MastodonStatus | null; + application: unknown; + media_attachments: unknown[]; + mentions: unknown[]; + tags: unknown[]; + emojis: unknown[]; + poll: unknown; + quote?: MastodonStatus | null; + quote_id: string | null; + uri: string; + url: string; + zapped: boolean; + pleroma: { + emoji_reactions: { name: string; count: number; me: boolean }[]; + expires_at?: string; + quotes_count: number; + }; +} diff --git a/src/entities/PreviewCard.ts b/src/entities/PreviewCard.ts new file mode 100644 index 00000000..2decf926 --- /dev/null +++ b/src/entities/PreviewCard.ts @@ -0,0 +1,16 @@ +export 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; +} diff --git a/src/utils/unfurl.ts b/src/utils/unfurl.ts index e5ae428d..41ffcd3a 100644 --- a/src/utils/unfurl.ts +++ b/src/utils/unfurl.ts @@ -3,28 +3,12 @@ import Debug from '@soapbox/stickynotes/debug'; import DOMPurify from 'isomorphic-dompurify'; import { unfurl } from 'unfurl.js'; +import { PreviewCard } from '@/entities/PreviewCard.ts'; import { Time } from '@/utils/time.ts'; import { fetchWorker } from '@/workers/fetch.ts'; const debug = Debug('ditto:unfurl'); -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 { debug(`Unfurling ${url}...`); try { diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 99f69f00..5de6c389 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -3,6 +3,7 @@ import { escape } from 'entities'; import { nip19, UnsignedEvent } from 'nostr-tools'; import { Conf } from '@/config.ts'; +import { MastodonAccount } from '@/entities/MastodonAccount.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getLnurl } from '@/utils/lnurl.ts'; import { nip05Cache } from '@/utils/nip05.ts'; @@ -17,10 +18,17 @@ interface ToAccountOpts { async function renderAccount( event: Omit, opts: ToAccountOpts = {}, -) { +): Promise { const { withSource = false } = opts; const { pubkey } = event; + const names = getTagSet(event.user?.tags ?? [], 'n'); + if (names.has('disabled') || names.has('suspended')) { + const account = await accountFromPubkey(pubkey, opts); + account.pleroma.deactivated = true; + return account; + } + const { name, nip05, @@ -34,7 +42,6 @@ async function renderAccount( const npub = nip19.npubEncode(pubkey); const parsed05 = await parseAndVerifyNip05(nip05, pubkey); - const names = getTagSet(event.user?.tags ?? [], 'n'); return { id: pubkey, @@ -77,6 +84,7 @@ async function renderAccount( accepts_zaps: Boolean(getLnurl({ lud06, lud16 })), }, pleroma: { + deactivated: names.has('disabled') || names.has('suspended'), is_admin: names.has('admin'), is_moderator: names.has('admin') || names.has('moderator'), is_suggested: names.has('suggested'), @@ -92,7 +100,7 @@ async function renderAccount( }; } -function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}) { +function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}): Promise { const event: UnsignedEvent = { kind: 0, pubkey, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index a0874b3a..4408c607 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -2,6 +2,8 @@ import { NostrEvent } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; import { Conf } from '@/config.ts'; +import { MastodonMention } from '@/entities/MastodonMention.ts'; +import { MastodonStatus } from '@/entities/MastodonStatus.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { Storages } from '@/storages.ts'; import { nostrDate } from '@/utils.ts'; @@ -17,7 +19,7 @@ interface RenderStatusOpts { depth?: number; } -async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise { +async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise { const { viewerPubkey, depth = 1 } = opts; if (depth > 2 || depth < 0) return; @@ -130,12 +132,14 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< }; } -async function renderReblog(event: DittoEvent, opts: RenderStatusOpts) { +async function renderReblog(event: DittoEvent, opts: RenderStatusOpts): Promise { const { viewerPubkey } = opts; if (!event.repost) return; const status = await renderStatus(event, {}); // omit viewerPubkey intentionally - const reblog = await renderStatus(event.repost, { viewerPubkey }); + if (!status) return; + + const reblog = await renderStatus(event.repost, { viewerPubkey }) ?? null; return { ...status, @@ -145,7 +149,7 @@ async function renderReblog(event: DittoEvent, opts: RenderStatusOpts) { }; } -async function toMention(pubkey: string, event?: NostrEvent) { +async function toMention(pubkey: string, event?: NostrEvent): Promise { const account = event ? await renderAccount(event) : undefined; if (account) { @@ -166,9 +170,7 @@ async function toMention(pubkey: string, event?: NostrEvent) { } } -type Mention = Awaited>; - -function buildInlineRecipients(mentions: Mention[]): string { +function buildInlineRecipients(mentions: MastodonMention[]): string { if (!mentions.length) return ''; const elements = mentions.reduce((acc, { url, username }) => {