mirror of
https://github.com/aljazceru/ditto.git
synced 2025-12-27 10:14:39 +01:00
Merge remote-tracking branch 'origin/main' into feat-persist-user-preference
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import { NostrFilter } from '@nostrify/nostrify';
|
||||
import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { nip19 } from '@/deps.ts';
|
||||
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
|
||||
import { booleanParamSchema, fileSchema } from '@/schema.ts';
|
||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||
import { eventsDB, searchStore } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts';
|
||||
import { uploadFile } from '@/upload.ts';
|
||||
import { uploadFile } from '@/utils/upload.ts';
|
||||
import { nostrNow } from '@/utils.ts';
|
||||
import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||
import { lookupAccount } from '@/utils/lookup.ts';
|
||||
@@ -18,7 +17,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
import { renderRelationship } from '@/views/mastodon/relationships.ts';
|
||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { APISigner } from '@/signers/APISigner.ts';
|
||||
import { bech32ToPubkey } from '@/utils.ts';
|
||||
|
||||
const usernameSchema = z
|
||||
.string().min(1).max(30)
|
||||
@@ -30,7 +29,7 @@ const createAccountSchema = z.object({
|
||||
});
|
||||
|
||||
const createAccountController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const result = createAccountSchema.safeParse(await c.req.json());
|
||||
|
||||
if (!result.success) {
|
||||
@@ -46,28 +45,32 @@ const createAccountController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
const verifyCredentialsController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const signer = c.get('signer')!;
|
||||
const pubkey = await signer.getPublicKey();
|
||||
|
||||
const event = await getAuthor(pubkey, { relations: ['author_stats'] });
|
||||
if (event) {
|
||||
const account = await renderAccount(event, { withSource: true });
|
||||
const eventsDB = await Storages.db();
|
||||
|
||||
const [userPreferencesEvent] = await eventsDB.query([{
|
||||
const [author, [settingsStore]] = await Promise.all([
|
||||
getAuthor(pubkey, { signal: AbortSignal.timeout(5000) }),
|
||||
|
||||
eventsDB.query([{
|
||||
authors: [pubkey],
|
||||
kinds: [30078],
|
||||
'#d': ['pub.ditto.pleroma_settings_store'],
|
||||
limit: 1,
|
||||
}]);
|
||||
if (userPreferencesEvent) {
|
||||
const signer = new APISigner(c);
|
||||
const userPreference = JSON.parse(await signer.nip44.decrypt(pubkey, userPreferencesEvent.content));
|
||||
(account.pleroma as any).settings_store = userPreference;
|
||||
}
|
||||
}]),
|
||||
]);
|
||||
|
||||
return c.json(account);
|
||||
} else {
|
||||
return c.json(await accountFromPubkey(pubkey, { withSource: true }));
|
||||
const account = author
|
||||
? await renderAccount(author, { withSource: true })
|
||||
: await accountFromPubkey(pubkey, { withSource: true });
|
||||
|
||||
if (settingsStore) {
|
||||
const data = await signer.nip44!.decrypt(pubkey, settingsStore.content);
|
||||
account.pleroma.settings_store = JSON.parse(data);
|
||||
}
|
||||
|
||||
return c.json(account);
|
||||
};
|
||||
|
||||
const accountController: AppController = async (c) => {
|
||||
@@ -92,28 +95,44 @@ const accountLookupController: AppController = async (c) => {
|
||||
if (event) {
|
||||
return c.json(await renderAccount(event));
|
||||
}
|
||||
|
||||
return c.json({ error: 'Could not find user.' }, 404);
|
||||
try {
|
||||
const pubkey = bech32ToPubkey(decodeURIComponent(acct)) as string;
|
||||
return c.json(await accountFromPubkey(pubkey));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return c.json({ error: 'Could not find user.' }, 404);
|
||||
}
|
||||
};
|
||||
|
||||
const accountSearchController: AppController = async (c) => {
|
||||
const q = c.req.query('q');
|
||||
const accountSearchQuerySchema = z.object({
|
||||
q: z.string().transform(decodeURIComponent),
|
||||
resolve: booleanParamSchema.optional().transform(Boolean),
|
||||
following: z.boolean().default(false),
|
||||
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
|
||||
});
|
||||
|
||||
if (!q) {
|
||||
return c.json({ error: 'Missing `q` query parameter.' }, 422);
|
||||
const accountSearchController: AppController = async (c) => {
|
||||
const result = accountSearchQuerySchema.safeParse(c.req.query());
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
if (!result.success) {
|
||||
return c.json({ error: 'Bad request', schema: result.error }, 422);
|
||||
}
|
||||
|
||||
const { q, limit } = result.data;
|
||||
|
||||
const query = decodeURIComponent(q);
|
||||
const store = await Storages.search();
|
||||
|
||||
const [event, events] = await Promise.all([
|
||||
lookupAccount(query),
|
||||
searchStore.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }),
|
||||
store.query([{ kinds: [0], search: query, limit }], { signal }),
|
||||
]);
|
||||
|
||||
const results = await hydrateEvents({
|
||||
events: event ? [event, ...events] : events,
|
||||
storage: eventsDB,
|
||||
signal: c.req.raw.signal,
|
||||
store,
|
||||
signal,
|
||||
});
|
||||
|
||||
if ((results.length < 1) && query.match(/npub1\w+/)) {
|
||||
@@ -132,7 +151,7 @@ const accountSearchController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
const relationshipsController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const ids = z.array(z.string()).safeParse(c.req.queries('id[]'));
|
||||
|
||||
if (!ids.success) {
|
||||
@@ -157,8 +176,10 @@ const accountStatusesController: AppController = async (c) => {
|
||||
const { pinned, limit, exclude_replies, tagged } = accountStatusesQuerySchema.parse(c.req.query());
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const store = await Storages.db();
|
||||
|
||||
if (pinned) {
|
||||
const [pinEvent] = await eventsDB.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal });
|
||||
const [pinEvent] = await store.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal });
|
||||
if (pinEvent) {
|
||||
const pinnedEventIds = getTagSet(pinEvent.tags, 'e');
|
||||
return renderStatuses(c, [...pinnedEventIds].reverse());
|
||||
@@ -179,8 +200,8 @@ const accountStatusesController: AppController = async (c) => {
|
||||
filter['#t'] = [tagged];
|
||||
}
|
||||
|
||||
const events = await eventsDB.query([filter], { signal })
|
||||
.then((events) => hydrateEvents({ events, storage: eventsDB, signal }))
|
||||
const events = await store.query([filter], { signal })
|
||||
.then((events) => hydrateEvents({ events, store, signal }))
|
||||
.then((events) => {
|
||||
if (exclude_replies) {
|
||||
return events.filter((event) => !findReplyTag(event.tags));
|
||||
@@ -188,7 +209,11 @@ const accountStatusesController: AppController = async (c) => {
|
||||
return events;
|
||||
});
|
||||
|
||||
const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })));
|
||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const statuses = await Promise.all(
|
||||
events.map((event) => renderStatus(event, { viewerPubkey })),
|
||||
);
|
||||
return paginated(c, events, statuses);
|
||||
};
|
||||
|
||||
@@ -201,11 +226,12 @@ const updateCredentialsSchema = z.object({
|
||||
bot: z.boolean().optional(),
|
||||
discoverable: z.boolean().optional(),
|
||||
nip05: z.string().optional(),
|
||||
pleroma_settings_store: z.object({ soapbox_fe: z.record(z.string(), z.unknown()) }).optional(),
|
||||
pleroma_settings_store: z.unknown().optional(),
|
||||
});
|
||||
|
||||
const updateCredentialsController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const signer = c.get('signer')!;
|
||||
const pubkey = await signer.getPublicKey();
|
||||
const body = await parseBody(c.req.raw);
|
||||
const result = updateCredentialsSchema.safeParse(body);
|
||||
|
||||
@@ -214,7 +240,7 @@ const updateCredentialsController: AppController = async (c) => {
|
||||
}
|
||||
|
||||
const author = await getAuthor(pubkey);
|
||||
const meta = author ? jsonMetaContentSchema.parse(author.content) : {};
|
||||
const meta = author ? n.json().pipe(n.metadata()).catch({}).parse(author.content) : {};
|
||||
|
||||
const {
|
||||
avatar: avatarFile,
|
||||
@@ -225,8 +251,8 @@ const updateCredentialsController: AppController = async (c) => {
|
||||
} = result.data;
|
||||
|
||||
const [avatar, header] = await Promise.all([
|
||||
avatarFile ? uploadFile(avatarFile, { pubkey }) : undefined,
|
||||
headerFile ? uploadFile(headerFile, { pubkey }) : undefined,
|
||||
avatarFile ? uploadFile(c, avatarFile, { pubkey }) : undefined,
|
||||
headerFile ? uploadFile(c, headerFile, { pubkey }) : undefined,
|
||||
]);
|
||||
|
||||
meta.name = display_name ?? meta.name;
|
||||
@@ -241,55 +267,46 @@ const updateCredentialsController: AppController = async (c) => {
|
||||
tags: [],
|
||||
}, c);
|
||||
|
||||
const pleroma_frontend = result.data.pleroma_settings_store;
|
||||
if (pleroma_frontend) {
|
||||
const signer = new APISigner(c);
|
||||
const account = await renderAccount(event, { withSource: true });
|
||||
const settingsStore = result.data.pleroma_settings_store;
|
||||
|
||||
if (settingsStore) {
|
||||
await createEvent({
|
||||
kind: 30078,
|
||||
tags: [['d', 'pub.ditto.pleroma_settings_store']],
|
||||
content: await signer.nip44.encrypt(pubkey, JSON.stringify(pleroma_frontend)),
|
||||
content: await signer.nip44!.encrypt(pubkey, JSON.stringify(settingsStore)),
|
||||
}, c);
|
||||
}
|
||||
|
||||
const account = await renderAccount(event, { withSource: true });
|
||||
|
||||
const [userPreferencesEvent] = await eventsDB.query([{
|
||||
authors: [pubkey],
|
||||
kinds: [30078],
|
||||
'#d': ['pub.ditto.pleroma_settings_store'],
|
||||
limit: 1,
|
||||
}]);
|
||||
if (userPreferencesEvent) {
|
||||
const signer = new APISigner(c);
|
||||
const userPreference = JSON.parse(await signer.nip44.decrypt(pubkey, userPreferencesEvent.content));
|
||||
(account.pleroma as any).settings_store = userPreference;
|
||||
}
|
||||
account.pleroma.settings_store = settingsStore;
|
||||
|
||||
return c.json(account);
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#follow */
|
||||
const followController: AppController = async (c) => {
|
||||
const sourcePubkey = c.get('pubkey')!;
|
||||
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const targetPubkey = c.req.param('pubkey');
|
||||
|
||||
await updateListEvent(
|
||||
{ kinds: [3], authors: [sourcePubkey] },
|
||||
{ kinds: [3], authors: [sourcePubkey], limit: 1 },
|
||||
(tags) => addTag(tags, ['p', targetPubkey]),
|
||||
c,
|
||||
);
|
||||
|
||||
const relationship = await renderRelationship(sourcePubkey, targetPubkey);
|
||||
relationship.following = true;
|
||||
|
||||
return c.json(relationship);
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#unfollow */
|
||||
const unfollowController: AppController = async (c) => {
|
||||
const sourcePubkey = c.get('pubkey')!;
|
||||
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const targetPubkey = c.req.param('pubkey');
|
||||
|
||||
await updateListEvent(
|
||||
{ kinds: [3], authors: [sourcePubkey] },
|
||||
{ kinds: [3], authors: [sourcePubkey], limit: 1 },
|
||||
(tags) => deleteTag(tags, ['p', targetPubkey]),
|
||||
c,
|
||||
);
|
||||
@@ -311,12 +328,22 @@ const followingController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#block */
|
||||
const blockController: AppController = async (c) => {
|
||||
const sourcePubkey = c.get('pubkey')!;
|
||||
const blockController: AppController = (c) => {
|
||||
return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#unblock */
|
||||
const unblockController: AppController = (c) => {
|
||||
return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#mute */
|
||||
const muteController: AppController = async (c) => {
|
||||
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const targetPubkey = c.req.param('pubkey');
|
||||
|
||||
await updateListEvent(
|
||||
{ kinds: [10000], authors: [sourcePubkey] },
|
||||
{ kinds: [10000], authors: [sourcePubkey], limit: 1 },
|
||||
(tags) => addTag(tags, ['p', targetPubkey]),
|
||||
c,
|
||||
);
|
||||
@@ -325,13 +352,13 @@ const blockController: AppController = async (c) => {
|
||||
return c.json(relationship);
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#unblock */
|
||||
const unblockController: AppController = async (c) => {
|
||||
const sourcePubkey = c.get('pubkey')!;
|
||||
/** https://docs.joinmastodon.org/methods/accounts/#unmute */
|
||||
const unmuteController: AppController = async (c) => {
|
||||
const sourcePubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const targetPubkey = c.req.param('pubkey');
|
||||
|
||||
await updateListEvent(
|
||||
{ kinds: [10000], authors: [sourcePubkey] },
|
||||
{ kinds: [10000], authors: [sourcePubkey], limit: 1 },
|
||||
(tags) => deleteTag(tags, ['p', targetPubkey]),
|
||||
c,
|
||||
);
|
||||
@@ -341,11 +368,13 @@ const unblockController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
const favouritesController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const params = paginationSchema.parse(c.req.query());
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const events7 = await eventsDB.query(
|
||||
const store = await Storages.db();
|
||||
|
||||
const events7 = await store.query(
|
||||
[{ kinds: [7], authors: [pubkey], ...params }],
|
||||
{ signal },
|
||||
);
|
||||
@@ -354,10 +383,14 @@ const favouritesController: AppController = async (c) => {
|
||||
.map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1])
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
const events1 = await eventsDB.query([{ kinds: [1], ids }], { signal })
|
||||
.then((events) => hydrateEvents({ events, storage: eventsDB, signal }));
|
||||
const events1 = await store.query([{ kinds: [1], ids }], { signal })
|
||||
.then((events) => hydrateEvents({ events, store, signal }));
|
||||
|
||||
const statuses = await Promise.all(events1.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })));
|
||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const statuses = await Promise.all(
|
||||
events1.map((event) => renderStatus(event, { viewerPubkey })),
|
||||
);
|
||||
return paginated(c, events1, statuses);
|
||||
};
|
||||
|
||||
@@ -372,9 +405,11 @@ export {
|
||||
followController,
|
||||
followersController,
|
||||
followingController,
|
||||
muteController,
|
||||
relationshipsController,
|
||||
unblockController,
|
||||
unfollowController,
|
||||
unmuteController,
|
||||
updateCredentialsController,
|
||||
verifyCredentialsController,
|
||||
};
|
||||
|
||||
@@ -2,10 +2,12 @@ import { z } from 'zod';
|
||||
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { booleanParamSchema } from '@/schema.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { addTag } from '@/tags.ts';
|
||||
import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts';
|
||||
import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts';
|
||||
import { paginated, paginationSchema } from '@/utils/api.ts';
|
||||
|
||||
const adminAccountQuerySchema = z.object({
|
||||
local: booleanParamSchema.optional(),
|
||||
@@ -38,16 +40,17 @@ const adminAccountsController: AppController = async (c) => {
|
||||
return c.json([]);
|
||||
}
|
||||
|
||||
const store = await Storages.db();
|
||||
const { since, until, limit } = paginationSchema.parse(c.req.query());
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const events = await eventsDB.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal });
|
||||
const events = await store.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal });
|
||||
const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!);
|
||||
const authors = await eventsDB.query([{ kinds: [0], authors: pubkeys }], { signal });
|
||||
const authors = await store.query([{ kinds: [0], authors: pubkeys }], { signal });
|
||||
|
||||
for (const event of events) {
|
||||
const d = event.tags.find(([name]) => name === 'd')?.[1];
|
||||
event.d_author = authors.find((author) => author.pubkey === d);
|
||||
(event as DittoEvent).d_author = authors.find((author) => author.pubkey === d);
|
||||
}
|
||||
|
||||
const accounts = await Promise.all(
|
||||
@@ -57,4 +60,32 @@ const adminAccountsController: AppController = async (c) => {
|
||||
return paginated(c, events, accounts);
|
||||
};
|
||||
|
||||
export { adminAccountsController };
|
||||
const adminAccountActionSchema = z.object({
|
||||
type: z.enum(['none', 'sensitive', 'disable', 'silence', 'suspend']),
|
||||
});
|
||||
|
||||
const adminAccountAction: AppController = async (c) => {
|
||||
const body = await parseBody(c.req.raw);
|
||||
const result = adminAccountActionSchema.safeParse(body);
|
||||
const authorId = c.req.param('id');
|
||||
|
||||
if (!result.success) {
|
||||
return c.json({ error: 'This action is not allowed' }, 403);
|
||||
}
|
||||
|
||||
const { data } = result;
|
||||
|
||||
if (data.type !== 'disable') {
|
||||
return c.json({ error: 'Record invalid' }, 422);
|
||||
}
|
||||
|
||||
await updateListAdminEvent(
|
||||
{ kinds: [10000], authors: [Conf.pubkey], limit: 1 },
|
||||
(tags) => addTag(tags, ['p', authorId]),
|
||||
c,
|
||||
);
|
||||
|
||||
return c.json({}, 200);
|
||||
};
|
||||
|
||||
export { adminAccountAction, adminAccountsController };
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { getTagSet } from '@/tags.ts';
|
||||
import { renderAccounts } from '@/views.ts';
|
||||
import { AppController } from '@/app.ts';
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/blocks/#get */
|
||||
const blocksController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const [event10000] = await eventsDB.query(
|
||||
[{ kinds: [10000], authors: [pubkey], limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
if (event10000) {
|
||||
const pubkeys = getTagSet(event10000.tags, 'p');
|
||||
return renderAccounts(c, [...pubkeys].reverse());
|
||||
} else {
|
||||
return c.json([]);
|
||||
}
|
||||
export const blocksController: AppController = (c) => {
|
||||
return c.json({ error: 'Blocking is not supported by Nostr' }, 422);
|
||||
};
|
||||
|
||||
export { blocksController };
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { getTagSet } from '@/tags.ts';
|
||||
import { renderStatuses } from '@/views.ts';
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
||||
const bookmarksController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const store = await Storages.db();
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const [event10003] = await eventsDB.query(
|
||||
const [event10003] = await store.query(
|
||||
[{ kinds: [10003], authors: [pubkey], limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
@@ -3,19 +3,22 @@ import { z } from 'zod';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
|
||||
const markerSchema = z.enum(['read', 'write']);
|
||||
|
||||
const relaySchema = z.object({
|
||||
url: z.string().url(),
|
||||
read: z.boolean(),
|
||||
write: z.boolean(),
|
||||
marker: markerSchema.optional(),
|
||||
});
|
||||
|
||||
type RelayEntity = z.infer<typeof relaySchema>;
|
||||
|
||||
export const adminRelaysController: AppController = async (c) => {
|
||||
const [event] = await eventsDB.query([
|
||||
const store = await Storages.db();
|
||||
|
||||
const [event] = await store.query([
|
||||
{ kinds: [10002], authors: [Conf.pubkey], limit: 1 },
|
||||
]);
|
||||
|
||||
@@ -27,16 +30,17 @@ export const adminRelaysController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
export const adminSetRelaysController: AppController = async (c) => {
|
||||
const store = await Storages.db();
|
||||
const relays = relaySchema.array().parse(await c.req.json());
|
||||
|
||||
const event = await new AdminSigner().signEvent({
|
||||
kind: 10002,
|
||||
tags: relays.map(({ url, read, write }) => ['r', url, read && write ? '' : read ? 'read' : 'write']),
|
||||
tags: relays.map(({ url, marker }) => marker ? ['r', url, marker] : ['r', url]),
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
await eventsDB.event(event);
|
||||
await store.event(event);
|
||||
|
||||
return c.json(renderRelays(event));
|
||||
};
|
||||
@@ -47,8 +51,7 @@ function renderRelays(event: NostrEvent): RelayEntity[] {
|
||||
if (name === 'r') {
|
||||
const relay: RelayEntity = {
|
||||
url,
|
||||
read: !marker || marker === 'read',
|
||||
write: !marker || marker === 'write',
|
||||
marker: markerSchema.safeParse(marker).success ? marker as 'read' | 'write' : undefined,
|
||||
};
|
||||
acc.push(relay);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { jsonServerMetaSchema } from '@/schemas/nostr.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||
|
||||
const instanceController: AppController = async (c) => {
|
||||
const { host, protocol } = Conf.url;
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const [event] = await eventsDB.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal });
|
||||
const meta = jsonServerMetaSchema.parse(event?.content);
|
||||
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
|
||||
|
||||
/** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */
|
||||
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
||||
|
||||
return c.json({
|
||||
uri: host,
|
||||
title: meta.name ?? 'Ditto',
|
||||
description: meta.about ?? 'Nostr and the Fediverse',
|
||||
short_description: meta.tagline ?? meta.about ?? 'Nostr and the Fediverse',
|
||||
title: meta.name,
|
||||
description: meta.about,
|
||||
short_description: meta.tagline,
|
||||
registrations: true,
|
||||
max_toot_chars: Conf.postCharLimit,
|
||||
configuration: {
|
||||
@@ -43,6 +40,7 @@ const instanceController: AppController = async (c) => {
|
||||
'mastodon_api_streaming',
|
||||
'exposable_reactions',
|
||||
'quote_posting',
|
||||
'v2_suggestions',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -56,7 +54,7 @@ const instanceController: AppController = async (c) => {
|
||||
streaming_api: `${wsProtocol}//${host}`,
|
||||
},
|
||||
version: '0.0.0 (compatible; Ditto 0.0.1)',
|
||||
email: meta.email ?? `postmaster@${host}`,
|
||||
email: meta.email,
|
||||
nostr: {
|
||||
pubkey: Conf.pubkey,
|
||||
relay: `${wsProtocol}//${host}/relay`,
|
||||
|
||||
64
src/controllers/api/markers.ts
Normal file
64
src/controllers/api/markers.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { parseBody } from '@/utils/api.ts';
|
||||
|
||||
const kv = await Deno.openKv();
|
||||
|
||||
type Timeline = 'home' | 'notifications';
|
||||
|
||||
interface Marker {
|
||||
last_read_id: string;
|
||||
version: number;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export const markersController: AppController = async (c) => {
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const timelines = c.req.queries('timeline[]') ?? [];
|
||||
|
||||
const results = await kv.getMany<Marker[]>(
|
||||
timelines.map((timeline) => ['markers', pubkey, timeline]),
|
||||
);
|
||||
|
||||
const marker = results.reduce<Record<string, Marker>>((acc, { key, value }) => {
|
||||
if (value) {
|
||||
const timeline = key[key.length - 1] as string;
|
||||
acc[timeline] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return c.json(marker);
|
||||
};
|
||||
|
||||
const markerDataSchema = z.object({
|
||||
last_read_id: z.string(),
|
||||
});
|
||||
|
||||
export const updateMarkersController: AppController = async (c) => {
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw));
|
||||
const timelines = Object.keys(record) as Timeline[];
|
||||
|
||||
const markers: Record<string, Marker> = {};
|
||||
|
||||
const entries = await kv.getMany<Marker[]>(
|
||||
timelines.map((timeline) => ['markers', pubkey, timeline]),
|
||||
);
|
||||
|
||||
for (const timeline of timelines) {
|
||||
const last = entries.find(({ key }) => key[key.length - 1] === timeline);
|
||||
|
||||
const marker: Marker = {
|
||||
last_read_id: record[timeline]!.last_read_id,
|
||||
version: last?.value ? last.value.version + 1 : 1,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await kv.set(['markers', pubkey, timeline], marker);
|
||||
markers[timeline] = marker;
|
||||
}
|
||||
|
||||
return c.json(markers);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { AppController } from '@/app.ts';
|
||||
import { fileSchema } from '@/schema.ts';
|
||||
import { parseBody } from '@/utils/api.ts';
|
||||
import { renderAttachment } from '@/views/mastodon/attachments.ts';
|
||||
import { uploadFile } from '@/upload.ts';
|
||||
import { uploadFile } from '@/utils/upload.ts';
|
||||
|
||||
const mediaBodySchema = z.object({
|
||||
file: fileSchema,
|
||||
@@ -14,7 +14,7 @@ const mediaBodySchema = z.object({
|
||||
});
|
||||
|
||||
const mediaController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
@@ -24,7 +24,7 @@ const mediaController: AppController = async (c) => {
|
||||
|
||||
try {
|
||||
const { file, description } = result.data;
|
||||
const media = await uploadFile(file, { pubkey, description }, signal);
|
||||
const media = await uploadFile(c, file, { pubkey, description }, signal);
|
||||
return c.json(renderAttachment(media));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
25
src/controllers/api/mutes.ts
Normal file
25
src/controllers/api/mutes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { getTagSet } from '@/tags.ts';
|
||||
import { renderAccounts } from '@/views.ts';
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
||||
const mutesController: AppController = async (c) => {
|
||||
const store = await Storages.db();
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const [event10000] = await store.query(
|
||||
[{ kinds: [10000], authors: [pubkey], limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
if (event10000) {
|
||||
const pubkeys = getTagSet(event10000.tags, 'p');
|
||||
return renderAccounts(c, [...pubkeys].reverse());
|
||||
} else {
|
||||
return c.json([]);
|
||||
}
|
||||
};
|
||||
|
||||
export { mutesController };
|
||||
@@ -1,20 +1,40 @@
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { NostrFilter } from '@nostrify/nostrify';
|
||||
|
||||
import { AppContext, AppController } from '@/app.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { paginated, paginationSchema } from '@/utils/api.ts';
|
||||
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
||||
|
||||
const notificationsController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const { since, until } = paginationSchema.parse(c.req.query());
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const events = await eventsDB.query(
|
||||
[{ kinds: [1], '#p': [pubkey], since, until }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
const statuses = await Promise.all(events.map((event) => renderNotification(event, pubkey)));
|
||||
return paginated(c, events, statuses);
|
||||
return renderNotifications(c, [{ kinds: [1, 6, 7], '#p': [pubkey], since, until }]);
|
||||
};
|
||||
|
||||
async function renderNotifications(c: AppContext, filters: NostrFilter[]) {
|
||||
const store = c.get('store');
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
const events = await store
|
||||
.query(filters, { signal })
|
||||
.then((events) => events.filter((event) => event.pubkey !== pubkey))
|
||||
.then((events) => hydrateEvents({ events, store, signal }));
|
||||
|
||||
if (!events.length) {
|
||||
return c.json([]);
|
||||
}
|
||||
|
||||
const notifications = (await Promise
|
||||
.all(events.map((event) => renderNotification(event, { viewerPubkey: pubkey }))))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!notifications.length) {
|
||||
return c.json([]);
|
||||
}
|
||||
|
||||
return paginated(c, events, notifications);
|
||||
}
|
||||
|
||||
export { notificationsController };
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { encodeBase64 } from '@std/encoding/base64';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { lodash, nip19 } from '@/deps.ts';
|
||||
import { AppController } from '@/app.ts';
|
||||
import { lodash } from '@/deps.ts';
|
||||
import { nostrNow } from '@/utils.ts';
|
||||
import { parseBody } from '@/utils/api.ts';
|
||||
import { getClientConnectUri } from '@/utils/connect.ts';
|
||||
|
||||
const passwordGrantSchema = z.object({
|
||||
grant_type: z.literal('password'),
|
||||
@@ -59,25 +62,16 @@ const createTokenController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
/** Display the OAuth form. */
|
||||
const oauthController: AppController = (c) => {
|
||||
const oauthController: AppController = async (c) => {
|
||||
const encodedUri = c.req.query('redirect_uri');
|
||||
if (!encodedUri) {
|
||||
return c.text('Missing `redirect_uri` query param.', 422);
|
||||
}
|
||||
|
||||
const redirectUri = maybeDecodeUri(encodedUri);
|
||||
const connectUri = await getClientConnectUri(c.req.raw.signal);
|
||||
|
||||
c.res.headers.set(
|
||||
'content-security-policy',
|
||||
"default-src 'self' 'sha256-m2qD6rbE2Ixbo2Bjy2dgQebcotRIAawW7zbmXItIYAM='",
|
||||
);
|
||||
|
||||
return c.html(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Log in with Ditto</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<script>
|
||||
const script = `
|
||||
window.addEventListener('load', function() {
|
||||
if ('nostr' in window) {
|
||||
nostr.getPublicKey().then(function(pubkey) {
|
||||
@@ -86,7 +80,21 @@ const oauthController: AppController = (c) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
`;
|
||||
|
||||
const hash = encodeBase64(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(script)));
|
||||
|
||||
c.res.headers.set(
|
||||
'content-security-policy',
|
||||
`default-src 'self' 'sha256-${hash}'`,
|
||||
);
|
||||
|
||||
return c.html(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Log in with Ditto</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<script>${script}</script>
|
||||
</head>
|
||||
<body>
|
||||
<form id="oauth_form" action="/oauth/authorize" method="post">
|
||||
@@ -95,6 +103,8 @@ const oauthController: AppController = (c) => {
|
||||
<input type="hidden" name="redirect_uri" id="redirect_uri" value="${lodash.escape(redirectUri)}">
|
||||
<button type="submit">Authorize</button>
|
||||
</form>
|
||||
<br>
|
||||
<a href="${lodash.escape(connectUri)}">Nostr Connect</a>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { NSchema as n, NStore } from '@nostrify/nostrify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { createAdminEvent } from '@/utils/api.ts';
|
||||
import { jsonSchema } from '@/schema.ts';
|
||||
|
||||
const frontendConfigController: AppController = async (c) => {
|
||||
const configs = await getConfigs(c.req.raw.signal);
|
||||
const store = await Storages.db();
|
||||
const configs = await getConfigs(store, c.req.raw.signal);
|
||||
const frontendConfig = configs.find(({ group, key }) => group === ':pleroma' && key === ':frontend_configurations');
|
||||
|
||||
if (frontendConfig) {
|
||||
@@ -25,7 +26,8 @@ const frontendConfigController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
const configController: AppController = async (c) => {
|
||||
const configs = await getConfigs(c.req.raw.signal);
|
||||
const store = await Storages.db();
|
||||
const configs = await getConfigs(store, c.req.raw.signal);
|
||||
return c.json({ configs, need_reboot: false });
|
||||
};
|
||||
|
||||
@@ -33,7 +35,8 @@ const configController: AppController = async (c) => {
|
||||
const updateConfigController: AppController = async (c) => {
|
||||
const { pubkey } = Conf;
|
||||
|
||||
const configs = await getConfigs(c.req.raw.signal);
|
||||
const store = await Storages.db();
|
||||
const configs = await getConfigs(store, c.req.raw.signal);
|
||||
const { configs: newConfigs } = z.object({ configs: z.array(configSchema) }).parse(await c.req.json());
|
||||
|
||||
for (const { group, key, value } of newConfigs) {
|
||||
@@ -63,10 +66,10 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => {
|
||||
return c.json({});
|
||||
};
|
||||
|
||||
async function getConfigs(signal: AbortSignal): Promise<PleromaConfig[]> {
|
||||
async function getConfigs(store: NStore, signal: AbortSignal): Promise<PleromaConfig[]> {
|
||||
const { pubkey } = Conf;
|
||||
|
||||
const [event] = await eventsDB.query([{
|
||||
const [event] = await store.query([{
|
||||
kinds: [30078],
|
||||
authors: [pubkey],
|
||||
'#d': ['pub.ditto.pleroma.config'],
|
||||
@@ -75,7 +78,7 @@ async function getConfigs(signal: AbortSignal): Promise<PleromaConfig[]> {
|
||||
|
||||
try {
|
||||
const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content);
|
||||
return jsonSchema.pipe(configSchema.array()).catch([]).parse(decrypted);
|
||||
return n.json().pipe(configSchema.array()).catch([]).parse(decrypted);
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
121
src/controllers/api/reports.ts
Normal file
121
src/controllers/api/reports.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NSchema as n } from '@nostrify/nostrify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { createAdminEvent, createEvent, parseBody } from '@/utils/api.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { renderAdminReport } from '@/views/mastodon/reports.ts';
|
||||
import { renderReport } from '@/views/mastodon/reports.ts';
|
||||
|
||||
const reportSchema = z.object({
|
||||
account_id: n.id(),
|
||||
status_ids: n.id().array().default([]),
|
||||
comment: z.string().max(1000).default(''),
|
||||
category: z.string().default('other'),
|
||||
// TODO: rules_ids[] is not implemented
|
||||
});
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/reports/#post */
|
||||
const reportController: AppController = async (c) => {
|
||||
const store = c.get('store');
|
||||
const body = await parseBody(c.req.raw);
|
||||
const result = reportSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
return c.json(result.error, 422);
|
||||
}
|
||||
|
||||
const {
|
||||
account_id,
|
||||
status_ids,
|
||||
comment,
|
||||
category,
|
||||
} = result.data;
|
||||
|
||||
const tags = [
|
||||
['p', account_id, category],
|
||||
['P', Conf.pubkey],
|
||||
];
|
||||
|
||||
for (const status of status_ids) {
|
||||
tags.push(['e', status, category]);
|
||||
}
|
||||
|
||||
const event = await createEvent({
|
||||
kind: 1984,
|
||||
content: comment,
|
||||
tags,
|
||||
}, c);
|
||||
|
||||
await hydrateEvents({ events: [event], store });
|
||||
return c.json(await renderReport(event));
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/admin/reports/#get */
|
||||
const adminReportsController: AppController = async (c) => {
|
||||
const store = c.get('store');
|
||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const reports = await store.query([{ kinds: [1984], '#P': [Conf.pubkey] }])
|
||||
.then((events) => hydrateEvents({ store, events: events, signal: c.req.raw.signal }))
|
||||
.then((events) =>
|
||||
Promise.all(
|
||||
events.map((event) => renderAdminReport(event, { viewerPubkey })),
|
||||
)
|
||||
);
|
||||
|
||||
return c.json(reports);
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/admin/reports/#get-one */
|
||||
const adminReportController: AppController = async (c) => {
|
||||
const eventId = c.req.param('id');
|
||||
const { signal } = c.req.raw;
|
||||
const store = c.get('store');
|
||||
const pubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const [event] = await store.query([{
|
||||
kinds: [1984],
|
||||
ids: [eventId],
|
||||
limit: 1,
|
||||
}], { signal });
|
||||
|
||||
if (!event) {
|
||||
return c.json({ error: 'This action is not allowed' }, 403);
|
||||
}
|
||||
|
||||
await hydrateEvents({ events: [event], store, signal });
|
||||
|
||||
return c.json(await renderAdminReport(event, { viewerPubkey: pubkey }));
|
||||
};
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/admin/reports/#resolve */
|
||||
const adminReportResolveController: AppController = async (c) => {
|
||||
const eventId = c.req.param('id');
|
||||
const { signal } = c.req.raw;
|
||||
const store = c.get('store');
|
||||
const pubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const [event] = await store.query([{
|
||||
kinds: [1984],
|
||||
ids: [eventId],
|
||||
limit: 1,
|
||||
}], { signal });
|
||||
|
||||
if (!event) {
|
||||
return c.json({ error: 'This action is not allowed' }, 403);
|
||||
}
|
||||
|
||||
await hydrateEvents({ events: [event], store, signal });
|
||||
|
||||
await createAdminEvent({
|
||||
kind: 5,
|
||||
tags: [['e', event.id]],
|
||||
content: 'Report closed.',
|
||||
}, c);
|
||||
|
||||
return c.json(await renderAdminReport(event, { viewerPubkey: pubkey, actionTaken: true }));
|
||||
};
|
||||
|
||||
export { adminReportController, adminReportResolveController, adminReportsController, reportController };
|
||||
@@ -1,11 +1,10 @@
|
||||
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { nip19 } from '@/deps.ts';
|
||||
import { booleanParamSchema } from '@/schema.ts';
|
||||
import { nostrIdSchema } from '@/schemas/nostr.ts';
|
||||
import { searchStore } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { dedupeEvents } from '@/utils.ts';
|
||||
import { nip05Cache } from '@/utils/nip05.ts';
|
||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
@@ -20,7 +19,7 @@ const searchQuerySchema = z.object({
|
||||
type: z.enum(['accounts', 'statuses', 'hashtags']).optional(),
|
||||
resolve: booleanParamSchema.optional().transform(Boolean),
|
||||
following: z.boolean().default(false),
|
||||
account_id: nostrIdSchema.optional(),
|
||||
account_id: n.id().optional(),
|
||||
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
|
||||
});
|
||||
|
||||
@@ -44,6 +43,7 @@ const searchController: AppController = async (c) => {
|
||||
}
|
||||
|
||||
const results = dedupeEvents(events);
|
||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const [accounts, statuses] = await Promise.all([
|
||||
Promise.all(
|
||||
@@ -55,7 +55,7 @@ const searchController: AppController = async (c) => {
|
||||
Promise.all(
|
||||
results
|
||||
.filter((event) => event.kind === 1)
|
||||
.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))
|
||||
.map((event) => renderStatus(event, { viewerPubkey }))
|
||||
.filter(Boolean),
|
||||
),
|
||||
]);
|
||||
@@ -78,7 +78,7 @@ const searchController: AppController = async (c) => {
|
||||
};
|
||||
|
||||
/** Get events for the search params. */
|
||||
function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise<NostrEvent[]> {
|
||||
async function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: AbortSignal): Promise<NostrEvent[]> {
|
||||
if (type === 'hashtags') return Promise.resolve([]);
|
||||
|
||||
const filter: NostrFilter = {
|
||||
@@ -91,8 +91,10 @@ function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: Abort
|
||||
filter.authors = [account_id];
|
||||
}
|
||||
|
||||
return searchStore.query([filter], { signal })
|
||||
.then((events) => hydrateEvents({ events, storage: searchStore, signal }));
|
||||
const store = await Storages.search();
|
||||
|
||||
return store.query([filter], { signal })
|
||||
.then((events) => hydrateEvents({ events, store, signal }));
|
||||
}
|
||||
|
||||
/** Get event kinds to search from `type` query param. */
|
||||
@@ -110,9 +112,10 @@ function typeToKinds(type: SearchQuery['type']): number[] {
|
||||
/** Resolve a searched value into an event, if applicable. */
|
||||
async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise<NostrEvent | undefined> {
|
||||
const filters = await getLookupFilters(query, signal);
|
||||
const store = await Storages.search();
|
||||
|
||||
return searchStore.query(filters, { limit: 1, signal })
|
||||
.then((events) => hydrateEvents({ events, storage: searchStore, signal }))
|
||||
return store.query(filters, { limit: 1, signal })
|
||||
.then((events) => hydrateEvents({ events, store, signal }))
|
||||
.then(([event]) => event);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { NIP05, NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
||||
import ISO6391 from 'iso-639-1';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { DittoDB } from '@/db/DittoDB.ts';
|
||||
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
|
||||
import { ISO6391, nip19 } from '@/deps.ts';
|
||||
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||
import { addTag, deleteTag } from '@/tags.ts';
|
||||
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||
import { renderEventAccounts } from '@/views.ts';
|
||||
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { getLnurl } from '@/utils/lnurl.ts';
|
||||
import { nip05Cache } from '@/utils/nip05.ts';
|
||||
import { asyncReplaceAll } from '@/utils/text.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||
|
||||
const createStatusSchema = z.object({
|
||||
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
||||
@@ -31,6 +32,7 @@ const createStatusSchema = z.object({
|
||||
sensitive: z.boolean().nullish(),
|
||||
spoiler_text: z.string().nullish(),
|
||||
status: z.string().nullish(),
|
||||
to: z.string().array().nullish(),
|
||||
visibility: z.enum(['public', 'unlisted', 'private', 'direct']).nullish(),
|
||||
quote_id: z.string().nullish(),
|
||||
}).refine(
|
||||
@@ -47,7 +49,7 @@ const statusController: AppController = async (c) => {
|
||||
});
|
||||
|
||||
if (event) {
|
||||
return c.json(await renderStatus(event, { viewerPubkey: c.get('pubkey') }));
|
||||
return c.json(await renderStatus(event, { viewerPubkey: await c.get('signer')?.getPublicKey() }));
|
||||
}
|
||||
|
||||
return c.json({ error: 'Event not found.' }, 404);
|
||||
@@ -56,6 +58,7 @@ const statusController: AppController = async (c) => {
|
||||
const createStatusController: AppController = async (c) => {
|
||||
const body = await parseBody(c.req.raw);
|
||||
const result = createStatusSchema.safeParse(body);
|
||||
const kysely = await DittoDB.getInstance();
|
||||
|
||||
if (!result.success) {
|
||||
return c.json({ error: 'Bad request', schema: result.error }, 400);
|
||||
@@ -89,45 +92,58 @@ const createStatusController: AppController = async (c) => {
|
||||
tags.push(['subject', data.spoiler_text]);
|
||||
}
|
||||
|
||||
if (data.media_ids?.length) {
|
||||
const media = await getUnattachedMediaByIds(data.media_ids)
|
||||
.then((media) => media.filter(({ pubkey }) => pubkey === c.get('pubkey')))
|
||||
.then((media) => media.map(({ url, data }) => ['media', url, data]));
|
||||
const media = data.media_ids?.length ? await getUnattachedMediaByIds(kysely, data.media_ids) : [];
|
||||
|
||||
tags.push(...media);
|
||||
}
|
||||
const imeta: string[][] = media.map(({ data }) => {
|
||||
const values: string[] = data.map((tag) => tag.join(' '));
|
||||
return ['imeta', ...values];
|
||||
});
|
||||
|
||||
tags.push(...imeta);
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
|
||||
const content = await asyncReplaceAll(data.status ?? '', /@([\w@+._]+)/g, async (match, username) => {
|
||||
const pubkey = await lookupPubkey(username);
|
||||
if (!pubkey) return match;
|
||||
|
||||
// Content addressing (default)
|
||||
if (!data.to) {
|
||||
pubkeys.add(pubkey);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = nip19.decode(username);
|
||||
if (result.type === 'npub') {
|
||||
tags.push(['p', result.data]);
|
||||
return `nostr:${username}`;
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
} catch (_e) {
|
||||
// do nothing
|
||||
return `nostr:${nip19.npubEncode(pubkey)}`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
|
||||
if (NIP05.regex().test(username)) {
|
||||
const pointer = await nip05Cache.fetch(username);
|
||||
if (pointer) {
|
||||
tags.push(['p', pointer.pubkey]);
|
||||
return `nostr:${nip19.npubEncode(pointer.pubkey)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Explicit addressing
|
||||
for (const to of data.to ?? []) {
|
||||
const pubkey = await lookupPubkey(to);
|
||||
if (pubkey) {
|
||||
pubkeys.add(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
tags.push(['p', pubkey]);
|
||||
}
|
||||
|
||||
for (const match of content.matchAll(/#(\w+)/g)) {
|
||||
tags.push(['t', match[1]]);
|
||||
}
|
||||
|
||||
const mediaUrls: string[] = media
|
||||
.map(({ data }) => data.find(([name]) => name === 'url')?.[1])
|
||||
.filter((url): url is string => Boolean(url));
|
||||
|
||||
const mediaCompat: string = mediaUrls.length ? ['', '', ...mediaUrls].join('\n') : '';
|
||||
|
||||
const event = await createEvent({
|
||||
kind: 1,
|
||||
content,
|
||||
content: content + mediaCompat,
|
||||
tags,
|
||||
}, c);
|
||||
|
||||
@@ -136,17 +152,17 @@ const createStatusController: AppController = async (c) => {
|
||||
if (data.quote_id) {
|
||||
await hydrateEvents({
|
||||
events: [event],
|
||||
storage: eventsDB,
|
||||
store: await Storages.db(),
|
||||
signal: c.req.raw.signal,
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(await renderStatus({ ...event, author }, { viewerPubkey: c.get('pubkey') }));
|
||||
return c.json(await renderStatus({ ...event, author }, { viewerPubkey: await c.get('signer')?.getPublicKey() }));
|
||||
};
|
||||
|
||||
const deleteStatusController: AppController = async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const pubkey = c.get('pubkey');
|
||||
const pubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const event = await getEvent(id, { signal: c.req.raw.signal });
|
||||
|
||||
@@ -170,9 +186,12 @@ const deleteStatusController: AppController = async (c) => {
|
||||
const contextController: AppController = async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
|
||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
async function renderStatuses(events: NostrEvent[]) {
|
||||
const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })));
|
||||
const statuses = await Promise.all(
|
||||
events.map((event) => renderStatus(event, { viewerPubkey })),
|
||||
);
|
||||
return statuses.filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -202,7 +221,7 @@ const favouriteController: AppController = async (c) => {
|
||||
],
|
||||
}, c);
|
||||
|
||||
const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') });
|
||||
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
||||
|
||||
if (status) {
|
||||
status.favourited = true;
|
||||
@@ -241,11 +260,11 @@ const reblogStatusController: AppController = async (c) => {
|
||||
|
||||
await hydrateEvents({
|
||||
events: [reblogEvent],
|
||||
storage: eventsDB,
|
||||
store: await Storages.db(),
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
const status = await renderReblog(reblogEvent, { viewerPubkey: c.get('pubkey') });
|
||||
const status = await renderReblog(reblogEvent, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
||||
|
||||
return c.json(status);
|
||||
};
|
||||
@@ -253,23 +272,28 @@ const reblogStatusController: AppController = async (c) => {
|
||||
/** https://docs.joinmastodon.org/methods/statuses/#unreblog */
|
||||
const unreblogStatusController: AppController = async (c) => {
|
||||
const eventId = c.req.param('id');
|
||||
const pubkey = c.get('pubkey') as string;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const store = await Storages.db();
|
||||
|
||||
const event = await getEvent(eventId, {
|
||||
kind: 1,
|
||||
});
|
||||
if (!event) return c.json({ error: 'Event not found.' }, 404);
|
||||
const [event] = await store.query([{ ids: [eventId], kinds: [1] }]);
|
||||
if (!event) {
|
||||
return c.json({ error: 'Record not found' }, 404);
|
||||
}
|
||||
|
||||
const filters: NostrFilter[] = [{ kinds: [6], authors: [pubkey], '#e': [event.id] }];
|
||||
const [repostedEvent] = await eventsDB.query(filters, { limit: 1 });
|
||||
if (!repostedEvent) return c.json({ error: 'Event not found.' }, 404);
|
||||
const [repostedEvent] = await store.query(
|
||||
[{ kinds: [6], authors: [pubkey], '#e': [event.id], limit: 1 }],
|
||||
);
|
||||
|
||||
if (!repostedEvent) {
|
||||
return c.json({ error: 'Record not found' }, 404);
|
||||
}
|
||||
|
||||
await createEvent({
|
||||
kind: 5,
|
||||
tags: [['e', repostedEvent.id]],
|
||||
}, c);
|
||||
|
||||
return c.json(await renderStatus(event, {}));
|
||||
return c.json(await renderStatus(event, { viewerPubkey: pubkey }));
|
||||
};
|
||||
|
||||
const rebloggedByController: AppController = (c) => {
|
||||
@@ -280,7 +304,7 @@ const rebloggedByController: AppController = (c) => {
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/statuses/#bookmark */
|
||||
const bookmarkController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const eventId = c.req.param('id');
|
||||
|
||||
const event = await getEvent(eventId, {
|
||||
@@ -290,7 +314,7 @@ const bookmarkController: AppController = async (c) => {
|
||||
|
||||
if (event) {
|
||||
await updateListEvent(
|
||||
{ kinds: [10003], authors: [pubkey] },
|
||||
{ kinds: [10003], authors: [pubkey], limit: 1 },
|
||||
(tags) => addTag(tags, ['e', eventId]),
|
||||
c,
|
||||
);
|
||||
@@ -307,7 +331,7 @@ const bookmarkController: AppController = async (c) => {
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/statuses/#unbookmark */
|
||||
const unbookmarkController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const eventId = c.req.param('id');
|
||||
|
||||
const event = await getEvent(eventId, {
|
||||
@@ -317,7 +341,7 @@ const unbookmarkController: AppController = async (c) => {
|
||||
|
||||
if (event) {
|
||||
await updateListEvent(
|
||||
{ kinds: [10003], authors: [pubkey] },
|
||||
{ kinds: [10003], authors: [pubkey], limit: 1 },
|
||||
(tags) => deleteTag(tags, ['e', eventId]),
|
||||
c,
|
||||
);
|
||||
@@ -334,7 +358,7 @@ const unbookmarkController: AppController = async (c) => {
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/statuses/#pin */
|
||||
const pinController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const eventId = c.req.param('id');
|
||||
|
||||
const event = await getEvent(eventId, {
|
||||
@@ -344,7 +368,7 @@ const pinController: AppController = async (c) => {
|
||||
|
||||
if (event) {
|
||||
await updateListEvent(
|
||||
{ kinds: [10001], authors: [pubkey] },
|
||||
{ kinds: [10001], authors: [pubkey], limit: 1 },
|
||||
(tags) => addTag(tags, ['e', eventId]),
|
||||
c,
|
||||
);
|
||||
@@ -361,7 +385,7 @@ const pinController: AppController = async (c) => {
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/statuses/#unpin */
|
||||
const unpinController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const eventId = c.req.param('id');
|
||||
const { signal } = c.req.raw;
|
||||
|
||||
@@ -373,7 +397,7 @@ const unpinController: AppController = async (c) => {
|
||||
|
||||
if (event) {
|
||||
await updateListEvent(
|
||||
{ kinds: [10001], authors: [pubkey] },
|
||||
{ kinds: [10001], authors: [pubkey], limit: 1 },
|
||||
(tags) => deleteTag(tags, ['e', eventId]),
|
||||
c,
|
||||
);
|
||||
@@ -405,7 +429,7 @@ const zapController: AppController = async (c) => {
|
||||
|
||||
const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'], signal });
|
||||
const author = target?.author;
|
||||
const meta = jsonMetaContentSchema.parse(author?.content);
|
||||
const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
|
||||
const lnurl = getLnurl(meta);
|
||||
|
||||
if (target && lnurl) {
|
||||
@@ -421,7 +445,7 @@ const zapController: AppController = async (c) => {
|
||||
],
|
||||
}, c);
|
||||
|
||||
const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') });
|
||||
const status = await renderStatus(target, { viewerPubkey: await c.get('signer')?.getPublicKey() });
|
||||
status.zapped = true;
|
||||
|
||||
return c.json(status);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { NostrFilter } from '@nostrify/nostrify';
|
||||
import Debug from '@soapbox/stickynotes/debug';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { Debug } from '@/deps.ts';
|
||||
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
||||
import { getFeedPubkeys } from '@/queries.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { bech32ToPubkey } from '@/utils.ts';
|
||||
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { eventsDB } from '@/storages.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
|
||||
const debug = Debug('ditto:streaming');
|
||||
|
||||
@@ -69,11 +69,24 @@ const streamingController: AppController = (c) => {
|
||||
if (!filter) return;
|
||||
|
||||
try {
|
||||
for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) {
|
||||
const pubsub = await Storages.pubsub();
|
||||
const optimizer = await Storages.optimizer();
|
||||
|
||||
for await (const msg of pubsub.req([filter], { signal: controller.signal })) {
|
||||
if (msg[0] === 'EVENT') {
|
||||
const [event] = await hydrateEvents({
|
||||
events: [msg[2]],
|
||||
storage: eventsDB,
|
||||
const event = msg[2];
|
||||
|
||||
if (pubkey) {
|
||||
const policy = new MuteListPolicy(pubkey, await Storages.admin());
|
||||
const [, , ok] = await policy.call(event);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await hydrateEvents({
|
||||
events: [event],
|
||||
store: optimizer,
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
|
||||
|
||||
51
src/controllers/api/suggestions.ts
Normal file
51
src/controllers/api/suggestions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NStore } from '@nostrify/nostrify';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { getTagSet } from '@/tags.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
|
||||
export const suggestionsV1Controller: AppController = async (c) => {
|
||||
const store = c.get('store');
|
||||
const signal = c.req.raw.signal;
|
||||
const accounts = await renderSuggestedAccounts(store, signal);
|
||||
|
||||
return c.json(accounts);
|
||||
};
|
||||
|
||||
export const suggestionsV2Controller: AppController = async (c) => {
|
||||
const store = c.get('store');
|
||||
const signal = c.req.raw.signal;
|
||||
const accounts = await renderSuggestedAccounts(store, signal);
|
||||
|
||||
const suggestions = accounts.map((account) => ({
|
||||
source: 'staff',
|
||||
account,
|
||||
}));
|
||||
|
||||
return c.json(suggestions);
|
||||
};
|
||||
|
||||
async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) {
|
||||
const [follows] = await store.query(
|
||||
[{ kinds: [3], authors: [Conf.pubkey], limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
// TODO: pagination
|
||||
const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')].slice(0, 20);
|
||||
|
||||
const profiles = await store.query(
|
||||
[{ kinds: [0], authors: pubkeys, limit: pubkeys.length }],
|
||||
{ signal },
|
||||
)
|
||||
.then((events) => hydrateEvents({ events, store, signal }));
|
||||
|
||||
const accounts = await Promise.all(pubkeys.map((pubkey) => {
|
||||
const profile = profiles.find((event) => event.pubkey === pubkey);
|
||||
return profile ? renderAccount(profile) : accountFromPubkey(pubkey);
|
||||
}));
|
||||
|
||||
return accounts.filter(Boolean);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NostrFilter, NStore } from '@nostrify/nostrify';
|
||||
import { NostrFilter } from '@nostrify/nostrify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type AppContext, type AppController } from '@/app.ts';
|
||||
@@ -11,7 +11,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
|
||||
const homeTimelineController: AppController = async (c) => {
|
||||
const params = paginationSchema.parse(c.req.query());
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const pubkey = await c.get('signer')?.getPublicKey()!;
|
||||
const authors = await getFeedPubkeys(pubkey);
|
||||
return renderStatuses(c, [{ authors, kinds: [1, 6], ...params }]);
|
||||
};
|
||||
@@ -37,7 +37,7 @@ const publicTimelineController: AppController = (c) => {
|
||||
};
|
||||
|
||||
const hashtagTimelineController: AppController = (c) => {
|
||||
const hashtag = c.req.param('hashtag')!;
|
||||
const hashtag = c.req.param('hashtag')!.toLowerCase();
|
||||
const params = paginationSchema.parse(c.req.query());
|
||||
return renderStatuses(c, [{ kinds: [1], '#t': [hashtag], ...params }]);
|
||||
};
|
||||
@@ -45,28 +45,24 @@ const hashtagTimelineController: AppController = (c) => {
|
||||
/** Render statuses for timelines. */
|
||||
async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||
const { signal } = c.req.raw;
|
||||
const store = c.get('store') as NStore;
|
||||
const store = c.get('store');
|
||||
|
||||
const events = await store
|
||||
.query(filters, { signal })
|
||||
.then((events) =>
|
||||
hydrateEvents({
|
||||
events,
|
||||
storage: store,
|
||||
signal,
|
||||
})
|
||||
);
|
||||
.then((events) => hydrateEvents({ events, store, signal }));
|
||||
|
||||
if (!events.length) {
|
||||
return c.json([]);
|
||||
}
|
||||
|
||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||
|
||||
const statuses = (await Promise.all(events.map((event) => {
|
||||
if (event.kind === 6) {
|
||||
return renderReblog(event, { viewerPubkey: c.get('pubkey') });
|
||||
return renderReblog(event, { viewerPubkey });
|
||||
}
|
||||
return renderStatus(event, { viewerPubkey: c.get('pubkey') });
|
||||
}))).filter((boolean) => boolean);
|
||||
return renderStatus(event, { viewerPubkey });
|
||||
}))).filter(Boolean);
|
||||
|
||||
if (!statuses.length) {
|
||||
return c.json([]);
|
||||
|
||||
Reference in New Issue
Block a user