diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9dee45..b2140db 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: denoland/deno:1.41.3 +image: denoland/deno:1.43.3 default: interruptible: true diff --git a/.tool-versions b/.tool-versions index a13fd5f..b3e19cd 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -deno 1.41.3 \ No newline at end of file +deno 1.43.3 \ No newline at end of file diff --git a/deno.json b/deno.json index 25ec2ed..75ea333 100644 --- a/deno.json +++ b/deno.json @@ -11,7 +11,8 @@ "nsec": "deno run scripts/nsec.ts", "admin:event": "deno run -A scripts/admin-event.ts", "admin:role": "deno run -A scripts/admin-role.ts", - "stats:recompute": "deno run -A scripts/stats-recompute.ts" + "stats:recompute": "deno run -A scripts/stats-recompute.ts", + "soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip soapbox.zip && rm soapbox.zip" }, "unstable": ["ffi", "kv", "worker-options"], "exclude": ["./public"], @@ -20,8 +21,9 @@ "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@db/sqlite": "jsr:@db/sqlite@^0.11.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", + "@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.19.1", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.22.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", @@ -31,15 +33,18 @@ "@std/dotenv": "jsr:@std/dotenv@^0.224.0", "@std/encoding": "jsr:@std/encoding@^0.224.0", "@std/json": "jsr:@std/json@^0.223.0", - "@std/media-types": "jsr:@std/media-types@^0.224.0", + "@std/media-types": "jsr:@std/media-types@^0.224.1", "@std/streams": "jsr:@std/streams@^0.223.0", "comlink": "npm:comlink@^4.4.1", - "deno-safe-fetch": "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts", + "deno-safe-fetch/load": "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts", + "deno-sqlite": "https://raw.githubusercontent.com/alexgleason/deno-sqlite/325f66d8c395e7f6f5ee78ebfa42a0eeea4a942b/mod.ts", + "entities": "npm:entities@^4.5.0", "fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0", "formdata-helper": "npm:formdata-helper@^0.3.0", "hono": "https://deno.land/x/hono@v3.10.1/mod.ts", "hono/middleware": "https://deno.land/x/hono@v3.10.1/middleware.ts", "iso-639-1": "npm:iso-639-1@2.1.15", + "isomorphic-dompurify": "npm:isomorphic-dompurify@^2.11.0", "kysely": "npm:kysely@^0.27.3", "kysely_deno_postgres": "https://deno.land/x/kysely_deno_postgres@v0.4.0/mod.ts", "linkify-plugin-hashtag": "npm:linkify-plugin-hashtag@^4.1.1", @@ -53,7 +58,6 @@ "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", "unfurl.js": "npm:unfurl.js@^6.4.0", - "uuid62": "npm:uuid62@^1.0.2", "zod": "npm:zod@^3.23.5", "~/fixtures/": "./fixtures/" }, diff --git a/fixtures/events/event-imeta.json b/fixtures/events/event-imeta.json new file mode 100644 index 0000000..4952ca8 --- /dev/null +++ b/fixtures/events/event-imeta.json @@ -0,0 +1,20 @@ +{ + "id": "1264cc4051db59af9a21f7fd001fdf5213424f558ea9ab16a1b014fca2250af5", + "pubkey": "6be38f8c63df7dbf84db7ec4a6e6fbbd8d19dca3b980efad18585c46f04b26f9", + "created_at": 1716306470, + "kind": 1, + "tags": [ + [ + "imeta", + "url https://image.nostr.build/258d978b91e7424cfa43b31f3cfc077d7172ae10b3b45ac956feff9e72175126.png", + "m image/png", + "x b1ceee58405ef05a41190a0946ca6b6511dff426c68013cdd165514c1ef301f9", + "ox 258d978b91e7424cfa43b31f3cfc077d7172ae10b3b45ac956feff9e72175126", + "size 114350", + "dim 1414x594", + "blurhash LDRfkC.8_4_N_3NGR*t8%gIVWBxt" + ] + ], + "content": "Today we were made aware of multiple Fediverse blog posts incorrectly attributing “vote Trump” spam on Bluesky to the Mostr.pub Bridge. \n\nThis spam is NOT coming from Mostr. From the screenshots used in these blogs, it's clear the spam is coming from an entirely different bridge called momostr.pink. This bridge is not affiliated with Mostr, and is not even a fork of Mostr. We appreciate that the authors of these posts responded quickly to us and have since corrected the blogs. \n\nMostr.pub uses stirfry policies for anti-spam filtering. This includes an anti-duplication policy that prevents spam like the recent “vote Trump” posts we’ve seen repeated over and over. \n\nIt is important to note WHY there are multiple bridges, though. \n\nWhen Mostr.pub launched, multiple major servers immediately blocked Mostr, including Mastodon.social. The moderators of Mastodon.social claimed that this was because Nostr was unregulated, and suggested to one user that if they want to bridge their account they should host their own bridge.\n\nThat is exactly what momostr.pink, the source of this spam, has done. \n\nThe obvious response to the censorship of the Mostr Bridge is to build more bridges. \n\nWhile we have opted for pro-social policies that aim to reduce spam and build better connections between decentralized platforms, other bridges built to get around censorship of the Mostr Bridge may not — as we’re already seeing.\n\nThere will inevitably be multiple bridges, and we’re working on creating solutions to the problems that arise from that. In the meantime, if the Fediverse could do itself a favor and chill with the censorship for two seconds, we might not have so many problems. \n\n\nhttps://image.nostr.build/258d978b91e7424cfa43b31f3cfc077d7172ae10b3b45ac956feff9e72175126.png", + "sig": "b950e6e2ff1dc786ef344e7dad3edf8aa315a1053ede146725bde181acf7c2c1a5fcf1e0c796552b743607d6ae161a3ff4eb3af5033ffbfd314e68213d315215" +} diff --git a/fixtures/nostrbuild-gif.json b/fixtures/nostrbuild-gif.json new file mode 100644 index 0000000..49a969a --- /dev/null +++ b/fixtures/nostrbuild-gif.json @@ -0,0 +1,34 @@ +{ + "status": "success", + "message": "Upload successful.", + "data": [ + { + "input_name": "APIv2", + "name": "e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "sha256": "0a71f1c9dd982079bc52e96403368209cbf9507c5f6956134686f56e684b6377", + "original_sha256": "e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3", + "type": "picture", + "mime": "image/gif", + "size": 1796276, + "blurhash": "LGH-S^Vwm]x]04kX-qR-R]SL5FxZ", + "dimensions": { + "width": 360, + "height": 216 + }, + "dimensionsString": "360x216", + "url": "https://image.nostr.build/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "thumbnail": "https://image.nostr.build/thumb/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "responsive": { + "240p": "https://image.nostr.build/resp/240p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "360p": "https://image.nostr.build/resp/360p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "480p": "https://image.nostr.build/resp/480p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "720p": "https://image.nostr.build/resp/720p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "1080p": "https://image.nostr.build/resp/1080p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif" + }, + "metadata": { + "date:create": "2024-05-18T02:11:39+00:00", + "date:modify": "2024-05-18T02:11:39+00:00" + } + } + ] +} \ No newline at end of file diff --git a/fixtures/nostrbuild-mp3.json b/fixtures/nostrbuild-mp3.json new file mode 100644 index 0000000..42a60b4 --- /dev/null +++ b/fixtures/nostrbuild-mp3.json @@ -0,0 +1,29 @@ +{ + "status": "success", + "message": "Upload successful.", + "data": [ + { + "id": 0, + "input_name": "APIv2", + "name": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "url": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "thumbnail": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "responsive": { + "240p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "360p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "480p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "720p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "1080p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3" + }, + "blurhash": "", + "sha256": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725", + "original_sha256": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725", + "type": "video", + "mime": "audio/mpeg", + "size": 1519616, + "metadata": [], + "dimensions": [], + "dimensionsString": "0x0" + } + ] +} \ No newline at end of file diff --git a/scripts/stats-recompute.ts b/scripts/stats-recompute.ts index dcb0bc0..4037a85 100644 --- a/scripts/stats-recompute.ts +++ b/scripts/stats-recompute.ts @@ -1,8 +1,6 @@ import { nip19 } from 'nostr-tools'; -import { DittoDB } from '@/db/DittoDB.ts'; -import { DittoTables } from '@/db/DittoTables.ts'; -import { Storages } from '@/storages.ts'; +import { refreshAuthorStats } from '@/stats.ts'; let pubkey: string; try { @@ -17,23 +15,4 @@ try { Deno.exit(1); } -const store = await Storages.db(); -const kysely = await DittoDB.getInstance(); - -const [followList] = await store.query([{ kinds: [3], authors: [pubkey], limit: 1 }]); - -const authorStats: DittoTables['author_stats'] = { - pubkey, - followers_count: (await store.count([{ kinds: [3], '#p': [pubkey] }])).count, - following_count: followList?.tags.filter(([name]) => name === 'p')?.length ?? 0, - notes_count: (await store.count([{ kinds: [1], authors: [pubkey] }])).count, -}; - -await kysely.insertInto('author_stats') - .values(authorStats) - .onConflict((oc) => - oc - .column('pubkey') - .doUpdateSet(authorStats) - ) - .execute(); +await refreshAuthorStats(pubkey); diff --git a/src/app.ts b/src/app.ts index 059e883..be2ce84 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NostrSigner, NStore } from '@nostrify/nostrify'; +import { NostrEvent, NostrSigner, NStore, NUploader } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; @@ -7,7 +7,6 @@ import { Conf } from '@/config.ts'; import { startFirehose } from '@/firehose.ts'; import { Time } from '@/utils.ts'; -import { actorController } from '@/controllers/activitypub/actor.ts'; import { accountController, accountLookupController, @@ -77,10 +76,8 @@ import { } from '@/controllers/api/timelines.ts'; import { trendingTagsController } from '@/controllers/api/trends.ts'; import { indexController } from '@/controllers/site.ts'; -import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; -import { webfingerController } from '@/controllers/well-known/webfinger.ts'; import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; @@ -89,11 +86,14 @@ import { signerMiddleware } from '@/middleware/signerMiddleware.ts'; import { storeMiddleware } from '@/middleware/storeMiddleware.ts'; import { blockController } from '@/controllers/api/accounts.ts'; import { unblockController } from '@/controllers/api/accounts.ts'; +import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts'; interface AppEnv extends HonoEnv { Variables: { /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ signer?: NostrSigner; + /** Uploader for the user to upload files. */ + uploader?: NUploader; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; /** Store */ @@ -129,17 +129,14 @@ app.use( cspMiddleware(), cors({ origin: '*', exposeHeaders: ['link'] }), signerMiddleware, + uploaderMiddleware, auth98Middleware(), storeMiddleware, ); -app.get('/.well-known/webfinger', webfingerController); -app.get('/.well-known/host-meta', hostMetaController); app.get('/.well-known/nodeinfo', nodeInfoController); app.get('/.well-known/nostr.json', nostrController); -app.get('/users/:username', actorController); - app.get('/nodeinfo/:version', nodeInfoSchemaController); app.get('/api/v1/instance', cacheMiddleware({ cacheName: 'web', expires: Time.minutes(5) }), instanceController); diff --git a/src/config.ts b/src/config.ts index 6fe62b9..cc14998 100644 --- a/src/config.ts +++ b/src/config.ts @@ -136,6 +136,14 @@ class Conf { return Deno.env.get('IPFS_API_URL') || 'http://localhost:5001'; }, }; + /** nostr.build API endpoint when the `nostrbuild` uploader is used. */ + static get nostrbuildEndpoint(): string { + return Deno.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; + } + /** Default Blossom servers to use when the `blossom` uploader is set. */ + static get blossomServers(): string[] { + return Deno.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/']; + } /** Module to upload files with. */ static get uploader() { return Deno.env.get('DITTO_UPLOADER'); diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts deleted file mode 100644 index 19f5f10..0000000 --- a/src/controllers/activitypub/actor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getAuthor } from '@/queries.ts'; -import { activityJson } from '@/utils/api.ts'; -import { renderActor } from '@/views/activitypub/actor.ts'; -import { localNip05Lookup } from '@/utils/nip05.ts'; - -import type { AppContext, AppController } from '@/app.ts'; - -const actorController: AppController = async (c) => { - const username = c.req.param('username'); - const { signal } = c.req.raw; - - const pointer = await localNip05Lookup(c.get('store'), username); - if (!pointer) return notFound(c); - - const event = await getAuthor(pointer.pubkey, { signal }); - if (!event) return notFound(c); - - const actor = await renderActor(event, username); - if (!actor) return notFound(c); - - return activityJson(c, actor); -}; - -function notFound(c: AppContext) { - return c.json({ error: 'Not found' }, 404); -} - -export { actorController }; diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 5c26ba5..2d61adc 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -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); }; diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 77571aa..d7cd365 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -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({ diff --git a/src/controllers/api/bookmarks.ts b/src/controllers/api/bookmarks.ts index 7655182..6d80b50 100644 --- a/src/controllers/api/bookmarks.ts +++ b/src/controllers/api/bookmarks.ts @@ -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 */ diff --git a/src/controllers/api/media.ts b/src/controllers/api/media.ts index 33b7981..71b3e78 100644 --- a/src/controllers/api/media.ts +++ b/src/controllers/api/media.ts @@ -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); diff --git a/src/controllers/api/mutes.ts b/src/controllers/api/mutes.ts index 4afb6c4..31f54ee 100644 --- a/src/controllers/api/mutes.ts +++ b/src/controllers/api/mutes.ts @@ -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 */ diff --git a/src/controllers/api/oauth.ts b/src/controllers/api/oauth.ts index a755a4d..c1407f4 100644 --- a/src/controllers/api/oauth.ts +++ b/src/controllers/api/oauth.ts @@ -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) => {