mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Add nip05 and favicon results to the database, make renderAccount synchronous
This commit is contained in:
parent
c476596d0a
commit
d9b0bc1437
8 changed files with 133 additions and 35 deletions
|
|
@ -5,6 +5,7 @@ import { NPostgresSchema } from '@nostrify/db';
|
||||||
export interface DittoTables extends NPostgresSchema {
|
export interface DittoTables extends NPostgresSchema {
|
||||||
auth_tokens: AuthTokenRow;
|
auth_tokens: AuthTokenRow;
|
||||||
author_stats: AuthorStatsRow;
|
author_stats: AuthorStatsRow;
|
||||||
|
domain_favicons: DomainFaviconRow;
|
||||||
event_stats: EventStatsRow;
|
event_stats: EventStatsRow;
|
||||||
pubkey_domains: PubkeyDomainRow;
|
pubkey_domains: PubkeyDomainRow;
|
||||||
event_zaps: EventZapRow;
|
event_zaps: EventZapRow;
|
||||||
|
|
@ -19,6 +20,9 @@ interface AuthorStatsRow {
|
||||||
search: string;
|
search: string;
|
||||||
streak_start: number | null;
|
streak_start: number | null;
|
||||||
streak_end: number | null;
|
streak_end: number | null;
|
||||||
|
nip05: string | null;
|
||||||
|
nip05_domain: string | null;
|
||||||
|
nip05_hostname: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventStatsRow {
|
interface EventStatsRow {
|
||||||
|
|
@ -46,6 +50,12 @@ interface PubkeyDomainRow {
|
||||||
last_updated_at: number;
|
last_updated_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DomainFaviconRow {
|
||||||
|
domain: string;
|
||||||
|
favicon: string;
|
||||||
|
last_updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface EventZapRow {
|
interface EventZapRow {
|
||||||
receipt_id: string;
|
receipt_id: string;
|
||||||
target_event_id: string;
|
target_event_id: string;
|
||||||
|
|
|
||||||
48
src/db/migrations/046_author_stats_nip05.ts
Normal file
48
src/db/migrations/046_author_stats_nip05.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addColumn('nip05', 'varchar(320)')
|
||||||
|
.addColumn('nip05_domain', 'varchar(253)')
|
||||||
|
.addColumn('nip05_hostname', 'varchar(253)')
|
||||||
|
.addColumn('nip05_last_verified_at', 'integer')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addCheckConstraint('author_stats_nip05_domain_lowercase_chk', sql`nip05_domain = lower(nip05_domain)`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addCheckConstraint('author_stats_nip05_hostname_lowercase_chk', sql`nip05_hostname = lower(nip05_hostname)`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.addCheckConstraint('author_stats_nip05_hostname_domain_chk', sql`nip05_hostname like '%' || nip05_domain`)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('author_stats_nip05_domain_idx')
|
||||||
|
.on('author_stats')
|
||||||
|
.column('nip05_domain')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('author_stats_nip05_hostname_idx')
|
||||||
|
.on('author_stats')
|
||||||
|
.column('nip05_hostname')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('author_stats')
|
||||||
|
.dropColumn('nip05')
|
||||||
|
.dropColumn('nip05_domain')
|
||||||
|
.dropColumn('nip05_hostname')
|
||||||
|
.dropColumn('nip05_last_verified_at')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
15
src/db/migrations/047_add_domain_favicons.ts
Normal file
15
src/db/migrations/047_add_domain_favicons.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable('domain_favicons')
|
||||||
|
.addColumn('domain', 'varchar(253)', (col) => col.primaryKey())
|
||||||
|
.addColumn('favicon', 'varchar(2048)', (col) => col.notNull())
|
||||||
|
.addColumn('last_updated_at', 'integer', (col) => col.notNull())
|
||||||
|
.addCheckConstraint('domain_favicons_https_chk', sql`url ~* '^https:\\/\\/'`)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.dropTable('domain_favicons').execute();
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,11 @@ export interface AuthorStats {
|
||||||
notes_count: number;
|
notes_count: number;
|
||||||
streak_start?: number;
|
streak_start?: number;
|
||||||
streak_end?: number;
|
streak_end?: number;
|
||||||
|
nip05?: string;
|
||||||
|
nip05_domain?: string;
|
||||||
|
nip05_hostname?: string;
|
||||||
|
nip05_last_verified_at?: number;
|
||||||
|
favicon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ditto internal stats for the event. */
|
/** Ditto internal stats for the event. */
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,30 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
cache.push(event);
|
cache.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authorStats = await gatherAuthorStats(cache, kysely as Kysely<DittoTables>);
|
||||||
|
const eventStats = await gatherEventStats(cache, kysely as Kysely<DittoTables>);
|
||||||
|
|
||||||
|
const domains = authorStats.reduce((result, { nip05_hostname }) => {
|
||||||
|
if (nip05_hostname) result.add(nip05_hostname);
|
||||||
|
return result;
|
||||||
|
}, new Set<string>());
|
||||||
|
|
||||||
|
const favicons = (
|
||||||
|
await kysely
|
||||||
|
.selectFrom('domain_favicons')
|
||||||
|
.select(['domain', 'favicon'])
|
||||||
|
.where('domain', 'in', [...domains])
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
.reduce((result, { domain, favicon }) => {
|
||||||
|
result[domain] = favicon;
|
||||||
|
return result;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
authors: await gatherAuthorStats(cache, kysely as Kysely<DittoTables>),
|
authors: authorStats,
|
||||||
events: await gatherEventStats(cache, kysely as Kysely<DittoTables>),
|
events: eventStats,
|
||||||
|
favicons,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dedupe events.
|
// Dedupe events.
|
||||||
|
|
@ -85,7 +106,11 @@ async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
|
||||||
export function assembleEvents(
|
export function assembleEvents(
|
||||||
a: DittoEvent[],
|
a: DittoEvent[],
|
||||||
b: DittoEvent[],
|
b: DittoEvent[],
|
||||||
stats: { authors: DittoTables['author_stats'][]; events: DittoTables['event_stats'][] },
|
stats: {
|
||||||
|
authors: DittoTables['author_stats'][];
|
||||||
|
events: DittoTables['event_stats'][];
|
||||||
|
favicons: Record<string, string>;
|
||||||
|
},
|
||||||
): DittoEvent[] {
|
): DittoEvent[] {
|
||||||
const admin = Conf.pubkey;
|
const admin = Conf.pubkey;
|
||||||
|
|
||||||
|
|
@ -94,6 +119,10 @@ export function assembleEvents(
|
||||||
...stat,
|
...stat,
|
||||||
streak_start: stat.streak_start ?? undefined,
|
streak_start: stat.streak_start ?? undefined,
|
||||||
streak_end: stat.streak_end ?? undefined,
|
streak_end: stat.streak_end ?? undefined,
|
||||||
|
nip05: stat.nip05 ?? undefined,
|
||||||
|
nip05_domain: stat.nip05_domain ?? undefined,
|
||||||
|
nip05_hostname: stat.nip05_hostname ?? undefined,
|
||||||
|
favicon: stats.favicons[stat.nip05_hostname!],
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
}, {} as Record<string, DittoEvent['author_stats']>);
|
}, {} as Record<string, DittoEvent['author_stats']>);
|
||||||
|
|
@ -390,13 +419,10 @@ async function gatherAuthorStats(
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
pubkey: row.pubkey,
|
...row,
|
||||||
followers_count: Math.max(0, row.followers_count),
|
followers_count: Math.max(0, row.followers_count),
|
||||||
following_count: Math.max(0, row.following_count),
|
following_count: Math.max(0, row.following_count),
|
||||||
notes_count: Math.max(0, row.notes_count),
|
notes_count: Math.max(0, row.notes_count),
|
||||||
search: row.search,
|
|
||||||
streak_start: row.streak_start,
|
|
||||||
streak_end: row.streak_end,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { SimpleLRU } from '@/utils/SimpleLRU.ts';
|
||||||
import { Nip05, parseNip05 } from '@/utils.ts';
|
import { Nip05, parseNip05 } from '@/utils.ts';
|
||||||
import { fetchWorker } from '@/workers/fetch.ts';
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
|
|
||||||
const nip05Cache = new SimpleLRU<string, nip19.ProfilePointer>(
|
export const nip05Cache = new SimpleLRU<string, nip19.ProfilePointer>(
|
||||||
async (nip05, { signal }) => {
|
async (nip05, { signal }) => {
|
||||||
const tld = tldts.parse(nip05);
|
const tld = tldts.parse(nip05);
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ const nip05Cache = new SimpleLRU<string, nip19.ProfilePointer>(
|
||||||
{ ...Conf.caches.nip05, gauge: cachedNip05sSizeGauge },
|
{ ...Conf.caches.nip05, gauge: cachedNip05sSizeGauge },
|
||||||
);
|
);
|
||||||
|
|
||||||
async function localNip05Lookup(store: NStore, localpart: string): Promise<nip19.ProfilePointer | undefined> {
|
export async function localNip05Lookup(store: NStore, localpart: string): Promise<nip19.ProfilePointer | undefined> {
|
||||||
const [grant] = await store.query([{
|
const [grant] = await store.query([{
|
||||||
kinds: [30360],
|
kinds: [30360],
|
||||||
'#d': [`${localpart}@${Conf.url.host}`],
|
'#d': [`${localpart}@${Conf.url.host}`],
|
||||||
|
|
@ -76,5 +76,3 @@ export async function parseAndVerifyNip05(
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { localNip05Lookup, nip05Cache };
|
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,9 @@ export async function countAuthorStats(
|
||||||
search,
|
search,
|
||||||
streak_start: null,
|
streak_start: null,
|
||||||
streak_end: null,
|
streak_end: null,
|
||||||
|
nip05: null,
|
||||||
|
nip05_domain: null,
|
||||||
|
nip05_hostname: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,9 @@ import { MastodonAccount } from '@/entities/MastodonAccount.ts';
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { metadataSchema } from '@/schemas/nostr.ts';
|
import { metadataSchema } from '@/schemas/nostr.ts';
|
||||||
import { getLnurl } from '@/utils/lnurl.ts';
|
import { getLnurl } from '@/utils/lnurl.ts';
|
||||||
import { parseAndVerifyNip05 } from '@/utils/nip05.ts';
|
|
||||||
import { parseNoteContent } from '@/utils/note.ts';
|
import { parseNoteContent } from '@/utils/note.ts';
|
||||||
import { getTagSet } from '@/utils/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { faviconCache } from '@/utils/favicon.ts';
|
import { nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
||||||
import { nostrDate, nostrNow } from '@/utils.ts';
|
|
||||||
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
||||||
|
|
||||||
type ToAccountOpts = {
|
type ToAccountOpts = {
|
||||||
|
|
@ -20,16 +18,14 @@ type ToAccountOpts = {
|
||||||
withSource?: false;
|
withSource?: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function renderAccount(
|
function renderAccount(event: Omit<DittoEvent, 'id' | 'sig'>, opts: ToAccountOpts = {}): MastodonAccount {
|
||||||
event: Omit<DittoEvent, 'id' | 'sig'>,
|
|
||||||
opts: ToAccountOpts = {},
|
|
||||||
signal = AbortSignal.timeout(3000),
|
|
||||||
): Promise<MastodonAccount> {
|
|
||||||
const { pubkey } = event;
|
const { pubkey } = event;
|
||||||
|
|
||||||
|
const stats = event.author_stats;
|
||||||
const names = getTagSet(event.user?.tags ?? [], 'n');
|
const names = getTagSet(event.user?.tags ?? [], 'n');
|
||||||
|
|
||||||
if (names.has('disabled')) {
|
if (names.has('disabled')) {
|
||||||
const account = await accountFromPubkey(pubkey, opts);
|
const account = accountFromPubkey(pubkey, opts);
|
||||||
account.pleroma.deactivated = true;
|
account.pleroma.deactivated = true;
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
@ -48,17 +44,14 @@ async function renderAccount(
|
||||||
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
const nprofile = nip19.nprofileEncode({ pubkey, relays: [Conf.relay] });
|
const nprofile = nip19.nprofileEncode({ pubkey, relays: [Conf.relay] });
|
||||||
const parsed05 = await parseAndVerifyNip05(nip05, pubkey, signal);
|
const parsed05 = stats?.nip05 ? parseNip05(stats.nip05) : undefined;
|
||||||
const acct = parsed05?.handle || npub;
|
const acct = parsed05?.handle || npub;
|
||||||
|
|
||||||
let favicon: URL | undefined;
|
let favicon: string | undefined = stats?.favicon;
|
||||||
if (parsed05?.domain) {
|
if (!favicon && parsed05) {
|
||||||
try {
|
favicon = new URL('/favicon.ico', `https://${parsed05.domain}/`).toString();
|
||||||
favicon = await faviconCache.fetch(parsed05.domain, { signal });
|
|
||||||
} catch {
|
|
||||||
favicon = new URL('/favicon.ico', `https://${parsed05.domain}/`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { html } = parseNoteContent(about || '', []);
|
const { html } = parseNoteContent(about || '', []);
|
||||||
|
|
||||||
const fields = _fields
|
const fields = _fields
|
||||||
|
|
@ -70,8 +63,8 @@ async function renderAccount(
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
let streakDays = 0;
|
let streakDays = 0;
|
||||||
let streakStart = event.author_stats?.streak_start ?? null;
|
let streakStart = stats?.streak_start ?? null;
|
||||||
let streakEnd = event.author_stats?.streak_end ?? null;
|
let streakEnd = stats?.streak_end ?? null;
|
||||||
const { streakWindow } = Conf;
|
const { streakWindow } = Conf;
|
||||||
|
|
||||||
if (streakStart && streakEnd) {
|
if (streakStart && streakEnd) {
|
||||||
|
|
@ -97,8 +90,8 @@ async function renderAccount(
|
||||||
emojis: renderEmojis(event),
|
emojis: renderEmojis(event),
|
||||||
fields: fields.map((field) => ({ ...field, value: parseNoteContent(field.value, []).html })),
|
fields: fields.map((field) => ({ ...field, value: parseNoteContent(field.value, []).html })),
|
||||||
follow_requests_count: 0,
|
follow_requests_count: 0,
|
||||||
followers_count: event.author_stats?.followers_count ?? 0,
|
followers_count: stats?.followers_count ?? 0,
|
||||||
following_count: event.author_stats?.following_count ?? 0,
|
following_count: stats?.following_count ?? 0,
|
||||||
fqn: parsed05?.handle || npub,
|
fqn: parsed05?.handle || npub,
|
||||||
header: banner,
|
header: banner,
|
||||||
header_static: banner,
|
header_static: banner,
|
||||||
|
|
@ -122,7 +115,7 @@ async function renderAccount(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
statuses_count: event.author_stats?.notes_count ?? 0,
|
statuses_count: stats?.notes_count ?? 0,
|
||||||
uri: Conf.local(`/users/${acct}`),
|
uri: Conf.local(`/users/${acct}`),
|
||||||
url: Conf.local(`/@${acct}`),
|
url: Conf.local(`/@${acct}`),
|
||||||
username: parsed05?.nickname || npub.substring(0, 8),
|
username: parsed05?.nickname || npub.substring(0, 8),
|
||||||
|
|
@ -144,7 +137,7 @@ async function renderAccount(
|
||||||
is_local: parsed05?.domain === Conf.url.host,
|
is_local: parsed05?.domain === Conf.url.host,
|
||||||
settings_store: opts.withSource ? opts.settingsStore : undefined,
|
settings_store: opts.withSource ? opts.settingsStore : undefined,
|
||||||
tags: [...getTagSet(event.user?.tags ?? [], 't')],
|
tags: [...getTagSet(event.user?.tags ?? [], 't')],
|
||||||
favicon: favicon?.toString(),
|
favicon,
|
||||||
},
|
},
|
||||||
nostr: {
|
nostr: {
|
||||||
pubkey,
|
pubkey,
|
||||||
|
|
@ -154,7 +147,7 @@ async function renderAccount(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}): Promise<MastodonAccount> {
|
function accountFromPubkey(pubkey: string, opts: ToAccountOpts = {}): MastodonAccount {
|
||||||
const event: UnsignedEvent = {
|
const event: UnsignedEvent = {
|
||||||
kind: 0,
|
kind: 0,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue