From 339b13c084051a9d1f4189285c2d1c4fd3c46fbe Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 1 Nov 2024 23:14:59 -0300 Subject: [PATCH] feat: create updateInstanceController for now only update: title, description, screenshots (user must provide the image URL) and thumbnail (user must provide the image URL) screenshots array is stored in the content of the kind 0 of the --- src/app.ts | 3 ++ src/controllers/api/ditto.ts | 55 ++++++++++++++++++++++++++++++++- src/controllers/api/instance.ts | 1 + src/controllers/manifest.ts | 1 + src/schemas/nostr.ts | 35 +++++++++++++++++++-- src/utils/instance.ts | 5 ++- 6 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index fdcacf29..74e6ff72 100644 --- a/src/app.ts +++ b/src/app.ts @@ -49,6 +49,7 @@ import { nameRequestController, nameRequestsController, statusZapSplitsController, + updateInstanceController, updateZapSplitsController, } from '@/controllers/api/ditto.ts'; import { emptyArrayController, notImplementedController } from '@/controllers/api/fallback.ts'; @@ -303,6 +304,8 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); +app.put('/api/v1/admin/ditto/instance', requireRole('admin'), updateInstanceController); + app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index b9be027f..6682522a 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -7,11 +7,13 @@ import { addTag } from '@/utils/tags.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { booleanParamSchema, percentageSchema } from '@/schema.ts'; import { Conf } from '@/config.ts'; -import { createEvent, paginated, parseBody } from '@/utils/api.ts'; +import { createEvent, paginated, parseBody, updateEvent } from '@/utils/api.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; import { deleteTag } from '@/utils/tags.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts'; import { getAuthor } from '@/queries.ts'; +import { screenshotsSchema } from '@/schemas/nostr.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderNameRequest } from '@/views/ditto.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; @@ -287,3 +289,54 @@ export const statusZapSplitsController: AppController = async (c) => { return c.json(zapSplits, 200); }; + +const updateInstanceSchema = z.object({ + title: z.string().optional(), + description: z.string().optional(), + screenshots: screenshotsSchema.optional(), + thumbnail: z.object({ + url: z.string().url(), + blurhash: z.string().optional(), + versions: z.object({ + '@1x': z.string().url().optional(), + '@2x': z.string().url().optional(), + }).optional(), + }).optional(), +}).strict(); + +export const updateInstanceController: AppController = async (c) => { + const body = await parseBody(c.req.raw); + const result = updateInstanceSchema.safeParse(body); + const pubkey = Conf.pubkey; + + if (!result.success) { + return c.json(result.error, 422); + } + + await updateEvent( + { kinds: [0], authors: [pubkey], limit: 1 }, + async (_) => { + const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal); + const { + title, + description, + screenshots, + thumbnail, + } = result.data; + + meta.name = title ?? meta.name; + meta.about = description ?? meta.about; + meta.screenshots = screenshots ?? meta.screenshots; + meta.thumbnail = thumbnail ?? meta.thumbnail; + + return { + kind: 0, + content: JSON.stringify(meta), + tags: [], + }; + }, + c, + ); + + return c.json(204); +}; diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 78a72dd4..9f504cad 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -96,6 +96,7 @@ const instanceV2Controller: AppController = async (c) => { '@2x': meta.picture, }, }, + screenshots: meta.screenshots, languages: [ 'en', ], diff --git a/src/controllers/manifest.ts b/src/controllers/manifest.ts index 60e2a2ac..2e75de04 100644 --- a/src/controllers/manifest.ts +++ b/src/controllers/manifest.ts @@ -20,6 +20,7 @@ export const manifestController: AppController = async (c) => { scope: '/', short_name: meta.name, start_url: '/', + screenshots: meta.screenshots, }; return c.json(manifest, { diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index d8aa29a4..46f68a34 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -2,17 +2,48 @@ import { NSchema as n } from '@nostrify/nostrify'; import { getEventHash, verifyEvent } from 'nostr-tools'; import { z } from 'zod'; -import { safeUrlSchema } from '@/schema.ts'; +import { safeUrlSchema, sizesSchema } from '@/schema.ts'; /** Nostr event schema that also verifies the event's signature. */ const signedEventSchema = n.event() .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine(verifyEvent, 'Event signature is invalid'); +/** + * Stored in the kind 0 content. + * https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots + */ +const screenshotsSchema = z.array(z.object({ + form_factor: z.enum(['narrow', 'wide']).optional(), + label: z.string().optional(), + platform: z.enum([ + 'android', + 'chromeos', + 'ipados', + 'ios', + 'kaios', + 'macos', + 'windows', + 'xbox', + 'chrome_web_store', + 'itunes', + 'microsoft-inbox', + 'microsoft-store', + 'play', + ]).optional(), + /** https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots#sizes */ + sizes: sizesSchema, + /** Absolute URL. */ + src: z.string().url(), + /** MIME type of the image. */ + type: z.string().optional(), +})); + /** Kind 0 content schema for the Ditto server admin user. */ const serverMetaSchema = n.metadata().and(z.object({ tagline: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined), + screenshots: screenshotsSchema.optional(), })); /** NIP-11 Relay Information Document. */ @@ -32,4 +63,4 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url() /** NIP-30 custom emoji tag. */ type EmojiTag = z.infer; -export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, serverMetaSchema, signedEventSchema }; +export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema, signedEventSchema }; diff --git a/src/utils/instance.ts b/src/utils/instance.ts index f46541c2..c0b9c0d4 100644 --- a/src/utils/instance.ts +++ b/src/utils/instance.ts @@ -1,7 +1,8 @@ import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify'; +import { z } from 'zod'; import { Conf } from '@/config.ts'; -import { serverMetaSchema } from '@/schemas/nostr.ts'; +import { screenshotsSchema, serverMetaSchema } from '@/schemas/nostr.ts'; /** Like NostrMetadata, but some fields are required and also contains some extra fields. */ export interface InstanceMetadata extends NostrMetadata { @@ -11,6 +12,7 @@ export interface InstanceMetadata extends NostrMetadata { picture: string; tagline: string; event?: NostrEvent; + screenshots: z.infer; } /** Get and parse instance metadata from the kind 0 of the admin user. */ @@ -34,5 +36,6 @@ export async function getInstanceMetadata(store: NStore, signal?: AbortSignal): email: meta.email ?? `postmaster@${Conf.url.host}`, picture: meta.picture ?? Conf.local('/images/thumbnail.png'), event, + screenshots: meta.screenshots ?? [], }; }