Merge remote-tracking branch 'origin/main' into refactor-trends

This commit is contained in:
Alex Gleason
2024-05-21 16:45:58 -05:00
65 changed files with 970 additions and 782 deletions

View File

@@ -7,8 +7,7 @@ import { Conf } from '@/config.ts';
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
import { booleanParamSchema, fileSchema } from '@/schema.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,6 +17,7 @@ import { renderRelationship } from '@/views/mastodon/relationships.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts';
import { hydrateEvents } from '@/storages/hydrate.ts';
import { bech32ToPubkey } from '@/utils.ts';
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
const usernameSchema = z
.string().min(1).max(30)
@@ -45,14 +45,32 @@ const createAccountController: AppController = async (c) => {
};
const verifyCredentialsController: AppController = async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!;
const signer = c.get('signer')!;
const pubkey = await signer.getPublicKey();
const event = await getAuthor(pubkey, { relations: ['author_stats'] });
if (event) {
return c.json(await renderAccount(event, { withSource: true }));
} else {
return c.json(await accountFromPubkey(pubkey, { withSource: true }));
const eventsDB = await Storages.db();
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,
}]),
]);
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) => {
@@ -86,25 +104,35 @@ const accountLookupController: AppController = async (c) => {
}
};
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),
store.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,
store,
signal: c.req.raw.signal,
signal,
});
if ((results.length < 1) && query.match(/npub1\w+/)) {
@@ -198,10 +226,12 @@ const updateCredentialsSchema = z.object({
bot: z.boolean().optional(),
discoverable: z.boolean().optional(),
nip05: z.string().optional(),
pleroma_settings_store: z.unknown().optional(),
});
const updateCredentialsController: AppController = async (c) => {
const pubkey = await c.get('signer')?.getPublicKey()!;
const signer = c.get('signer')!;
const pubkey = await signer.getPublicKey();
const body = await parseBody(c.req.raw);
const result = updateCredentialsSchema.safeParse(body);
@@ -221,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;
@@ -238,6 +268,18 @@ const updateCredentialsController: AppController = async (c) => {
}, 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(settingsStore)),
}, c);
}
account.pleroma.settings_store = settingsStore;
return c.json(account);
};

View File

@@ -5,8 +5,8 @@ import { Conf } from '@/config.ts';
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { booleanParamSchema } from '@/schema.ts';
import { Storages } from '@/storages.ts';
import { addTag } from '@/tags.ts';
import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts';
import { addTag } from '@/utils/tags.ts';
import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts';
const adminAccountQuerySchema = z.object({

View File

@@ -1,6 +1,6 @@
import { type AppController } from '@/app.ts';
import { Storages } from '@/storages.ts';
import { getTagSet } from '@/tags.ts';
import { getTagSet } from '@/utils/tags.ts';
import { renderStatuses } from '@/views.ts';
/** https://docs.joinmastodon.org/methods/bookmarks/#get */

View File

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

View File

@@ -1,6 +1,6 @@
import { type AppController } from '@/app.ts';
import { Storages } from '@/storages.ts';
import { getTagSet } from '@/tags.ts';
import { getTagSet } from '@/utils/tags.ts';
import { renderAccounts } from '@/views.ts';
/** https://docs.joinmastodon.org/methods/mutes/#get */

View File

@@ -1,9 +1,9 @@
import { encodeBase64 } from '@std/encoding/base64';
import { escape } from 'entities';
import { nip19 } from 'nostr-tools';
import { z } from 'zod';
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';
@@ -100,11 +100,11 @@ const oauthController: AppController = async (c) => {
<form id="oauth_form" action="/oauth/authorize" method="post">
<input type="text" placeholder="npub1... or nsec1..." name="nip19" autocomplete="off">
<input type="hidden" name="pubkey" id="pubkey" value="">
<input type="hidden" name="redirect_uri" id="redirect_uri" value="${lodash.escape(redirectUri)}">
<input type="hidden" name="redirect_uri" id="redirect_uri" value="${escape(redirectUri)}">
<button type="submit">Authorize</button>
</form>
<br>
<a href="${lodash.escape(connectUri)}">Nostr Connect</a>
<a href="${escape(connectUri)}">Nostr Connect</a>
</body>
</html>
`);

View File

@@ -1,20 +1,22 @@
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 { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.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 { asyncReplaceAll } from '@/utils/text.ts';
import { Storages } from '@/storages.ts';
import { hydrateEvents } from '@/storages/hydrate.ts';
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
import { getLnurl } from '@/utils/lnurl.ts';
import { lookupPubkey } from '@/utils/lookup.ts';
import { addTag, deleteTag } from '@/utils/tags.ts';
import { asyncReplaceAll } from '@/utils/text.ts';
const createStatusSchema = z.object({
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
@@ -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);
@@ -73,12 +76,21 @@ const createStatusController: AppController = async (c) => {
const tags: string[][] = [];
if (data.quote_id) {
tags.push(['q', data.quote_id]);
if (data.in_reply_to_id) {
const ancestor = await getEvent(data.in_reply_to_id);
if (!ancestor) {
return c.json({ error: 'Original post not found.' }, 404);
}
const root = ancestor.tags.find((tag) => tag[0] === 'e' && tag[3] === 'root')?.[1] ?? ancestor.id;
tags.push(['e', root, 'root']);
tags.push(['e', data.in_reply_to_id, 'reply']);
}
if (data.in_reply_to_id) {
tags.push(['e', data.in_reply_to_id, 'reply']);
if (data.quote_id) {
tags.push(['q', data.quote_id]);
}
if (data.sensitive && data.spoiler_text) {
@@ -89,15 +101,14 @@ const createStatusController: AppController = async (c) => {
tags.push(['subject', data.spoiler_text]);
}
const viewerPubkey = await c.get('signer')?.getPublicKey();
const media = data.media_ids?.length ? await getUnattachedMediaByIds(kysely, data.media_ids) : [];
if (data.media_ids?.length) {
const media = await getUnattachedMediaByIds(data.media_ids)
.then((media) => media.filter(({ pubkey }) => pubkey === viewerPubkey))
.then((media) => media.map(({ url, data }) => ['media', url, data]));
const imeta: string[][] = media.map(({ data }) => {
const values: string[] = data.map((tag) => tag.join(' '));
return ['imeta', ...values];
});
tags.push(...media);
}
tags.push(...imeta);
const pubkeys = new Set<string>();
@@ -110,7 +121,11 @@ const createStatusController: AppController = async (c) => {
pubkeys.add(pubkey);
}
return `nostr:${pubkey}`;
try {
return `nostr:${nip19.npubEncode(pubkey)}`;
} catch {
return match;
}
});
// Explicit addressing
@@ -129,9 +144,15 @@ const createStatusController: AppController = async (c) => {
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);
@@ -261,21 +282,19 @@ const reblogStatusController: AppController = async (c) => {
const unreblogStatusController: AppController = async (c) => {
const eventId = c.req.param('id');
const pubkey = await c.get('signer')?.getPublicKey()!;
const event = await getEvent(eventId, { kind: 1 });
if (!event) {
return c.json({ error: 'Event not found.' }, 404);
}
const store = await Storages.db();
const [event] = await store.query([{ ids: [eventId], kinds: [1] }]);
if (!event) {
return c.json({ error: 'Record not found' }, 404);
}
const [repostedEvent] = await store.query(
[{ kinds: [6], authors: [pubkey], '#e': [event.id], limit: 1 }],
);
if (!repostedEvent) {
return c.json({ error: 'Event not found.' }, 404);
return c.json({ error: 'Record not found' }, 404);
}
await createEvent({

View File

@@ -2,8 +2,8 @@ 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 { getTagSet } from '@/utils/tags.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
export const suggestionsV1Controller: AppController = async (c) => {