diff --git a/.config/example.yml b/.config/example.yml index 8d4a162cb..cec6b6611 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -153,3 +153,9 @@ redis: # info: /twemoji/1f440.svg # notFound: /twemoji/2049.svg # error: /twemoji/1f480.svg + +# Whether it should be allowed to fetch content in ActivityPub form without HTTP signatures. +# It is recommended to leave this as default to improve the effectiveness of instance blocks.s +# However, note that while this prevents fetching in ActivityPub form, it could still be scraped +# from the API or other representations if the other side is determined to do so. +#allowUnsignedFetches: false diff --git a/.mailmap b/.mailmap index 51a2eb9dd..5fd89868a 100644 --- a/.mailmap +++ b/.mailmap @@ -6,7 +6,7 @@ Chloe Kudryavtsev Chloe Kudryavtsev Dr. Gutfuck LLC <40531868+gutfuckllc@users.noreply.github.com> Ehsan Javadynia <31900907+ehsanjavadynia@users.noreply.github.com> -Francis Dinh +Norm Hakaba Hitoyo Hakaba Hitoyo Johann150 Michcio diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1099f2b..666b03ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,33 @@ Unreleased changes should not be listed in this file. Instead, run `git shortlog --format='%h %s' --group=trailer:changelog ..` to see unreleased changes; replace `` with the tag you wish to compare from. If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead. +## 13.0.0-preview6 - 2023-07-02 + +## Added +- **BREAKING** activitypub: validate fetch signatures + Fetching the ActivityPub representation of something now requires a valid HTTP signature. +- client: add MFM functions `position`, `scale`, `fg`, `bg` +- server: add webhook stat to nodeinfo +- activitypub: handle incoming Update Note activities + +## Changed +- client: change followers only icon to closed lock +- client: disable sound for received note by default +- client: always forbid MFM overflow +- make mutes case insensitive +- activitypub: improve JSON-LD context + The context now properly notes the `@type`s of defined attributes. +- docker: only publish port on localhost + +## Fixed +- server: fix internal download in emoji import +- server: replace unzipper with decompress + +## Removed +- migrate note favorites to clips + If you previously had favorites they will now be in a clip called "⭐". + If you want to add a note as a "favorite" you can use the menu item "Clip". + ## 13.0.0-preview5 - 2023-05-23 This release contains 6 breaking changes and 1 security update. diff --git a/docker-compose.yml b/docker-compose.yml index 68051c494..3f1264e4f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - redis # - es ports: - - "3000:3000" + - "127.0.0.1:3000:3000" networks: - internal_network - external_network diff --git a/locales/de-DE.yml b/locales/de-DE.yml index fb39f8141..01f92364a 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1309,6 +1309,7 @@ _notification: groupInvited: "Erhaltene Gruppeneinladungen" app: "Benachrichtigungen von Apps" move: Account-Umzüge + updated: Beobachtete Notiz wurde bearbeitet _actions: followBack: "folgt dir nun auch" reply: "Antworten" diff --git a/locales/en-US.yml b/locales/en-US.yml index 947ed7bfb..931634a7b 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1304,6 +1304,7 @@ _notification: reaction: "Reactions" pollVote: "Votes on polls" pollEnded: "Polls ending" + updated: "Watched Note was updated" receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" groupInvited: "Group invitations" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c10755320..d6a5ad9d5 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -748,6 +748,7 @@ _ffVisibility: followers: "フォロワーだけに公開" private: "非公開" + nobody: 誰にも見せない (あなたにさえも) _signup: almostThere: "ほとんど完了です" emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" diff --git a/package.json b/package.json index 831964a0f..f1a1e1ae9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foundkey", - "version": "13.0.0-preview5", + "version": "13.0.0-preview6", "repository": { "type": "git", "url": "https://akkoma.dev/FoundKeyGang/FoundKey.git" diff --git a/packages/backend/.mocharc.json b/packages/backend/.mocharc.json index f836f9e90..cfc511621 100644 --- a/packages/backend/.mocharc.json +++ b/packages/backend/.mocharc.json @@ -1,8 +1,6 @@ { - "extension": ["ts","js","cjs","mjs"], "node-option": [ - "experimental-specifier-resolution=node", - "loader=./test/loader.js" + "experimental-specifier-resolution=node" ], "slow": 1000, "timeout": 30000, diff --git a/packages/backend/migration/1685997617959-updatedNotification.js b/packages/backend/migration/1685997617959-updatedNotification.js new file mode 100644 index 000000000..8dff2b822 --- /dev/null +++ b/packages/backend/migration/1685997617959-updatedNotification.js @@ -0,0 +1,34 @@ +export class noteEditing1685997617959 { + name = 'noteEditing1685997617959'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`COMMENT ON COLUMN "note"."updatedAt" IS 'The updated date of the Note.'`); + + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`); + await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`); + await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`); + + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 097f6ee73..2c19d11f9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "13.0.0-preview5", + "version": "13.0.0-preview6", "main": "./index.js", "private": true, "type": "module", @@ -8,10 +8,10 @@ "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", "lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts", - "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", + "mocha": "NODE_ENV=test mocha", "migrate": "npx typeorm migration:run -d ormconfig.js", "start": "node --experimental-json-modules ./built/index.js", - "start:test": "cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js", + "start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js", "test": "npm run mocha" }, "dependencies": { @@ -41,6 +41,7 @@ "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.28.0", + "decompress": "4.2.1", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", "feed": "4.2.2", @@ -109,7 +110,6 @@ "tsconfig-paths": "4.1.0", "twemoji-parser": "14.0.0", "typeorm": "0.3.7", - "unzipper": "0.10.11", "uuid": "8.3.2", "web-push": "3.5.0", "ws": "8.8.0", @@ -164,7 +164,6 @@ "@types/ws": "8.5.3", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", - "cross-env": "7.0.3", "eslint": "^8.29.0", "eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules", "eslint-plugin-import": "^2.26.0", diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index f16fb850b..d1b303f73 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -61,6 +61,7 @@ export function loadConfig(): Config { proxyRemoteFiles: false, maxFileSize: 262144000, // 250 MiB maxNoteTextLength: 3000, + allowUnsignedFetches: false, }, config); mixin.version = meta.version; diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 686a8c242..2d163f91b 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -68,6 +68,8 @@ export type Source = { notFound?: string; error?: string; }; + + allowUnsignedFetches?: boolean; }; /** diff --git a/packages/backend/src/misc/convert-host.ts b/packages/backend/src/misc/convert-host.ts index 705edaedd..1e7e8b3eb 100644 --- a/packages/backend/src/misc/convert-host.ts +++ b/packages/backend/src/misc/convert-host.ts @@ -11,7 +11,7 @@ export function isSelfHost(host: string | null): boolean { return toPuny(config.host) === toPuny(host); } -export function extractDbHost(uri: string): string { +export function extractPunyHost(uri: string): string { const url = new URL(uri); return toPuny(url.hostname); } diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index e34dc53f7..f45ff9680 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -19,6 +19,12 @@ export class Note { }) public createdAt: Date; + @Column('timestamp with time zone', { + nullable: true, + comment: 'The updated date of the Note.', + }) + public updatedAt: Date | null; + @Index() @Column({ ...id(), diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index d93a47fc6..5895c267b 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -169,6 +169,7 @@ export const NoteRepository = db.getRepository(Note).extend({ const packed: Packed<'Note'> = await awaitAll({ id: note.id, createdAt: note.createdAt.toISOString(), + updatedAt: note.updatedAt?.toISOString() ?? null, userId: note.userId, user: Users.pack(note.user ?? note.userId, me, { detail: false, diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts index ab0ae05b0..b5992c051 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/backend/src/models/schema/note.ts @@ -12,6 +12,11 @@ export const packedNoteSchema = { optional: false, nullable: false, format: 'date-time', }, + updatedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, text: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 8c5d747bd..bfba6ec86 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -32,11 +32,6 @@ export const packedUserLiteSchema = { type: 'any', nullable: true, optional: false, }, - avatarColor: { - type: 'any', - nullable: true, optional: false, - default: null, - }, isAdmin: { type: 'boolean', nullable: false, optional: true, @@ -122,11 +117,6 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'any', nullable: true, optional: false, }, - bannerColor: { - type: 'any', - nullable: true, optional: false, - default: null, - }, isLocked: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts index 855017460..2a0aabe6f 100644 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts @@ -1,15 +1,15 @@ import * as fs from 'node:fs'; import Bull from 'bull'; -import unzipper from 'unzipper'; +import decompress from 'decompress'; import { db } from '@/db/postgre.js'; import { createTempDir } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; import { genId } from '@/misc/gen-id.js'; import { DriveFiles, Emojis } from '@/models/index.js'; import { DbUserImportJobData } from '@/queue/types.js'; import { queueLogger } from '@/queue/logger.js'; import { addFile } from '@/services/drive/add-file.js'; +import { copyFileTo } from '@/services/drive/read-file.js'; const logger = queueLogger.createSubLogger('import-custom-emojis'); @@ -33,7 +33,7 @@ export async function importCustomEmojis(job: Bull.Job, don try { fs.writeFileSync(destPath, '', 'binary'); - await downloadUrl(file.url, destPath); + await copyFileTo(file, destPath); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === 'string') { logger.error(e); @@ -42,44 +42,41 @@ export async function importCustomEmojis(job: Bull.Job, don } const outputPath = path + '/emojis'; - const unzipStream = fs.createReadStream(destPath); - const extractor = unzipper.Extract({ path: outputPath }); - extractor.on('close', async () => { - const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); - const meta = JSON.parse(metaRaw); - - for (const record of meta.emojis) { - if (!record.downloaded) continue; - if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) { - this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`); - continue; - } - const emojiInfo = record.emoji; - const emojiPath = outputPath + '/' + record.fileName; - await Emojis.delete({ - name: emojiInfo.name, - }); - const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true }); - await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: emojiInfo.name, - category: emojiInfo.category, - host: null, - aliases: emojiInfo.aliases, - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, - }); - } - - await db.queryResultCache!.remove(['meta_emojis']); - - cleanup(); - - logger.succ('Imported'); - done(); - }); - unzipStream.pipe(extractor); logger.succ(`Unzipping to ${outputPath}`); + await decompress(destPath, outputPath); + + const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); + const meta = JSON.parse(metaRaw); + + for (const record of meta.emojis) { + if (!record.downloaded) continue; + if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) { + this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`); + continue; + } + const emojiInfo = record.emoji; + const emojiPath = outputPath + '/' + record.fileName; + await Emojis.delete({ + name: emojiInfo.name, + }); + const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true }); + await Emojis.insert({ + id: genId(), + updatedAt: new Date(), + name: emojiInfo.name, + category: emojiInfo.category, + host: null, + aliases: emojiInfo.aliases, + originalUrl: driveFile.url, + publicUrl: driveFile.webpublicUrl ?? driveFile.url, + type: driveFile.webpublicType ?? driveFile.type, + }); + } + + await db.queryResultCache!.remove(['meta_emojis']); + + cleanup(); + + logger.succ('Imported'); + done(); } diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts index 066e92ca3..d9e1fdee5 100644 --- a/packages/backend/src/queue/processors/inbox.ts +++ b/packages/backend/src/queue/processors/inbox.ts @@ -1,27 +1,27 @@ import { URL } from 'node:url'; import Bull from 'bull'; -import httpSignature from '@peertube/http-signature'; import { perform } from '@/remote/activitypub/perform.js'; import Logger from '@/services/logger.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; import { Instances } from '@/models/index.js'; import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js'; -import { toPuny, extractDbHost } from '@/misc/convert-host.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; import { getApId } from '@/remote/activitypub/type.js'; import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js'; -import { getAuthUser } from '@/remote/activitypub/misc/auth-user.js'; -import { StatusError } from '@/misc/fetch.js'; +import { AuthUser, getAuthUser } from '@/remote/activitypub/misc/auth-user.js'; import { InboxJobData } from '@/queue/types.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js'; +import { verifyHttpSignature } from '@/remote/http-signature.js'; const logger = new Logger('inbox'); -// ユーザーのinboxにアクティビティが届いた時の処理 +// Processing when an activity arrives in the user's inbox export default async (job: Bull.Job): Promise => { const signature = job.data.signature; // HTTP-signature const activity = job.data.activity; + const resolver = new Resolver(); //#region Log const info = Object.assign({}, activity) as any; @@ -29,46 +29,12 @@ export default async (job: Bull.Job): Promise => { logger.debug(JSON.stringify(info, null, 2)); //#endregion - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith('acct:')) { - return `Old keyId is no longer supported. ${keyIdLower}`; - } - - const host = toPuny(new URL(keyIdLower).hostname); - - // Stop if the host is blocked. - if (await shouldBlockInstance(host)) { - return `Blocked request: ${host}`; - } - - const resolver = new Resolver(); - - let authUser; - try { - authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver); - } catch (e) { - if (e instanceof StatusError) { - if (e.isClientError) { - return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`; - } else { - throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`); - } - } - } - - if (authUser == null) { - // Key not found? Unacceptable! - return 'skip: failed to resolve user'; - } else { - // Found key! - } - - // verify the HTTP Signature - const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + const validated = await verifyHttpSignature(signature, resolver, getApId(activity.actor)); + let authUser = validated.authUser; // The signature must be valid. // The signature must also match the actor otherwise anyone could sign any activity. - if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { + if (validated.status !== 'valid' || validated.authUser.user.uri !== activity.actor) { // Last resort: LD-Signature if (activity.signature) { if (activity.signature.type !== 'RsaSignature2017') { @@ -98,7 +64,7 @@ export default async (job: Bull.Job): Promise => { } // Stop if the host is blocked. - const ldHost = extractDbHost(authUser.user.uri); + const ldHost = extractPunyHost(authUser.user.uri); if (await shouldBlockInstance(ldHost)) { return `Blocked request: ${ldHost}`; } @@ -107,15 +73,20 @@ export default async (job: Bull.Job): Promise => { } } + // authUser cannot be null at this point: + // either it was already not null because the HTTP signature was valid + // or, if the LD signature was not verified, this function will already have returned. + authUser = authUser as AuthUser; + // Verify that the actor's host is not blocked - const signerHost = extractDbHost(authUser.user.uri!); + const signerHost = extractPunyHost(authUser.user.uri!); if (await shouldBlockInstance(signerHost)) { return `Blocked request: ${signerHost}`; } if (typeof activity.id === 'string') { // Verify that activity and actor are from the same host. - const activityIdHost = extractDbHost(activity.id); + const activityIdHost = extractPunyHost(activity.id); if (signerHost !== activityIdHost) { return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; } diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index e0861024a..d38db1e4b 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -1,6 +1,6 @@ import post from '@/services/note/create.js'; import { IRemoteUser } from '@/models/entities/user.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; import { getApLock } from '@/misc/app-lock.js'; import { StatusError } from '@/misc/fetch.js'; import { Notes } from '@/models/index.js'; @@ -15,7 +15,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: const uri = getApId(activity); // Cancel if the announced from host is blocked. - if (await shouldBlockInstance(extractDbHost(uri))) return; + if (await shouldBlockInstance(extractPunyHost(uri))) return; const unlock = await getApLock(uri); diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts index 6fc7b6c2d..4394c647e 100644 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts @@ -1,6 +1,6 @@ import { IRemoteUser } from '@/models/entities/user.js'; import { getApLock } from '@/misc/app-lock.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; import { StatusError } from '@/misc/fetch.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { createNote, fetchNote } from '@/remote/activitypub/models/note.js'; @@ -18,7 +18,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj } if (typeof note.id === 'string') { - if (extractDbHost(actor.uri) !== extractDbHost(note.id)) { + if (extractPunyHost(actor.uri) !== extractPunyHost(note.id)) { return 'skip: host in actor.uri !== note.id'; } } diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts index 2a0918a4d..1bbb1401c 100644 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/index.ts @@ -1,7 +1,7 @@ import { IRemoteUser } from '@/models/entities/user.js'; import { toArray } from '@/prelude/array.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js'; import { apLogger } from '../logger.js'; import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js'; @@ -42,7 +42,7 @@ async function performOneActivity(actor: IRemoteUser, activity: IObject, resolve if (actor.isSuspended) return; if (typeof activity.id !== 'undefined') { - const host = extractDbHost(getApId(activity)); + const host = extractPunyHost(getApId(activity)); if (await shouldBlockInstance(host)) return; } diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts index cb147f2af..cd888d437 100644 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ b/packages/backend/src/remote/activitypub/kernel/read.ts @@ -1,5 +1,5 @@ import { IRemoteUser } from '@/models/entities/user.js'; -import { isSelfHost, extractDbHost } from '@/misc/convert-host.js'; +import { isSelfHost, extractPunyHost } from '@/misc/convert-host.js'; import { MessagingMessages } from '@/models/index.js'; import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js'; import { IRead, getApId } from '../type.js'; @@ -7,7 +7,7 @@ import { IRead, getApId } from '../type.js'; export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise => { const id = await getApId(activity.object); - if (!isSelfHost(extractDbHost(id))) { + if (!isSelfHost(extractPunyHost(id))) { return `skip: Read to foreign host (${id})`; } diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts index d34965db2..96367af0a 100644 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts @@ -1,9 +1,10 @@ import { IRemoteUser } from '@/models/entities/user.js'; -import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js'; +import { getApId, getOneApId, getApType, IUpdate, isActor, isPost } from '@/remote/activitypub/type.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { updateQuestion } from '@/remote/activitypub/models/question.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { updatePerson } from '@/remote/activitypub/models/person.js'; +import { update as updateNote } from '@/remote/activitypub/kernel/update/note.js'; /** * Updateアクティビティを捌きます @@ -30,6 +31,8 @@ export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver) } else if (getApType(object) === 'Question') { await updateQuestion(object, resolver).catch(e => console.log(e)); return 'ok: Question updated'; + } else if (isPost(object)) { + return await updateNote(actor, object, resolver); } else { return `skip: Unknown type: ${getApType(object)}`; } diff --git a/packages/backend/src/remote/activitypub/kernel/update/note.ts b/packages/backend/src/remote/activitypub/kernel/update/note.ts new file mode 100644 index 000000000..1c1140241 --- /dev/null +++ b/packages/backend/src/remote/activitypub/kernel/update/note.ts @@ -0,0 +1,50 @@ +import { IRemoteUser } from '@/models/entities/user.js'; +import { getApId } from '@/remote/activitypub/type.js'; +import { Resolver } from '@/remote/activitypub/resolver.js'; +import { Notes } from '@/models/index.js'; +import createNote from '@/remote/activitypub/kernel/create/note.js'; +import { getApLock } from '@/misc/app-lock.js'; +import { updateNote } from '@/remote/activitypub/models/note.js'; + +export async function update(actor: IRemoteUser, note: IObject, resolver: Resolver): Promise { + // check whether note exists + const uri = getApId(note); + const exists = await Notes.findOneBy({ uri }); + + if (exists == null) { + // does not yet exist, handle as if this was a create activity + // and since this is not a direct creation, handle it silently + createNote(resolver, actor, note, true); + + const unlock = await getApLock(uri); + try { + // if creating was successful... + const existsNow = await Notes.findOneByOrFail({ uri }); + // set the updatedAt timestamp since the note was changed + await Notes.update(existsNow.id, { updatedAt: new Date() }); + return 'ok: unknown note created and marked as updated'; + } catch (e) { + return `skip: updated note unknown and creating rejected: ${e.message}`; + } finally { + unlock(); + } + } else { + // check that actor is authorized to update this note + if (actor.id !== exists.userId) { + return 'skip: actor not authorized to update Note'; + } + // this does not redo the checks from the Create Note kernel + // since if the note made it into the database, we assume + // those checks must have been passed before. + + const unlock = await getApLock(uri); + try { + await updateNote(note, actor, resolver); + return 'ok: note updated'; + } catch (e) { + return `skip: update note rejected: ${e.message}`; + } finally { + unlock(); + } + } +} diff --git a/packages/backend/src/remote/activitypub/misc/auth-user.ts b/packages/backend/src/remote/activitypub/misc/auth-user.ts index e140d2fa1..94c60ef00 100644 --- a/packages/backend/src/remote/activitypub/misc/auth-user.ts +++ b/packages/backend/src/remote/activitypub/misc/auth-user.ts @@ -4,6 +4,7 @@ import { IRemoteUser } from '@/models/entities/user.js'; import { UserPublickey } from '@/models/entities/user-publickey.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { createPerson } from '@/remote/activitypub/models/person.js'; +import { Resolver } from '@/remote/activitypub/resolver.js'; export type AuthUser = { user: IRemoteUser; @@ -29,8 +30,8 @@ function authUserFromApId(uri: string): Promise { }); } -export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise { - let authUser = await publicKeyCache.fetch(keyId) +export async function authUserFromKeyId(keyId: string): Promise { + return await publicKeyCache.fetch(keyId) .then(async key => { if (!key) return null; else return { @@ -38,6 +39,10 @@ export async function getAuthUser(keyId: string, actorUri: string, resolver: Res key, }; }); +} + +export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise { + let authUser = await authUserFromKeyId(keyId); if (authUser != null) return authUser; authUser = await authUserFromApId(actorUri); diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 687582d20..02e2eca6c 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -1,14 +1,14 @@ import promiseLimit from 'promise-limit'; - +import * as foundkey from 'foundkey-js'; import config from '@/config/index.js'; import post from '@/services/note/create.js'; -import { IRemoteUser } from '@/models/entities/user.js'; +import { User, IRemoteUser } from '@/models/entities/user.js'; import { unique, toArray, toSingle } from '@/prelude/array.js'; import { vote } from '@/services/note/polls/vote.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { Polls, MessagingMessages } from '@/models/index.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; +import { Polls, MessagingMessages, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { Emoji } from '@/models/entities/emoji.js'; import { genId } from '@/misc/gen-id.js'; @@ -27,6 +27,8 @@ import { resolveImage } from './image.js'; import { extractApHashtags, extractQuoteUrl, extractEmojis } from './tag.js'; import { extractPollFromQuestion } from './question.js'; import { extractApMentions } from './mention.js'; +import { normalizeForSearch } from '@/misc/normalize-for-search.js'; +import { sideEffects } from '@/services/note/side-effects.js'; export function validateNote(object: IObject): Error | null { if (object == null) { @@ -45,9 +47,9 @@ export function validateNote(object: IObject): Error | null { } // Check that the server is authorized to act on behalf of this author. - const expectHost = extractDbHost(id); + const expectHost = extractPunyHost(id); const attributedToHost = object.attributedTo - ? extractDbHost(getOneApId(object.attributedTo)) + ? extractPunyHost(getOneApId(object.attributedTo)) : null; if (attributedToHost !== expectHost) { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`); @@ -93,7 +95,7 @@ async function processContent(actor: IRemoteUser, note: IPost, quoteUri: string text = fromHtml(note.content, quoteUri); } - const emojis = await extractEmojis(note.tag || [], extractDbHost(getApId(note))).catch(e => { + const emojis = await extractEmojis(note.tag || [], extractPunyHost(getApId(note))).catch(e => { apLogger.info(`extractEmojis: ${e}`); return [] as Emoji[]; }); @@ -299,7 +301,7 @@ export async function resolveNote(value: string | IObject, resolver: Resolver): if (uri == null) throw new Error('missing uri'); // Interrupt if blocked. - if (await shouldBlockInstance(extractDbHost(uri))) throw new StatusError('host blocked', 451, `host ${extractDbHost(uri)} is blocked`); + if (await shouldBlockInstance(extractPunyHost(uri))) throw new StatusError('host blocked', 451, `host ${extractPunyHost(uri)} is blocked`); const unlock = await getApLock(uri); @@ -324,3 +326,45 @@ export async function resolveNote(value: string | IObject, resolver: Resolver): unlock(); } } + +/** + * Update a note. + * + * If the target Note is not registered, it will be ignored. + */ +export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise { + const err = validateNote(value); + if (err) { + apLogger.error(`${err.message}`); + throw new Error('invalid updated note'); + } + + const uri = getApId(value); + const exists = await Notes.findOneBy({ uri }); + if (exists == null) return null; + + let quoteUri = null; + if (exists.renoteId && !foundkey.entities.isPureRenote(exists)) { + const quote = await Notes.findOneBy({ id: exists.renoteId }); + quoteUri = quote.uri; + } + + // process content and update attached files (e.g. also image descriptions) + const processedContent = await processContent(actor, value, quoteUri, resolver); + + // update note content itself + await Notes.update(exists.id, { + updatedAt: new Date(), + + cw: processedContent.cw, + fileIds: processedContent.files.map(file => file.id), + attachedFileTypes: processedContent.files.map(file => file.type), + text: processedContent.text, + emojis: processedContent.apEmoji, + tags: processedContent.apHashtags.map(tag => normalizeForSearch(tag)), + url: processedContent.url, + name: processedContent.name, + }); + + await sideEffects(actor, await Notes.findOneByOrFail({ id: exists.id }), false, false); +} diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index ed6e9fa03..3fe042a82 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -13,7 +13,7 @@ import { genId } from '@/misc/gen-id.js'; import { instanceChart, usersChart } from '@/services/chart/index.js'; import { UserPublickey } from '@/models/entities/user-publickey.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { toArray } from '@/prelude/array.js'; import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; @@ -57,7 +57,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise { // This check is security critical. // Without this check, an entry could be inserted into UserPublickey for a local user. - if (extractDbHost(uri) === extractDbHost(config.url)) { + if (extractPunyHost(uri) === extractPunyHost(config.url)) { throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); } @@ -108,7 +108,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise { // This is a security critical check to not insert or change an entry of // UserPublickey to point to a local key id. - if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) { + if (extractPunyHost(uri) !== extractPunyHost(x.publicKey.id)) { throw new Error('invalid Actor: publicKey.id has different host'); } } @@ -157,7 +157,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver): apLogger.info(`Creating the Person: ${person.id}`); - const host = extractDbHost(object.id); + const host = extractPunyHost(object.id); const { fields } = analyzeAttachments(person.attachment || []); diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 44e05b9e3..51fe89b35 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -1,6 +1,6 @@ import { ILocalUser } from '@/models/entities/user.js'; import { getInstanceActor } from '@/services/instance-actor.js'; -import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; +import { extractPunyHost, isSelfHost } from '@/misc/convert-host.js'; import { Notes, NoteReactions, Polls, Users } from '@/models/index.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; import { renderLike } from '@/remote/activitypub/renderer/like.js'; @@ -50,7 +50,7 @@ export class Resolver { if (typeof value !== 'string') { if (typeof value.id !== 'undefined') { - const host = extractDbHost(getApId(value)); + const host = extractPunyHost(getApId(value)); if (await shouldBlockInstance(host)) { throw new Error('instance is blocked'); } @@ -73,7 +73,7 @@ export class Resolver { } this.history.add(value); - const host = extractDbHost(value); + const host = extractPunyHost(value); if (isSelfHost(host)) { return await this.resolveLocal(value); } diff --git a/packages/backend/src/remote/http-signature.ts b/packages/backend/src/remote/http-signature.ts new file mode 100644 index 000000000..a5925292b --- /dev/null +++ b/packages/backend/src/remote/http-signature.ts @@ -0,0 +1,95 @@ +import { URL } from 'node:url'; +import { extractPunyHost } from "@/misc/convert-host.js"; +import { shouldBlockInstance } from "@/misc/should-block-instance.js"; +import httpSignature from "@peertube/http-signature"; +import { Resolver } from "./activitypub/resolver.js"; +import { StatusError } from "@/misc/fetch.js"; +import { AuthUser, authUserFromKeyId, getAuthUser } from "./activitypub/misc/auth-user.js"; +import { ApObject, getApId, isActor } from "./activitypub/type.js"; +import { createPerson } from "./activitypub/models/person.js"; + +async function resolveKeyId(keyId: string, resolver: Resolver): Promise { + // Do we already know that keyId? + const authUser = await authUserFromKeyId(keyId); + if (authUser != null) return authUser; + + // If not, discover it. + const keyUrl = new URL(keyId); + keyUrl.hash = ''; // Fragment should not be part of the request. + + const keyObject = await resolver.resolve(keyUrl.toString()); + + // Does the keyId end up resolving to an Actor? + if (isActor(keyObject)) { + await createPerson(keyObject, resolver); + return await getAuthUser(keyId, getApId(keyObject), resolver); + } + + // Does the keyId end up resolving to a Key-like? + const keyData = keyObject as any; + if (keyData.owner != null && keyData.publicKeyPem != null) { + await createPerson(keyData.owner, resolver); + return await getAuthUser(keyId, getApId(keyData.owner), resolver); + } + + // Cannot be resolved. + return null; +} + +export type SignatureValidationResult = { + status: 'missing' | 'invalid' | 'rejected'; + authUser: AuthUser | null; +} | { + status: 'valid'; + authUser: AuthUser; +}; + +export async function verifyHttpSignature(signature: httpSignature.IParsedSignature, resolver: Resolver, actor?: ApObject): Promise { + // This old `keyId` format is no longer supported. + const keyIdLower = signature.keyId.toLowerCase(); + if (keyIdLower.startsWith('acct:')) return { status: 'invalid', authUser: null }; + + const host = extractPunyHost(keyIdLower); + + // Reject if the host is blocked. + if (await shouldBlockInstance(host)) return { status: 'rejected', authUser: null }; + + let authUser = null; + try { + if (actor != null) { + authUser = await getAuthUser(signature.keyId, getApId(actor), resolver); + } else { + authUser = await resolveKeyId(signature.keyId, resolver); + } + } catch (e) { + if (e instanceof StatusError) { + if (e.isClientError) { + // Actor is deleted. + return { status: 'rejected', authUser }; + } else { + throw new Error(`Error in signature ${signature} - ${e.statusCode || e}`); + } + } + } + + if (authUser == null) { + // Key not found? Unacceptable! + return { status: 'invalid', authUser }; + } else { + // Found key! + } + + // Make sure the resolved user matches the keyId host. + if (authUser.user.host !== host) return { status: 'rejected', authUser }; + + // Verify the HTTP Signature + const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + if (httpSignatureValidated === true) + return { + status: 'valid', + authUser, + }; + + // Otherwise, fail. + return { status: 'invalid', authUser }; +} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 9ddda96d9..6feb06ba6 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -20,6 +20,11 @@ import Outbox from './activitypub/outbox.js'; import Followers from './activitypub/followers.js'; import Following from './activitypub/following.js'; import Featured from './activitypub/featured.js'; +import { isInstanceActor } from '@/services/instance-actor.js'; +import { getUser } from './api/common/getters.js'; +import config from '@/config/index.js'; +import { verifyHttpSignature } from '@/remote/http-signature.js'; +import { Resolver } from '@/remote/activitypub/resolver.js'; // Init router const router = new Router(); @@ -59,6 +64,36 @@ export function setResponseType(ctx: Router.RouterContext): void { } } +async function handleSignature(ctx: Router.RouterContext): Promise { + if (config.allowUnsignedFetches) { + // Fetch signature verification is disabled. + ctx.set('Cache-Control', 'public, max-age=180'); + return true; + } else { + let verified; + try { + let signature = httpSignature.parseRequest(ctx.req); + verified = await verifyHttpSignature(signature, new Resolver()); + } catch (e) { + verified = { status: 'missing' }; + } + + switch (verified.status) { + // Fetch signature verification succeeded. + case 'valid': + ctx.set('Cache-Control', 'no-store'); + return true; + case 'missing': + case 'invalid': + case 'rejected': + default: + ctx.status = 403; + ctx.set('Cache-Control', 'no-store'); + return false; + } + } +} + // inbox router.post('/inbox', json(), inbox); router.post('/users/:user/inbox', json(), inbox); @@ -66,6 +101,7 @@ router.post('/users/:user/inbox', json(), inbox); // note router.get('/notes/:note', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); + if (!(await handleSignature(ctx))) return; const note = await Notes.findOneBy({ id: ctx.params.note, @@ -89,7 +125,6 @@ router.get('/notes/:note', async (ctx, next) => { } ctx.body = renderActivity(await renderNote(note, false)); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); }); @@ -103,6 +138,7 @@ router.get('/notes/:note/activity', async ctx => { ctx.redirect(`/notes/${ctx.params.note}`); return; } + if (!(await handleSignature(ctx))) return; const note = await Notes.findOneBy({ id: ctx.params.note, @@ -117,23 +153,32 @@ router.get('/notes/:note/activity', async ctx => { } ctx.body = renderActivity(await renderNoteOrRenoteActivity(note)); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); }); +async function requireHttpSignature(ctx: Router.Context, next: () => Promise) { + if (!(await handleSignature(ctx))) { + return; + } else { + await next(); + } +} + // outbox -router.get('/users/:user/outbox', Outbox); +router.get('/users/:user/outbox', requireHttpSignature, Outbox); // followers -router.get('/users/:user/followers', Followers); +router.get('/users/:user/followers', requireHttpSignature, Followers); // following -router.get('/users/:user/following', Following); +router.get('/users/:user/following', requireHttpSignature, Following); // featured -router.get('/users/:user/collections/featured', Featured); +router.get('/users/:user/collections/featured', requireHttpSignature, Featured); // publickey +// This does not require HTTP signatures in order for other instances +// to be able to verify our own signatures. router.get('/users/:user/publickey', async ctx => { const userId = ctx.params.user; @@ -166,7 +211,6 @@ async function userInfo(ctx: Router.RouterContext, user: User | null): Promise { isSuspended: false, }); + // Allow fetching the instance actor without any HTTP signature. + // Only on this route, as it is the canonical route. + // If the user could not be resolved, or is not the instance actor, + // validate and enforce signatures. + if (user == null || !isInstanceActor(user)) + { + if (!(await handleSignature(ctx))) return; + } + else if (isInstanceActor(user)) + { + // Set cache at all times for instance actors. + ctx.set('Cache-Control', 'public, max-age=180'); + } + await userInfo(ctx, user); }); router.get('/@:user', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); + if (!(await handleSignature(ctx))) return; const user = await Users.findOneBy({ usernameLower: ctx.params.user.toLowerCase(), @@ -198,6 +257,9 @@ router.get('/@:user', async (ctx, next) => { // emoji router.get('/emojis/:emoji', async ctx => { + // Enforcing HTTP signatures on Emoji objects could cause problems for + // other software that might use those objects for copying custom emoji. + const emoji = await Emojis.findOneBy({ host: IsNull(), name: ctx.params.emoji, @@ -215,6 +277,7 @@ router.get('/emojis/:emoji', async ctx => { // like router.get('/likes/:like', async ctx => { + if (!(await handleSignature(ctx))) return; const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); if (reaction == null) { @@ -233,12 +296,12 @@ router.get('/likes/:like', async ctx => { } ctx.body = renderActivity(await renderLike(reaction, note)); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); }); // follow router.get('/follows/:follower/:followee', async ctx => { + if (!(await handleSignature(ctx))) return; // This may be used before the follow is completed, so we do not // check if the following exists. @@ -259,7 +322,6 @@ router.get('/follows/:follower/:followee', async ctx => { } ctx.body = renderActivity(renderFollow(follower, followee)); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); }); diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts index 09906250f..e33d89cbf 100644 --- a/packages/backend/src/server/activitypub/featured.ts +++ b/packages/backend/src/server/activitypub/featured.ts @@ -36,6 +36,5 @@ export default async (ctx: Router.RouterContext) => { ); ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); }; diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index 2c2b6cfb4..d6f42fba4 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -82,7 +82,6 @@ export default async (ctx: Router.RouterContext) => { // index page const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); } }; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index 4e156a19f..252eaf504 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -82,7 +82,6 @@ export default async (ctx: Router.RouterContext) => { // index page const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); } }; diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index a0a6af011..a0b69f44f 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -90,7 +90,6 @@ export default async (ctx: Router.RouterContext) => { `${partOf}?page=true&since_id=000000000000000000000000`, ); ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); } }; diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 31b0f1266..dfa2721cb 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -2,7 +2,7 @@ import { createPerson } from '@/remote/activitypub/models/person.js'; import { createNote } from '@/remote/activitypub/models/note.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; -import { extractDbHost } from '@/misc/convert-host.js'; +import { extractPunyHost } from '@/misc/convert-host.js'; import { Users, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; import { ILocalUser, User } from '@/models/entities/user.js'; @@ -87,7 +87,7 @@ export default define(meta, paramDef, async (ps, me) => { */ async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise | null> { // Stop if the host is blocked. - const host = extractDbHost(uri); + const host = extractPunyHost(uri); if (await shouldBlockInstance(host)) { return null; } diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index ace59d32d..bb0b17310 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -3,7 +3,7 @@ import { WebSocket } from 'ws'; import { readNote } from '@/services/note/read.js'; import { User } from '@/models/entities/user.js'; import { Channel as ChannelModel } from '@/models/entities/channel.js'; -import { Followings, Mutings, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; +import { Followings, Mutings, Notes, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js'; @@ -14,6 +14,7 @@ import { channels } from './channels/index.js'; import Channel from './channel.js'; import { StreamEventEmitter, StreamMessages } from './types.js'; import Logger from '@/services/logger.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; const logger = new Logger('streaming'); @@ -254,11 +255,43 @@ export class Connection { } private async onNoteStreamMessage(data: StreamMessages['note']['payload']) { - this.sendMessageToWs('noteUpdated', { - id: data.body.id, - type: data.type, - body: data.body.body, - }); + if (data.type === 'updated') { + const note = data.body.body.note; + // FIXME analogous to Channel.withPackedNote, but for some reason, the note + // stream is not handled as a channel but instead handled at the top level + // so this code is duplicated here I guess. + try { + // because `note` was previously JSON.stringify'ed, the fields that + // were objects before are now strings and have to be restored or + // removed from the object + note.createdAt = new Date(note.createdAt); + note.reply = null; + note.renote = null; + note.user = null; + note.channel = null; + + const packed = await Notes.pack(note, this.user, { detail: true }); + + this.sendMessageToWs('noteUpdated', { + id: data.body.id, + type: 'updated', + body: { note: packed }, + }); + } catch (err) { + if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') { + // skip: note not visible to user + return; + } else { + logger.error(err); + } + } + } else { + this.sendMessageToWs('noteUpdated', { + id: data.body.id, + type: data.type, + body: data.body.body, + }); + } } /** diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 6fab792ae..08ac6f694 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -128,6 +128,9 @@ export interface NoteStreamTypes { reaction: string; userId: User['id']; }; + updated: { + note: Note; + }; } type NoteStreamEventTypes = { [key in keyof NoteStreamTypes]: { diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index 6ce55239c..380383ee1 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -2,7 +2,7 @@ import Router from '@koa/router'; import { IsNull, MoreThan } from 'typeorm'; import config from '@/config/index.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, Notes } from '@/models/index.js'; +import { Users, Notes, Webhooks } from '@/models/index.js'; import { MONTH, DAY } from '@/const.js'; const router = new Router(); @@ -52,12 +52,14 @@ const nodeinfo2 = async (): Promise => { activeHalfyear, activeMonth, localPosts, + activeWebhooks, ] = await Promise.all([ fetchMeta(true), Users.count({ where: { host: IsNull() } }), Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 180 * DAY)) } }), Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - MONTH)) } }), Notes.count({ where: { userHost: IsNull() } }), + Webhooks.countBy({ active: true }), ]); const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null; @@ -100,6 +102,7 @@ const nodeinfo2 = async (): Promise => { enableEmail: meta.enableEmail, proxyAccountName: proxyAccount?.username ?? null, themeColor: meta.themeColor || '#86b300', + activeWebhooks, }, }; }; diff --git a/packages/backend/src/services/drive/read-file.ts b/packages/backend/src/services/drive/read-file.ts new file mode 100644 index 000000000..ef9f0b60c --- /dev/null +++ b/packages/backend/src/services/drive/read-file.ts @@ -0,0 +1,14 @@ +import * as fs from 'node:fs'; +import { DriveFiles } from '@/models/index.js'; +import { DriveFile } from '@/models/entities/drive-file.js'; +import { InternalStorage } from './internal-storage.js'; +import { downloadUrl } from '@/misc/download-url.js'; + +export async function copyFileTo(file: DriveFile, toPath: string): Promise { + if (file.storedInternal) { + const fromPath = InternalStorage.resolvePath(file.accessKey); + fs.copyFileSync(fromPath, toPath); + } else { + await downloadUrl(file.url, toPath); + } +} diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts index 15bdc674f..5e7be93f0 100644 --- a/packages/backend/src/services/instance-actor.ts +++ b/packages/backend/src/services/instance-actor.ts @@ -1,5 +1,5 @@ import { IsNull } from 'typeorm'; -import { ILocalUser } from '@/models/entities/user.js'; +import { ILocalUser, User } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { getSystemUser } from './system-user.js'; @@ -17,3 +17,7 @@ export async function getInstanceActor(): Promise { return instanceActor; } + +export function isInstanceActor(user: User): boolean { + return user.username === ACTOR_USERNAME && user.host === null; +} diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 9c0500bda..0d9c1a3a4 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -1,109 +1,21 @@ -import { ArrayOverlap, Not, In } from 'typeorm'; import * as mfm from 'mfm-js'; import { db } from '@/db/postgre.js'; -import { publishMainStream, publishNotesStream } from '@/services/stream.js'; -import { DeliverManager } from '@/remote/activitypub/deliver-manager.js'; -import renderNote from '@/remote/activitypub/renderer/note.js'; -import renderCreate from '@/remote/activitypub/renderer/create.js'; -import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { resolveUser } from '@/remote/resolve-user.js'; import { concat } from '@/prelude/array.js'; -import { insertNoteUnread } from '@/services/note/unread.js'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import { Note } from '@/models/entities/note.js'; -import { Mutings, Users, NoteWatchings, Notes, Instances, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; +import { Users, Channels } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { App } from '@/models/entities/app.js'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { genId } from '@/misc/gen-id.js'; -import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index.js'; import { Poll, IPoll } from '@/models/entities/poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { checkHitAntenna } from '@/misc/check-hit-antenna.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { countSameRenotes } from '@/misc/count-same-renotes.js'; import { Channel } from '@/models/entities/channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { getAntennas } from '@/misc/antenna-cache.js'; -import { endedPollNotificationQueue } from '@/queue/queues.js'; -import { webhookDeliver } from '@/queue/index.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { IActivity } from '@/remote/activitypub/type.js'; -import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js'; -import { updateHashtags } from '../update-hashtag.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import { createNotification } from '../create-notification.js'; -import { addNoteToAntenna } from '../add-note-to-antenna.js'; -import { deliverToRelays } from '../relay.js'; -import { mutedWordsCache, index } from './index.js'; - -type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; - -class NotificationManager { - private notifier: { id: User['id']; }; - private note: Note; - private queue: { - target: ILocalUser['id']; - reason: NotificationType; - }[]; - - constructor(notifier: { id: User['id']; }, note: Note) { - this.notifier = notifier; - this.note = note; - this.queue = []; - } - - public push(notifiee: ILocalUser['id'], reason: NotificationType): void { - // No notification to yourself. - if (this.notifier.id === notifiee) return; - - const exist = this.queue.find(x => x.target === notifiee); - - if (exist) { - // If you have been "mentioned and replied to," make the notification as a reply, not as a mention. - if (reason !== 'mention') { - exist.reason = reason; - } - } else { - this.queue.push({ - reason, - target: notifiee, - }); - } - } - - public async deliver(): Promise { - for (const x of this.queue) { - // check if the sender or thread are muted - const userMuted = await Mutings.countBy({ - muterId: x.target, - muteeId: this.notifier.id, - }); - - const threadMuted = await NoteThreadMutings.countBy({ - userId: x.target, - threadId: In([ - // replies - this.note.threadId ?? this.note.id, - // renotes - this.note.renoteId ?? undefined, - ]), - mutingNotificationTypes: ArrayOverlap([x.reason]), - }); - - if (!userMuted && !threadMuted) { - createNotification(x.target, x.reason, { - notifierId: this.notifier.id, - noteId: this.note.id, - }); - } - } - } -} +import { sideEffects } from './side-effects.js'; type MinimumUser = { id: User['id']; @@ -238,253 +150,9 @@ export default async (user: { id: User['id']; username: User['username']; host: res(note); - // Update Statistics - notesChart.update(note, true); - perUserNotesChart.update(user, note, true); - - // Register host - if (Users.isRemoteUser(user)) { - registerOrFetchInstanceDoc(user.host).then(i => { - Instances.increment({ id: i.id }, 'notesCount', 1); - instanceChart.updateNote(i.host, note, true); - }); - } - - // Hashtag Update - if (data.visibility === 'public' || data.visibility === 'home') { - updateHashtags(user, tags); - } - - // Increment notes count (user) - incNotesCountOfUser(user); - - // Word mute - mutedWordsCache.fetch('').then(us => { - for (const u of us) { - checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { - if (shouldMute) { - MutedNotes.insert({ - id: genId(), - userId: u.userId, - noteId: note.id, - reason: 'word', - }); - } - }); - } - }); - - // Antenna - for (const antenna of (await getAntennas())) { - checkHitAntenna(antenna, note, user).then(hit => { - if (hit) { - addNoteToAntenna(antenna, note, user); - } - }); - } - - // Channel - if (note.channelId) { - ChannelFollowings.findBy({ followeeId: note.channelId }).then(followings => { - for (const following of followings) { - insertNoteUnread(following.followerId, note, { - isSpecified: false, - isMentioned: false, - }); - } - }); - } - - if (data.reply) { - saveReply(data.reply, note); - } - - // When there is no re-note of the specified note by the specified user except for this post - if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) { - incRenoteCount(data.renote); - } - - if (data.poll && data.poll.expiresAt) { - const delay = data.poll.expiresAt.getTime() - Date.now(); - endedPollNotificationQueue.add({ - noteId: note.id, - }, { - delay, - removeOnComplete: true, - }); - } - - if (!silent) { - if (Users.isLocalUser(user)) activeUsersChart.write(user); - - // Create unread notifications - if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); - - for (const u of data.visibleUsers) { - // Local users only - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: true, - isMentioned: false, - }); - } - } else { - for (const u of mentionedUsers) { - // Local users only - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: false, - isMentioned: true, - }); - } - } - - publishNotesStream(note); - - const webhooks = await getActiveWebhooks().then(webhooks => webhooks.filter(x => x.userId === user.id && x.on.includes('note'))); - - for (const webhook of webhooks) { - webhookDeliver(webhook, 'note', { - note: await Notes.pack(note, user), - }); - } - - const nm = new NotificationManager(user, note); - const nmRelatedPromises = []; - - await createMentionedEvents(mentionedUsers, note, nm); - - // If has in reply to note - if (data.reply) { - // Fetch watchers - nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); - - // 通知 - if (data.reply.userHost === null) { - const threadMuted = await NoteThreadMutings.countBy({ - userId: data.reply.userId, - threadId: data.reply.threadId || data.reply.id, - }); - - if (!threadMuted) { - nm.push(data.reply.userId, 'reply'); - - const packedReply = await Notes.pack(note, { id: data.reply.userId }); - publishMainStream(data.reply.userId, 'reply', packedReply); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'reply', { - note: packedReply, - }); - } - } - } - } - - // If it is renote - if (data.renote) { - const type = data.text ? 'quote' : 'renote'; - - // Notify - if (data.renote.userHost === null) { - nm.push(data.renote.userId, type); - } - - // Fetch watchers - nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type)); - - // Publish event - if ((user.id !== data.renote.userId) && data.renote.userHost === null) { - const packedRenote = await Notes.pack(note, { id: data.renote.userId }); - publishMainStream(data.renote.userId, 'renote', packedRenote); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'renote', { - note: packedRenote, - }); - } - } - } - - Promise.all(nmRelatedPromises).then(() => { - nm.deliver(); - }); - - //#region AP deliver - if (Users.isLocalUser(user) && !data.localOnly) { - (async () => { - const noteActivity = renderActivity(await renderNoteOrRenoteActivity(note)); - const dm = new DeliverManager(user, noteActivity); - - // Delivered to remote users who have been mentioned - for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); - } - - // If the post is a reply and the poster is a local user and the poster of the post to which you are replying is a remote user, deliver - if (data.reply && data.reply.userHost !== null) { - const u = await Users.findOneBy({ id: data.reply.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // If the post is a Renote and the poster is a local user and the poster of the original Renote post is a remote user, deliver - if (data.renote && data.renote.userHost !== null) { - const u = await Users.findOneBy({ id: data.renote.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // Deliver to followers - if (['public', 'home', 'followers'].includes(note.visibility)) { - dm.addFollowersRecipe(); - } - - if (['public'].includes(note.visibility)) { - deliverToRelays(user, noteActivity); - } - - dm.execute(); - })(); - } - //#endregion - } - - if (data.channel) { - Channels.increment({ id: data.channel.id }, 'notesCount', 1); - Channels.update(data.channel.id, { - lastNotedAt: new Date(), - }); - - const count = await Notes.countBy({ - userId: user.id, - channelId: data.channel.id, - }); - - // This process takes place after the note is created, so if there is only one note, you can determine that it is the first submission. - // TODO: but there's also the messiness of deleting a note and posting it multiple times, which is incremented by the number of times it's posted, so I'd like to do something about that. - if (count === 1) { - Channels.increment({ id: data.channel.id }, 'usersCount', 1); - } - } - - // Register to search database - index(note); + sideEffects(user, note, silent, true); }); -function incRenoteCount(renote: Note): void { - Notes.createQueryBuilder().update() - .set({ - renoteCount: () => '"renoteCount" + 1', - score: () => '"score" + 1', - }) - .where('id = :id', { id: renote.id }) - .execute(); -} - async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: MinimumUser[]): Promise { const createdAt = data.createdAt ?? new Date(); @@ -563,77 +231,6 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O } } -async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise { - const watchers = await NoteWatchings.findBy({ - noteId: renote.id, - userId: Not(user.id), - }); - - for (const watcher of watchers) { - nm.push(watcher.userId, type); - } -} - -async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager): Promise { - const watchers = await NoteWatchings.findBy({ - noteId: reply.id, - userId: Not(user.id), - }); - - for (const watcher of watchers) { - nm.push(watcher.userId, 'reply'); - } -} - -async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager): Promise { - for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { - const threadMuted = await NoteThreadMutings.countBy({ - userId: u.id, - threadId: note.threadId || note.id, - }); - - if (threadMuted) { - continue; - } - - // note with "specified" visibility might not be visible to mentioned users - try { - const detailPackedNote = await Notes.pack(note, u, { - detail: true, - }); - - publishMainStream(u.id, 'mention', detailPackedNote); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'mention', { - note: detailPackedNote, - }); - } - } catch (err) { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') continue; - throw err; - } - - // Create notification - nm.push(u.id, 'mention'); - } -} - -function saveReply(reply: Note): void { - Notes.increment({ id: reply.id }, 'repliesCount', 1); -} - -function incNotesCountOfUser(user: { id: User['id']; }): void { - Users.createQueryBuilder().update() - .set({ - updatedAt: new Date(), - notesCount: () => '"notesCount" + 1', - }) - .where('id = :id', { id: user.id }) - .execute(); -} - async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise { if (tokens.length === 0) return []; diff --git a/packages/backend/src/services/note/side-effects.ts b/packages/backend/src/services/note/side-effects.ts new file mode 100644 index 000000000..d3d787bd7 --- /dev/null +++ b/packages/backend/src/services/note/side-effects.ts @@ -0,0 +1,474 @@ +import { ArrayOverlap, Not, In } from 'typeorm'; +import * as mfm from 'mfm-js'; +import { publishMainStream, publishNoteStream, publishNotesStream } from '@/services/stream.js'; +import { DeliverManager } from '@/remote/activitypub/deliver-manager.js'; +import { renderActivity } from '@/remote/activitypub/renderer/index.js'; +import { resolveUser } from '@/remote/resolve-user.js'; +import { insertNoteUnread } from '@/services/note/unread.js'; +import { extractMentions } from '@/misc/extract-mentions.js'; +import { extractHashtags } from '@/misc/extract-hashtags.js'; +import { Note } from '@/models/entities/note.js'; +import { AntennaNotes, Mutings, Users, NoteWatchings, Notes, Instances, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; +import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; +import { genId } from '@/misc/gen-id.js'; +import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index.js'; +import { checkHitAntenna } from '@/misc/check-hit-antenna.js'; +import { checkWordMute } from '@/misc/check-word-mute.js'; +import { countSameRenotes } from '@/misc/count-same-renotes.js'; +import { getAntennas } from '@/misc/antenna-cache.js'; +import { endedPollNotificationQueue } from '@/queue/queues.js'; +import { webhookDeliver } from '@/queue/index.js'; +import { getActiveWebhooks } from '@/misc/webhook-cache.js'; +import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js'; +import { updateHashtags } from '../update-hashtag.js'; +import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; +import { createNotification } from '../create-notification.js'; +import { addNoteToAntenna } from '../add-note-to-antenna.js'; +import { deliverToRelays } from '../relay.js'; +import { mutedWordsCache, index } from './index.js'; +import { Polls } from '@/models/index.js'; +import { Poll } from '@/models/entities/poll.js'; + + +type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'update'; + +class NotificationManager { + private notifier: { id: User['id']; }; + private note: Note; + private queue: { + target: ILocalUser['id']; + reason: NotificationType; + }[]; + + constructor(notifier: { id: User['id']; }, note: Note) { + this.notifier = notifier; + this.note = note; + this.queue = []; + } + + public push(notifiee: ILocalUser['id'], reason: NotificationType): void { + // No notification to yourself. + if (this.notifier.id === notifiee) return; + + const exist = this.queue.find(x => x.target === notifiee); + + if (exist) { + // If you have been "mentioned and replied to," make the notification as a reply, not as a mention. + if (reason !== 'mention') { + exist.reason = reason; + } + } else { + this.queue.push({ + reason, + target: notifiee, + }); + } + } + + public async deliver(): Promise { + for (const x of this.queue) { + // check if the sender or thread are muted + const userMuted = await Mutings.countBy({ + muterId: x.target, + muteeId: this.notifier.id, + }); + + const threadMuted = await NoteThreadMutings.countBy({ + userId: x.target, + threadId: In([ + // replies + this.note.threadId ?? this.note.id, + // renotes + this.note.renoteId ?? undefined, + ]), + mutingNotificationTypes: ArrayOverlap([x.reason]), + }); + + if (!userMuted && !threadMuted) { + createNotification(x.target, x.reason, { + notifierId: this.notifier.id, + noteId: this.note.id, + }); + } + } + } +} + +/** + * Perform side effects for a Note such as incrementing statistics, updating hashtag usage etc. + * + * @param user The author of the note. + * @param note The note for which the side effects should be performed. + * @param silent Whether notifications and similar side effects should be suppressed. + * @param created Whether statistics should be incremented (i.e. the note was inserted and not updated in the database) + */ +export async function sideEffects(user: User, note: Note, silent = false, created = true): Promise { + if (created) { + // Update Statistics + notesChart.update(note, true); + perUserNotesChart.update(user, note, true); + Users.createQueryBuilder().update() + .set({ + updatedAt: new Date(), + notesCount: () => '"notesCount" + 1', + }) + .where('id = :id', { id: user.id }) + .execute(); + + if (Users.isLocalUser(user)) { + activeUsersChart.write(user); + } else { + // Remote user, register host + registerOrFetchInstanceDoc(user.host).then(i => { + Instances.increment({ id: i.id }, 'notesCount', 1); + instanceChart.updateNote(i.host, note, true); + }); + } + + // Channel + if (note.channelId) { + ChannelFollowings.findBy({ followeeId: note.channelId }).then(followings => { + for (const following of followings) { + insertNoteUnread(following.followerId, note, { + isSpecified: false, + isMentioned: false, + }); + } + }); + + Channels.increment({ id: note.channelId }, 'notesCount', 1); + Channels.update(note.channelId, { + lastNotedAt: new Date(), + }); + + const count = await Notes.countBy({ + userId: user.id, + channelId: note.channelId, + }); + + // This process takes place after the note is created, so if there is only one note, you can determine that it is the first submission. + // TODO: but there's also the messiness of deleting a note and posting it multiple times, which is incremented by the number of times it's posted, so I'd like to do something about that. + if (count === 1) { + Channels.increment({ id: note.channelId }, 'usersCount', 1); + } + } + + if (note.replyId) { + Notes.increment({ id: note.replyId }, 'repliesCount', 1); + } + + // When there is no re-note of the specified note by the specified user except for this post + if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id) === 0)) { + Notes.createQueryBuilder().update() + .set({ + renoteCount: () => '"renoteCount" + 1', + score: () => '"score" + 1', + }) + .where('id = :id', { id: note.renoteId }) + .execute(); + } + + // create job for ended poll notifications + if (note.hasPoll) { + Polls.findOneByOrFail({ noteId: note.id }) + .then((poll: Poll) => { + if (poll.expiresAt) { + const delay = poll.expiresAt.getTime() - Date.now(); + endedPollNotificationQueue.add({ + noteId: note.id, + }, { + delay, + removeOnComplete: true, + }); + } + }) + } + } + + const { mentionedUsers, tags } = await extractFromMfm(user, note); + + // Hashtag Update + if (note.visibility === 'public' || note.visibility === 'home') { + updateHashtags(user, tags); + } + + // Word mute + mutedWordsCache.fetch('').then(us => { + for (const u of us) { + checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { + if (shouldMute) { + MutedNotes.insert({ + id: genId(), + userId: u.userId, + noteId: note.id, + reason: 'word', + }); + } + }); + } + }); + + // Antenna + if (!created) { + // Provisionally remove from antenna, it may be added again in the next step. + // But if it is not removed, it can cause duplicate key errors when trying to + // add it to the same antenna again. + await AntennaNotes.delete({ + noteId: note.id, + }); + } + getAntennas() + .then(async antennas => { + await Promise.all(antennas.map(antenna => { + return checkHitAntenna(antenna, note, user) + .then(hit => { if (hit) return addNoteToAntenna(antenna, note, user); }); + })); + }); + + // Notifications + if (!silent && created) { + // Create unread notifications + if (note.visibility === 'specified') { + if (note.visibleUserIds == null) { + throw new Error('specified note but does not have any visible user ids'); + } + + Users.findBy({ + id: In(note.visibleUserIds), + }).then((visibleUsers: User[]) => { + visibleUsers + .filter(u => Users.isLocalUser(u)) + .forEach(u => { + insertNoteUnread(u.id, note, { + isSpecified: true, + isMentioned: false, + }); + }); + }) + } else { + mentionedUsers + .filter(u => Users.isLocalUser(u)) + .forEach(u => { + insertNoteUnread(u.id, note, { + isSpecified: false, + isMentioned: true, + }); + }) + } + + publishNotesStream(note); + + const webhooks = await getActiveWebhooks().then(webhooks => webhooks.filter(x => x.userId === user.id && x.on.includes('note'))); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'note', { + note: await Notes.pack(note, user), + }); + } + + const nm = new NotificationManager(user, note); + const nmRelatedPromises = []; + + await createMentionedEvents(mentionedUsers, note, nm); + + // If it is in reply to another note + if (note.replyId) { + const reply = await Notes.findOneByOrFail({ id: note.replyId }); + + // Fetch watchers + nmRelatedPromises.push(notifyWatchers(note.replyId, user, nm, 'reply')); + + // Notifications + if (reply.userHost === null) { + const threadMuted = await NoteThreadMutings.countBy({ + userId: reply.userId, + threadId: reply.threadId ?? reply.id, + }); + + if (!threadMuted) { + nm.push(reply.userId, 'reply'); + + const packedReply = await Notes.pack(note, { id: reply.userId }); + publishMainStream(reply.userId, 'reply', packedReply); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === reply.userId && x.on.includes('reply')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'reply', { + note: packedReply, + }); + } + } + } + } + + // If it is a renote + if (note.renoteId) { + const type = note.text ? 'quote' : 'renote'; + const renote = await Notes.findOneByOrFail({ id : note.renoteId }); + + // Notify + if (renote.userHost === null) { + nm.push(renote.userId, type); + } + + // Fetch watchers + nmRelatedPromises.push(notifyWatchers(note.renoteId, user, nm, type)); + + // Publish event + if ((user.id !== renote.userId) && renote.userHost === null) { + const packedRenote = await Notes.pack(note, { id: renote.userId }); + publishMainStream(renote.userId, 'renote', packedRenote); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === renote.userId && x.on.includes('renote')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'renote', { + note: packedRenote, + }); + } + } + } + + Promise.all(nmRelatedPromises).then(() => { + nm.deliver(); + }); + + //#region AP deliver + if (Users.isLocalUser(user) && !note.localOnly && created) { + (async () => { + const noteActivity = renderActivity(await renderNoteOrRenoteActivity(note)); + const dm = new DeliverManager(user, noteActivity); + + // Delivered to remote users who have been mentioned + for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { + dm.addDirectRecipe(u as IRemoteUser); + } + + // If the post is a reply and the poster is a local user and the poster of the post to which you are replying is a remote user, deliver + if (note.replyId) { + const subquery = Notes.createaQueryBuilder() + .select('userId') + .where('"id" = :replyId', { replyId: note.replyId }); + const u = await Users.createQueryBuilder() + .where('"id" IN (' + subquery.getQuery() + ')') + .andWhere('"userHost" IS NOT NULL') + .getOne(); + if (u != null) dm.addDirectRecipe(u); + } + + // If the post is a Renote and the poster is a local user and the poster of the original Renote post is a remote user, deliver + if (note.renoteId) { + const subquery = Notes.createaQueryBuilder() + .select('userId') + .where('"id" = :renoteId', { renoteId: note.renoteId }); + const u = await Users.createQueryBuilder() + .where('"id" IN (' + subquery.getQuery() + ')') + .andWhere('"userHost" IS NOT NULL') + .getOne(); + if (u != null) dm.addDirectRecipe(u); + } + + // Deliver to followers + if (['public', 'home', 'followers'].includes(note.visibility)) { + dm.addFollowersRecipe(); + } + + if (['public'].includes(note.visibility)) { + deliverToRelays(user, noteActivity); + } + + dm.execute(); + })(); + } + //#endregion + } else if (!created) { + // updating a note does not change its un-/read status + // updating does not trigger notifications for replied to or renoted notes + // updating does not trigger notifications for mentioned users (since mentions cannot be changed) + + // TODO publish to streaming API + publishNoteStream(note.id, 'updated', { note }); + + const nm = new NotificationManager(user, note); + notifyWatchers(note.id, user, nm, 'update'); + await nm.deliver(); + + // TODO AP deliver + } + + // Register to search database + index(note); +} + +async function notifyWatchers(noteId: Note['id'], user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise { + const watchers = await NoteWatchings.findBy({ + noteId, + userId: Not(user.id), + }); + + for (const watcher of watchers) { + nm.push(watcher.userId, type); + } +} + +async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager): Promise { + for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { + const threadMuted = await NoteThreadMutings.countBy({ + userId: u.id, + threadId: note.threadId || note.id, + }); + + if (threadMuted) { + continue; + } + + // note with "specified" visibility might not be visible to mentioned users + try { + const detailPackedNote = await Notes.pack(note, u, { + detail: true, + }); + + publishMainStream(u.id, 'mention', detailPackedNote); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'mention', { + note: detailPackedNote, + }); + } + } catch (err) { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') continue; + throw err; + } + + // Create notification + nm.push(u.id, 'mention'); + } +} + +async function extractFromMfm(user: { host: User['host']; }, note: Note): Promise<{ mentionedUsers: User[], tags: string[] }> { + const tokens = mfm.parse(note.text ?? '').concat(mfm.parse(note.cw ?? '')); + + const tags = extractHashtags(tokens); + + if (tokens.length === 0) { + return { + mentionedUsers: [], + tags, + }; + } + + const mentions = extractMentions(tokens); + + let mentionedUsers = (await Promise.all(mentions.map(m => + resolveUser(m.username, m.host || user.host).catch(() => null), + ))).filter(x => x != null) as User[]; + + // Drop duplicate users + mentionedUsers = mentionedUsers.filter((u, i, self) => + i === self.findIndex(u2 => u.id === u2.id), + ); + + return { + mentionedUsers, + tags, + }; +} diff --git a/packages/backend/src/services/system-user.ts b/packages/backend/src/services/system-user.ts index 2ad32a1c4..dc5dcdbce 100644 --- a/packages/backend/src/services/system-user.ts +++ b/packages/backend/src/services/system-user.ts @@ -12,7 +12,7 @@ import { db } from '@/db/postgre.js'; import generateNativeUserToken from '@/server/api/common/generate-native-user-token.js'; export async function getSystemUser(username: string): Promise { - const exist = await Users.findBy({ + const exist = await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), }); diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.mjs similarity index 80% rename from packages/backend/test/activitypub.ts rename to packages/backend/test/activitypub.mjs index 6ce9c7c8e..844b2dbb1 100644 --- a/packages/backend/test/activitypub.ts +++ b/packages/backend/test/activitypub.mjs @@ -1,11 +1,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { initDb } from '../src/db/postgre.js'; -import { initTestDb } from './utils.js'; +import { initDb } from '../built/db/postgre.js'; +import { initTestDb } from './utils.mjs'; -function rndstr(length): string { +function rndstr(length) { const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const chars_len = 62; @@ -52,8 +52,8 @@ describe('ActivityPub', () => { }; it('Minimum Actor', async () => { - const { MockResolver } = await import('./misc/mock-resolver.js'); - const { createPerson } = await import('../src/remote/activitypub/models/person.js'); + const { MockResolver } = await import('./misc/mock-resolver.mjs'); + const { createPerson } = await import('../built/remote/activitypub/models/person.js'); const resolver = new MockResolver(); resolver._register(actor.id, actor); @@ -66,8 +66,8 @@ describe('ActivityPub', () => { }); it('Minimum Note', async () => { - const { MockResolver } = await import('./misc/mock-resolver.js'); - const { createNote } = await import('../src/remote/activitypub/models/note.js'); + const { MockResolver } = await import('./misc/mock-resolver.mjs'); + const { createNote } = await import('../built/remote/activitypub/models/note.js'); const resolver = new MockResolver(); resolver._register(actor.id, actor); @@ -99,8 +99,8 @@ describe('ActivityPub', () => { }; it('Actor', async () => { - const { MockResolver } = await import('./misc/mock-resolver.js'); - const { createPerson } = await import('../src/remote/activitypub/models/person.js'); + const { MockResolver } = await import('./misc/mock-resolver.mjs'); + const { createPerson } = await import('../built/remote/activitypub/models/person.js'); const resolver = new MockResolver(); resolver._register(actor.id, actor); diff --git a/packages/backend/test/ap-request.ts b/packages/backend/test/ap-request.mjs similarity index 86% rename from packages/backend/test/ap-request.ts rename to packages/backend/test/ap-request.mjs index 744b2f2c9..8b5fecfa9 100644 --- a/packages/backend/test/ap-request.ts +++ b/packages/backend/test/ap-request.mjs @@ -1,9 +1,9 @@ import * as assert from 'assert'; import httpSignature from '@peertube/http-signature'; -import { genRsaKeyPair } from '../src/misc/gen-key-pair.js'; -import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js'; +import { genRsaKeyPair } from '../built/misc/gen-key-pair.js'; +import { createSignedPost, createSignedGet } from '../built/remote/activitypub/ap-request.js'; -export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { +export const buildParsedSignature = (signingString, signature, algorithm) => { return { scheme: 'Signature', params: { diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.mjs similarity index 94% rename from packages/backend/test/api-visibility.ts rename to packages/backend/test/api-visibility.mjs index 18da1d0c9..941661297 100644 --- a/packages/backend/test/api-visibility.ts +++ b/packages/backend/test/api-visibility.mjs @@ -2,12 +2,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, startServer, shutdownServer } from './utils.js'; +import { async, signup, request, post, startServer, shutdownServer } from './utils.mjs'; describe('API visibility', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; before(async () => { p = await startServer(); @@ -20,48 +20,48 @@ describe('API visibility', function() { describe('Note visibility', async () => { //#region vars /** protagonist */ - let alice: any; + let alice; /** follower */ - let follower: any; + let follower; /** non-follower */ - let other: any; + let other; /** non-follower who has been replied to or mentioned */ - let target: any; + let target; /** actor for which a specified visibility was set */ - let target2: any; + let target2; /** public-post */ - let pub: any; + let pub; /** home-post */ - let home: any; + let home; /** followers-post */ - let fol: any; + let fol; /** specified-post */ - let spe: any; + let spe; /** public-reply to target's post */ - let pubR: any; + let pubR; /** home-reply to target's post */ - let homeR: any; + let homeR; /** followers-reply to target's post */ - let folR: any; + let folR; /** specified-reply to target's post */ - let speR: any; + let speR; /** public-mention to target */ - let pubM: any; + let pubM; /** home-mention to target */ - let homeM: any; + let homeM; /** followers-mention to target */ - let folM: any; + let folM; /** specified-mention to target */ - let speM: any; + let speM; /** reply target post */ - let tgt: any; + let tgt; //#endregion - const show = async (noteId: any, by: any) => { + const show = async (noteId, by) => { return await request('/notes/show', { noteId, }, by); @@ -412,21 +412,21 @@ describe('API visibility', function() { it('[TL] public post on author home TL', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, alice); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == pub.id); + const notes = res.body.filter((n) => n.id == pub.id); assert.strictEqual(notes[0].text, 'x'); })); it('[TL] public post absent from non-follower home TL', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == pub.id); + const notes = res.body.filter((n) => n.id == pub.id); assert.strictEqual(notes.length, 0); })); it('[TL] followers post on follower home TL', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == fol.id); + const notes = res.body.filter((n) => n.id == fol.id); assert.strictEqual(notes[0].text, 'x'); })); //#endregion @@ -435,21 +435,21 @@ describe('API visibility', function() { it('[TL] followers reply on follower reply TL', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); + const notes = res.body.filter((n) => n.id == folR.id); assert.strictEqual(notes[0].text, 'x'); })); it('[TL] followers reply absent from not replied to non-follower reply TL', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); + const notes = res.body.filter((n) => n.id == folR.id); assert.strictEqual(notes.length, 0); })); it('[TL] followers reply on replied to actor reply TL', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); + const notes = res.body.filter((n) => n.id == folR.id); assert.strictEqual(notes[0].text, 'x'); })); //#endregion @@ -458,14 +458,14 @@ describe('API visibility', function() { it('[TL] followers reply on replied to non-follower mention TL', async(async () => { const res = await request('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); + const notes = res.body.filter((n) => n.id == folR.id); assert.strictEqual(notes[0].text, 'x'); })); it('[TL] followers mention on mentioned non-follower mention TL', async(async () => { const res = await request('/notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folM.id); + const notes = res.body.filter((n) => n.id == folM.id); assert.strictEqual(notes[0].text, '@target x'); })); //#endregion diff --git a/packages/backend/test/api.ts b/packages/backend/test/api.mjs similarity index 94% rename from packages/backend/test/api.ts rename to packages/backend/test/api.mjs index 8bcef2ea2..7c5b7e0b2 100644 --- a/packages/backend/test/api.ts +++ b/packages/backend/test/api.mjs @@ -2,15 +2,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; +import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.mjs'; describe('API', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; + let p; + let alice, bob, carol; before(async () => { p = await startServer(); diff --git a/packages/backend/test/block.ts b/packages/backend/test/block.mjs similarity index 86% rename from packages/backend/test/block.ts rename to packages/backend/test/block.mjs index b01096e80..ed5cf2170 100644 --- a/packages/backend/test/block.ts +++ b/packages/backend/test/block.mjs @@ -2,17 +2,15 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, startServer, shutdownServer } from './utils.js'; +import { async, signup, request, post, startServer, shutdownServer } from './utils.mjs'; describe('Block', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; // alice blocks bob - let alice: any; - let bob: any; - let carol: any; + let alice, bob, carol; before(async () => { p = await startServer(); @@ -80,8 +78,8 @@ describe('Block', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === carolNote.id), true); })); }); diff --git a/packages/backend/test/chart.ts b/packages/backend/test/chart.mjs similarity index 95% rename from packages/backend/test/chart.ts rename to packages/backend/test/chart.mjs index ac0844679..70874df16 100644 --- a/packages/backend/test/chart.ts +++ b/packages/backend/test/chart.mjs @@ -2,18 +2,14 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as lolex from '@sinonjs/fake-timers'; -import TestChart from '../src/services/chart/charts/test.js'; -import TestGroupedChart from '../src/services/chart/charts/test-grouped.js'; -import TestUniqueChart from '../src/services/chart/charts/test-unique.js'; -import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js'; -import { initDb } from '../src/db/postgre.js'; +import TestChart from '../built/services/chart/charts/test.js'; +import TestGroupedChart from '../built/services/chart/charts/test-grouped.js'; +import TestUniqueChart from '../built/services/chart/charts/test-unique.js'; +import TestIntersectionChart from '../built/services/chart/charts/test-intersection.js'; +import { initDb } from '../built/db/postgre.js'; describe('Chart', () => { - let testChart: TestChart; - let testGroupedChart: TestGroupedChart; - let testUniqueChart: TestUniqueChart; - let testIntersectionChart: TestIntersectionChart; - let clock: lolex.InstalledClock; + let testChart, testGroupedChart, testUniqueChart, testIntersectionChart, clock; beforeEach(async () => { await initDb(true); diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml deleted file mode 100644 index 5f95bec4c..000000000 --- a/packages/backend/test/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3" - -services: - redistest: - image: redis:6 - ports: - - "127.0.0.1:56312:6379" - - dbtest: - image: postgres:13 - ports: - - "127.0.0.1:54312:5432" - environment: - POSTGRES_DB: "test-misskey" - POSTGRES_HOST_AUTH_METHOD: trust diff --git a/packages/backend/test/endpoints.ts b/packages/backend/test/endpoints.mjs similarity index 99% rename from packages/backend/test/endpoints.ts rename to packages/backend/test/endpoints.mjs index 2aedc25f2..40bb91ed7 100644 --- a/packages/backend/test/endpoints.ts +++ b/packages/backend/test/endpoints.mjs @@ -3,13 +3,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; +import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.mjs'; describe('API: Endpoints', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; + let p; + + let alice, bob, carol; before(async () => { p = await startServer(); diff --git a/packages/backend/test/extract-mentions.ts b/packages/backend/test/extract-mentions.mjs similarity index 81% rename from packages/backend/test/extract-mentions.ts rename to packages/backend/test/extract-mentions.mjs index 85afb098d..2512ac868 100644 --- a/packages/backend/test/extract-mentions.ts +++ b/packages/backend/test/extract-mentions.mjs @@ -1,11 +1,11 @@ import * as assert from 'assert'; import { parse } from 'mfm-js'; -import { extractMentions } from '../src/misc/extract-mentions.js'; +import { extractMentions } from '../built/misc/extract-mentions.js'; describe('Extract mentions', () => { it('simple', () => { - const ast = parse('@foo @bar @baz')!; + const ast = parse('@foo @bar @baz'); const mentions = extractMentions(ast); assert.deepStrictEqual(mentions, [{ username: 'foo', @@ -23,7 +23,7 @@ describe('Extract mentions', () => { }); it('nested', () => { - const ast = parse('@foo **@bar** @baz')!; + const ast = parse('@foo **@bar** @baz'); const mentions = extractMentions(ast); assert.deepStrictEqual(mentions, [{ username: 'foo', diff --git a/packages/backend/test/fetch-resource.ts b/packages/backend/test/fetch-resource.mjs similarity index 98% rename from packages/backend/test/fetch-resource.ts rename to packages/backend/test/fetch-resource.mjs index ba1bf54a9..ad135bcec 100644 --- a/packages/backend/test/fetch-resource.ts +++ b/packages/backend/test/fetch-resource.mjs @@ -3,7 +3,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; import * as openapi from '@redocly/openapi-core'; -import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js'; +import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.mjs'; // Request Accept const ONLY_AP = 'application/activity+json'; @@ -19,10 +19,9 @@ const HTML = 'text/html; charset=utf-8'; describe('Fetch resource', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; - let alice: any; - let alicesPost: any; + let alice, alicesPost; before(async () => { p = await startServer(); diff --git a/packages/backend/test/ff-visibility.ts b/packages/backend/test/ff-visibility.mjs similarity index 96% rename from packages/backend/test/ff-visibility.ts rename to packages/backend/test/ff-visibility.mjs index d71464a39..96126b072 100644 --- a/packages/backend/test/ff-visibility.ts +++ b/packages/backend/test/ff-visibility.mjs @@ -2,16 +2,14 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils.js'; +import { async, signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from './utils.mjs'; describe('FF visibility', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; - let alice: any; - let bob: any; - let follower: any; + let alice, bob, follower; before(async () => { p = await startServer(); diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.mjs similarity index 87% rename from packages/backend/test/get-file-info.ts rename to packages/backend/test/get-file-info.mjs index 7ce98db50..8aa6dd6f0 100644 --- a/packages/backend/test/get-file-info.ts +++ b/packages/backend/test/get-file-info.mjs @@ -1,8 +1,8 @@ import * as assert from 'assert'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; -import { getFileInfo } from '../src/misc/get-file-info.js'; -import { async } from './utils.js'; +import { getFileInfo } from '../built/misc/get-file-info.js'; +import { async } from './utils.mjs'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -10,7 +10,7 @@ const _dirname = dirname(_filename); describe('Get file info', () => { it('Empty file', async (async () => { const path = `${_dirname}/resources/emptyfile`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -28,7 +28,7 @@ describe('Get file info', () => { it('Generic JPEG', async (async () => { const path = `${_dirname}/resources/Lenna.jpg`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -46,7 +46,7 @@ describe('Get file info', () => { it('Generic APNG', async (async () => { const path = `${_dirname}/resources/anime.png`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -64,7 +64,7 @@ describe('Get file info', () => { it('Generic AGIF', async (async () => { const path = `${_dirname}/resources/anime.gif`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -82,7 +82,7 @@ describe('Get file info', () => { it('PNG with alpha', async (async () => { const path = `${_dirname}/resources/with-alpha.png`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -100,7 +100,7 @@ describe('Get file info', () => { it('Generic SVG', async (async () => { const path = `${_dirname}/resources/image.svg`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -119,7 +119,7 @@ describe('Get file info', () => { it('SVG with XML definition', async (async () => { // https://github.com/misskey-dev/misskey/issues/4413 const path = `${_dirname}/resources/with-xml-def.svg`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -137,7 +137,7 @@ describe('Get file info', () => { it('Dimension limit', async (async () => { const path = `${_dirname}/resources/25000x25000.png`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { @@ -155,7 +155,7 @@ describe('Get file info', () => { it('Rotate JPEG', async (async () => { const path = `${_dirname}/resources/rotate.jpg`; - const info = await getFileInfo(path) as any; + const info = await getFileInfo(path); delete info.warnings; delete info.blurhash; assert.deepStrictEqual(info, { diff --git a/packages/backend/test/loader.js b/packages/backend/test/loader.js deleted file mode 100644 index 6b21587e3..000000000 --- a/packages/backend/test/loader.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * ts-node/esmローダーに投げる前にpath mappingを解決する - * 参考 - * - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115 - * - https://nodejs.org/api/esm.html#loaders - * ※ https://github.com/TypeStrong/ts-node/pull/1585 が取り込まれたらこのカスタムローダーは必要なくなる - */ - -import { resolve as resolveTs, load } from 'ts-node/esm'; -import { loadConfig, createMatchPath } from 'tsconfig-paths'; -import { pathToFileURL } from 'url'; - -const tsconfig = loadConfig(); -const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths); - -export function resolve(specifier, ctx, defaultResolve) { - let resolvedSpecifier; - if (specifier.endsWith('.js')) { - // maybe transpiled - const specifierWithoutExtension = specifier.substring(0, specifier.length - '.js'.length); - const matchedSpecifier = matchPath(specifierWithoutExtension); - if (matchedSpecifier) { - resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href; - } - } else { - const matchedSpecifier = matchPath(specifier); - if (matchedSpecifier) { - resolvedSpecifier = pathToFileURL(matchedSpecifier).href; - } - } - return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve); -} - -export { load }; diff --git a/packages/backend/test/mfm.ts b/packages/backend/test/mfm.mjs similarity index 96% rename from packages/backend/test/mfm.ts rename to packages/backend/test/mfm.mjs index 5b9e47414..8ccf30911 100644 --- a/packages/backend/test/mfm.ts +++ b/packages/backend/test/mfm.mjs @@ -1,7 +1,7 @@ import * as assert from 'assert'; -import { toHtml } from '../src/mfm/to-html.js'; -import { fromHtml } from '../src/mfm/from-html.js'; +import { toHtml } from '../built/mfm/to-html.js'; +import { fromHtml } from '../built/mfm/from-html.js'; describe('toHtml', () => { it('br', async () => { diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.mjs similarity index 50% rename from packages/backend/test/misc/mock-resolver.ts rename to packages/backend/test/misc/mock-resolver.mjs index 75b80e98c..d74bc36a7 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.mjs @@ -1,21 +1,16 @@ -import { Resolver } from '../../src/remote/activitypub/resolver.js'; -import { IObject } from '../../src/remote/activitypub/type.js'; - -type MockResponse = { - type: string; - content: string; -}; +import { Resolver } from '../../built/remote/activitypub/resolver.js'; export class MockResolver extends Resolver { - private _rs = new Map(); - public async _register(uri: string, content: string | Record, type = 'application/activity+json') { + _rs = new Map(); + + async _register(uri, content, type = 'application/activity+json') { this._rs.set(uri, { type, content: typeof content === 'string' ? content : JSON.stringify(content), }); } - public async resolve(value: string | IObject): Promise { + async resolve(value) { if (typeof value !== 'string') return value; const r = this._rs.get(value); diff --git a/packages/backend/test/mute.ts b/packages/backend/test/mute.mjs similarity index 78% rename from packages/backend/test/mute.ts rename to packages/backend/test/mute.mjs index 178018eea..539f7dc3a 100644 --- a/packages/backend/test/mute.ts +++ b/packages/backend/test/mute.mjs @@ -2,17 +2,15 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.js'; +import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.mjs'; describe('Mute', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; // alice mutes carol - let alice: any; - let bob: any; - let carol: any; + let alice, bob, carol; before(async () => { p = await startServer(); @@ -41,8 +39,8 @@ describe('Mute', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false); })); it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => { @@ -86,9 +84,9 @@ describe('Mute', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false); })); it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => { @@ -102,9 +100,9 @@ describe('Mute', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some((note) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note) => note.id === carolNote.id), false); })); }); @@ -118,8 +116,8 @@ describe('Mute', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some((notification) => notification.userId === bob.id), true); + assert.strictEqual(res.body.some((notification) => notification.userId === carol.id), false); })); }); }); diff --git a/packages/backend/test/note.ts b/packages/backend/test/note.mjs similarity index 98% rename from packages/backend/test/note.ts rename to packages/backend/test/note.mjs index 0f10f5ff9..f9a66c465 100644 --- a/packages/backend/test/note.ts +++ b/packages/backend/test/note.mjs @@ -2,17 +2,16 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { Note } from '../src/models/entities/note.js'; -import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.js'; +import { Note } from '../built/models/entities/note.js'; +import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.mjs'; describe('Note', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; - let Notes: any; + let p; + let Notes; - let alice: any; - let bob: any; + let alice, bob; before(async () => { p = await startServer(); diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.mjs similarity index 82% rename from packages/backend/test/prelude/url.ts rename to packages/backend/test/prelude/url.mjs index df102c8df..66879ce30 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.mjs @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { query } from '../../src/prelude/url.js'; +import { query } from '../../built/prelude/url.js'; describe('url', () => { it('query', () => { diff --git a/packages/backend/test/reaction-lib.ts b/packages/backend/test/reaction-lib.ts deleted file mode 100644 index 7c61dc76c..000000000 --- a/packages/backend/test/reaction-lib.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* -import * as assert from 'assert'; - -import { toDbReaction } from '../src/misc/reaction-lib.js'; - -describe('toDbReaction', async () => { - it('既存の文字列リアクションはそのまま', async () => { - assert.strictEqual(await toDbReaction('like'), 'like'); - }); - - it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => { - assert.strictEqual(await toDbReaction('🍮'), '🍮'); - }); - - it('プリン以外の既存のリアクションは文字列化する like', async () => { - assert.strictEqual(await toDbReaction('👍'), 'like'); - }); - - it('プリン以外の既存のリアクションは文字列化する love', async () => { - assert.strictEqual(await toDbReaction('❤️'), 'love'); - }); - - it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => { - assert.strictEqual(await toDbReaction('❤'), 'love'); - }); - - it('プリン以外の既存のリアクションは文字列化する laugh', async () => { - assert.strictEqual(await toDbReaction('😆'), 'laugh'); - }); - - it('プリン以外の既存のリアクションは文字列化する hmm', async () => { - assert.strictEqual(await toDbReaction('🤔'), 'hmm'); - }); - - it('プリン以外の既存のリアクションは文字列化する surprise', async () => { - assert.strictEqual(await toDbReaction('😮'), 'surprise'); - }); - - it('プリン以外の既存のリアクションは文字列化する congrats', async () => { - assert.strictEqual(await toDbReaction('🎉'), 'congrats'); - }); - - it('プリン以外の既存のリアクションは文字列化する angry', async () => { - assert.strictEqual(await toDbReaction('💢'), 'angry'); - }); - - it('プリン以外の既存のリアクションは文字列化する confused', async () => { - assert.strictEqual(await toDbReaction('😥'), 'confused'); - }); - - it('プリン以外の既存のリアクションは文字列化する rip', async () => { - assert.strictEqual(await toDbReaction('😇'), 'rip'); - }); - - it('それ以外はUnicodeのまま', async () => { - assert.strictEqual(await toDbReaction('🍅'), '🍅'); - }); - - it('異体字セレクタ除去', async () => { - assert.strictEqual(await toDbReaction('㊗️'), '㊗'); - }); - - it('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await toDbReaction('㊗'), '㊗'); - }); - - it('fallback - undefined', async () => { - assert.strictEqual(await toDbReaction(undefined), 'like'); - }); - - it('fallback - null', async () => { - assert.strictEqual(await toDbReaction(null), 'like'); - }); - - it('fallback - empty', async () => { - assert.strictEqual(await toDbReaction(''), 'like'); - }); - - it('fallback - unknown', async () => { - assert.strictEqual(await toDbReaction('unknown'), 'like'); - }); -}); -*/ diff --git a/packages/backend/test/services/blocking.ts b/packages/backend/test/services/blocking.mjs similarity index 67% rename from packages/backend/test/services/blocking.ts rename to packages/backend/test/services/blocking.mjs index 122e8126e..f0e39772b 100644 --- a/packages/backend/test/services/blocking.ts +++ b/packages/backend/test/services/blocking.mjs @@ -3,17 +3,15 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; import * as sinon from 'sinon'; -import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.js'; +import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.mjs'; describe('Creating a block activity', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; // alice blocks bob - let alice: any; - let bob: any; - let carol: any; + let alice, bob, carol; before(async () => { await initTestDb(); @@ -34,10 +32,10 @@ describe('Creating a block activity', function() { }); it('Should federate blocks normally', async(async () => { - const createBlock = (await import('../../src/services/blocking/create')).default; - const deleteBlock = (await import('../../src/services/blocking/delete')).default; + const createBlock = (await import('../../built/services/blocking/create')).default; + const deleteBlock = (await import('../../built/services/blocking/delete')).default; - const queues = await import('../../src/queue/index'); + const queues = await import('../../built/queue/index'); const spy = sinon.spy(queues, 'deliver'); await createBlock(alice, bob); assert(spy.calledOnce); @@ -46,12 +44,12 @@ describe('Creating a block activity', function() { })); it('Should not federate blocks if federateBlocks is false', async () => { - const createBlock = (await import('../../src/services/blocking/create')).default; - const deleteBlock = (await import('../../src/services/blocking/delete')).default; + const createBlock = (await import('../../built/services/blocking/create')).default; + const deleteBlock = (await import('../../built/services/blocking/delete')).default; alice.federateBlocks = true; - const queues = await import('../../src/queue/index'); + const queues = await import('../../built/queue/index'); const spy = sinon.spy(queues, 'deliver'); await createBlock(alice, carol); await deleteBlock(alice, carol); diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.mjs similarity index 96% rename from packages/backend/test/streaming.ts rename to packages/backend/test/streaming.mjs index ad326703d..830dd91d7 100644 --- a/packages/backend/test/streaming.ts +++ b/packages/backend/test/streaming.mjs @@ -2,14 +2,14 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { Following } from '../src/models/entities/following.js'; -import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.js'; +import { Following } from '../built/models/entities/following.js'; +import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.mjs'; describe('Streaming', () => { - let p: childProcess.ChildProcess; - let Followings: any; + let p; + let Followings; - const follow = async (follower: any, followee: any) => { + const follow = async (follower, followee) => { await Followings.save({ id: 'a', createdAt: new Date(), @@ -28,16 +28,12 @@ describe('Streaming', () => { this.timeout(20*60*1000); // Local users - let ayano: any; - let kyoko: any; - let chitose: any; + let ayano, kyoko, chitose; // Remote users - let akari: any; - let chinatsu: any; + let akari, chinatsu; - let kyokoNote: any; - let list: any; + let kyokoNote, list; before(async () => { p = await startServer(); @@ -388,7 +384,7 @@ describe('Streaming', () => { }); describe('Hashtag Timeline', () => { - it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { + it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { if (type == 'note') { assert.deepStrictEqual(body.text, '#foo'); @@ -406,7 +402,7 @@ describe('Streaming', () => { }); })); - it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { + it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { let fooCount = 0; let barCount = 0; let fooBarCount = 0; @@ -444,7 +440,7 @@ describe('Streaming', () => { }, 3000); })); - it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { + it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { let fooCount = 0; let barCount = 0; let fooBarCount = 0; @@ -490,7 +486,7 @@ describe('Streaming', () => { }, 3000); })); - it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { + it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { let fooCount = 0; let barCount = 0; let fooBarCount = 0; diff --git a/packages/backend/test/thread-mute.ts b/packages/backend/test/thread-mute.mjs similarity index 84% rename from packages/backend/test/thread-mute.ts rename to packages/backend/test/thread-mute.mjs index d4ca80af6..aa2b4679d 100644 --- a/packages/backend/test/thread-mute.ts +++ b/packages/backend/test/thread-mute.mjs @@ -2,16 +2,14 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.js'; +import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.mjs'; describe('Note thread mute', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; - let alice: any; - let bob: any; - let carol: any; + let alice, bob, carol; before(async () => { p = await startServer(); @@ -37,9 +35,9 @@ describe('Note thread mute', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some((note) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note) => note.id === carolReply.id), false); + assert.strictEqual(res.body.some((note) => note.id === carolReplyWithoutMention.id), false); })); it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => { @@ -97,8 +95,8 @@ describe('Note thread mute', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some((notification) => notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some((notification) => notification.note.id === carolReplyWithoutMention.id), false); // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい })); diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json deleted file mode 100644 index 3f9020d46..000000000 --- a/packages/backend/test/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": true, - "target": "es2017", - "module": "es2020", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "isolatedModules": true, - "baseUrl": "./", - "paths": { - "@/*": ["../src/*"] - }, - "typeRoots": [ - "../node_modules/@types", - "../src/@types" - ], - "lib": [ - "esnext" - ] - }, - "compileOnSave": false, - "include": [ - "./**/*.ts" - ] -} diff --git a/packages/backend/test/user-notes.ts b/packages/backend/test/user-notes.mjs similarity index 73% rename from packages/backend/test/user-notes.ts rename to packages/backend/test/user-notes.mjs index 9d11d2304..4a0e87c27 100644 --- a/packages/backend/test/user-notes.ts +++ b/packages/backend/test/user-notes.mjs @@ -2,17 +2,14 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import * as childProcess from 'child_process'; -import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.js'; +import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.mjs'; describe('users/notes', function() { this.timeout(20*60*1000); - let p: childProcess.ChildProcess; + let p; - let alice: any; - let jpgNote: any; - let pngNote: any; - let jpgPngNote: any; + let alice, jpgNote, pngNote, jpgPngNote; before(async () => { p = await startServer(); @@ -43,8 +40,8 @@ describe('users/notes', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(res.body.length, 2); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === jpgPngNote.id), true); })); it('ファイルタイプ指定 (jpg or png)', async(async () => { @@ -56,8 +53,8 @@ describe('users/notes', function() { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); assert.strictEqual(res.body.length, 3); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === pngNote.id), true); + assert.strictEqual(res.body.some((note) => note.id === jpgPngNote.id), true); })); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.mjs similarity index 74% rename from packages/backend/test/utils.ts rename to packages/backend/test/utils.mjs index 64a3b8b8b..e0af3ac46 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.mjs @@ -10,8 +10,8 @@ import * as foundkey from 'foundkey-js'; import fetch from 'node-fetch'; import FormData from 'form-data'; import { DataSource } from 'typeorm'; -import { loadConfig } from '../src/config/load.js'; -import { entities } from '../src/db/postgre.js'; +import { loadConfig } from '../built/config/load.js'; +import { entities } from '../built/db/postgre.js'; import got from 'got'; const _filename = fileURLToPath(import.meta.url); @@ -20,20 +20,20 @@ const _dirname = dirname(_filename); const config = loadConfig(); export const port = config.port; -export const async = (fn: Function) => (done: Function) => { +export const async = (fn) => (done) => { fn().then(() => { done(); - }, (err: Error) => { + }, (err) => { done(err); }); }; -export const api = async (endpoint: string, params: any, me?: any) => { +export const api = async (endpoint, params, me) => { endpoint = endpoint.replace(/^\//, ''); const auth = me ? { authorization: `Bearer ${me.token}` } : {}; - const res = await got(`http://localhost:${port}/api/${endpoint}`, { + const res = await got(`http://localhost:${port}/api/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -63,7 +63,7 @@ export const api = async (endpoint: string, params: any, me?: any) => { }; }; -export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => { +export const request = async (endpoint, params, me) => { const auth = me ? { authorization: `Bearer ${me.token}` } : {}; const res = await fetch(`http://localhost:${port}/api${endpoint}`, { @@ -83,7 +83,7 @@ export const request = async (endpoint: string, params: any, me?: any): Promise< }; }; -export const signup = async (params?: any): Promise => { +export const signup = async (params) => { const q = Object.assign({ username: 'test', password: 'test', @@ -94,7 +94,7 @@ export const signup = async (params?: any): Promise => { return res.body; }; -export const post = async (user: any, params?: foundkey.Endpoints['notes/create']['req']): Promise => { +export const post = async (user, params) => { const q = Object.assign({ text: 'test', }, params); @@ -104,7 +104,7 @@ export const post = async (user: any, params?: foundkey.Endpoints['notes/create' return res.body ? res.body.createdNote : null; }; -export const react = async (user: any, note: any, reaction: string): Promise => { +export const react = async (user, note, reaction) => { await api('notes/reactions/create', { noteId: note.id, reaction: reaction, @@ -116,10 +116,10 @@ export const react = async (user: any, note: any, reaction: string): Promise => { +export const uploadFile = async (user, _path) => { const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`; - const formData = new FormData() as any; + const formData = new FormData(); formData.append('i', user.token); formData.append('file', fs.createReadStream(absPath)); formData.append('force', 'true'); @@ -137,8 +137,8 @@ export const uploadFile = async (user: any, _path?: string): Promise => { return body; }; -export const uploadUrl = async (user: any, url: string) => { - let file: any; +export const uploadUrl = async (user, url) => { + let file; const ws = await connectStream(user, 'main', (msg) => { if (msg.type === 'driveFileCreated') { @@ -157,7 +157,7 @@ export const uploadUrl = async (user: any, url: string) => { return file; }; -export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { +export function connectStream(user, channel, listener, params) { return new Promise((res, rej) => { const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`); @@ -184,11 +184,11 @@ export function connectStream(user: any, channel: string, listener: (message: Re }); } -export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { - return new Promise(async (res, rej) => { - let timer: NodeJS.Timeout; +export const waitFire = async (user, channel, trgr, cond, params) => { + return new Promise(async (res, rej) => { + let timer; - let ws: WebSocket; + let ws; try { ws = await connectStream(user, channel, msg => { if (cond(msg)) { @@ -201,7 +201,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond rej(e); } - if (!ws!) return; + if (!ws) return; timer = setTimeout(() => { ws.close(); @@ -218,7 +218,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond }) }; -export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => { +export const simpleGet = async (path, accept = '*/*') => { // node-fetchだと3xxを取れない return await new Promise((resolve, reject) => { const req = http.request(`http://localhost:${port}${path}`, { @@ -226,7 +226,7 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status? Accept: accept, }, }, res => { - if (res.statusCode! >= 400) { + if (res.statusCode >= 400) { reject(res); } else { resolve({ @@ -241,8 +241,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status? }); }; -export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise = async () => {}) { - return (done: (err?: Error) => any) => { +export function launchServer(callbackSpawnedProcess, moreProcess = async () => {}) { + return (done) => { const p = childProcess.spawn('node', [_dirname + '/../index.js'], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env: { NODE_ENV: 'test', PATH: process.env.PATH }, @@ -254,7 +254,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce }; } -export async function initTestDb(justBorrow = false, initEntities?: any[]) { +export async function initTestDb(justBorrow = false, initEntities) { if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; const db = new DataSource({ @@ -274,7 +274,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { return db; } -export function startServer(timeout = 60 * 1000): Promise { +export function startServer(timeout = 60 * 1000) { return new Promise((res, rej) => { const t = setTimeout(() => { p.kill(SIGKILL); @@ -297,7 +297,7 @@ export function startServer(timeout = 60 * 1000): Promise { const t = setTimeout(() => { p.kill(SIGKILL); @@ -313,8 +313,8 @@ export function shutdownServer(p: childProcess.ChildProcess, timeout = 20 * 1000 }); } -export function sleep(msec: number) { - return new Promise(res => { +export function sleep(msec) { + return new Promise(res => { setTimeout(() => { res(); }, msec); diff --git a/packages/client/package.json b/packages/client/package.json index 545c58c9f..791aa0230 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "13.0.0-preview5", + "version": "13.0.0-preview6", "private": true, "scripts": { "watch": "vite build --watch --mode development", diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index 7f6d90080..a023c19c2 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -6,7 +6,7 @@

- +

diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue index eb0c5f34b..f04d632f6 100644 --- a/packages/client/src/components/global/misskey-flavored-markdown.vue +++ b/packages/client/src/components/global/misskey-flavored-markdown.vue @@ -175,6 +175,9 @@ withDefaults(defineProps<{ diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue index 73fd48384..1c8e435d2 100644 --- a/packages/client/src/components/note-preview.vue +++ b/packages/client/src/components/note-preview.vue @@ -7,7 +7,7 @@
- +
diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue index 2b876b4c2..eb16b96c5 100644 --- a/packages/client/src/components/note-simple.vue +++ b/packages/client/src/components/note-simple.vue @@ -5,7 +5,7 @@

- +

diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 8324f083a..f87c17ea3 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -37,19 +37,19 @@

- +

- + RN:
{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: - +
diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue index 078197e9b..99c13a52c 100644 --- a/packages/client/src/components/sub-note-content.vue +++ b/packages/client/src/components/sub-note-content.vue @@ -3,7 +3,7 @@
({{ i18n.ts.deleted }}) - + RN: ...
diff --git a/packages/client/src/components/user-info.vue b/packages/client/src/components/user-info.vue index a885f6696..8f5da858d 100644 --- a/packages/client/src/components/user-info.vue +++ b/packages/client/src/components/user-info.vue @@ -8,7 +8,7 @@
- +
{{ i18n.ts.noAccountDescription }}
diff --git a/packages/client/src/components/user-preview.vue b/packages/client/src/components/user-preview.vue index 3cf7c49e5..9245c752d 100644 --- a/packages/client/src/components/user-preview.vue +++ b/packages/client/src/components/user-preview.vue @@ -11,7 +11,7 @@

- +
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue index fa706434b..12697d1af 100644 --- a/packages/client/src/pages/channel.vue +++ b/packages/client/src/pages/channel.vue @@ -19,7 +19,7 @@
- +
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue index 1abe072e3..8300c4d87 100644 --- a/packages/client/src/pages/clip.vue +++ b/packages/client/src/pages/clip.vue @@ -5,7 +5,7 @@
- +
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index 95ee40552..d4a766cf0 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -17,7 +17,7 @@

@{{ acct(req.follower) }}

- +
diff --git a/packages/client/src/pages/messaging/messaging-room.message.vue b/packages/client/src/pages/messaging/messaging-room.message.vue index 85f919208..583bc1aad 100644 --- a/packages/client/src/pages/messaging/messaging-room.message.vue +++ b/packages/client/src/pages/messaging/messaging-room.message.vue @@ -7,7 +7,7 @@ Delete
- +

{{ i18n.ts.noAccountDescription }}

@@ -74,7 +74,7 @@
- +
diff --git a/packages/client/src/pages/welcome.timeline.vue b/packages/client/src/pages/welcome.timeline.vue index 00de07d31..c9fe60c5a 100644 --- a/packages/client/src/pages/welcome.timeline.vue +++ b/packages/client/src/pages/welcome.timeline.vue @@ -5,7 +5,7 @@
- + RN: ...
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index a267a3634..42c89bed9 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -243,7 +243,7 @@ export class ColdDeviceStorage { plugins: [] as Plugin[], mediaVolume: 0.5, sound_masterVolume: 0.3, - sound_note: { type: 'syuilo/down', volume: 1 }, + sound_note: { type: 'syuilo/down', volume: 0 }, sound_noteMy: { type: 'syuilo/up', volume: 1 }, sound_notification: { type: 'syuilo/pope2', volume: 1 }, sound_chat: { type: 'syuilo/pope1', volume: 1 }, diff --git a/packages/foundkey-js/package.json b/packages/foundkey-js/package.json index b01ff78e5..60151848c 100644 --- a/packages/foundkey-js/package.json +++ b/packages/foundkey-js/package.json @@ -1,6 +1,6 @@ { "name": "foundkey-js", - "version": "13.0.0-preview5", + "version": "13.0.0-preview6", "description": "Fork of misskey-js for Foundkey", "type": "module", "main": "./built/index.js", diff --git a/packages/foundkey-js/src/consts.ts b/packages/foundkey-js/src/consts.ts index 1c941d3d5..62301e12d 100644 --- a/packages/foundkey-js/src/consts.ts +++ b/packages/foundkey-js/src/consts.ts @@ -1,6 +1,6 @@ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app'] as const; +export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app'] as const; -export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded'] as const; +export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update'] as const; export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; diff --git a/packages/foundkey-js/src/entities.ts b/packages/foundkey-js/src/entities.ts index 173f0f28f..39ff8d773 100644 --- a/packages/foundkey-js/src/entities.ts +++ b/packages/foundkey-js/src/entities.ts @@ -33,7 +33,6 @@ export type UserLite = { export type UserDetailed = UserLite & { bannerBlurhash: string | null; - bannerColor: string | null; bannerUrl: string | null; birthday: string | null; createdAt: DateString; @@ -130,6 +129,7 @@ export type DriveFolder = TODO; export type Note = { id: ID; createdAt: DateString; + updatedAt: DateString | null; text: string | null; cw: string | null; user: User; @@ -207,6 +207,11 @@ export type Notification = { user: User; userId: User['id']; note: Note; +} | { + type: 'update'; + user: User; + userId: User['id']; + note: Note; } | { type: 'follow'; user: User; diff --git a/packages/foundkey-js/src/streaming.types.ts b/packages/foundkey-js/src/streaming.types.ts index 84c948538..942b1975c 100644 --- a/packages/foundkey-js/src/streaming.types.ts +++ b/packages/foundkey-js/src/streaming.types.ts @@ -142,6 +142,12 @@ export type NoteUpdatedEvent = { choice: number; userId: User['id']; }; +} | { + id: Note['id']; + type: 'updated'; + body: { + note: Note; + }; }; export type BroadcastEvents = { diff --git a/packages/sw/package.json b/packages/sw/package.json index c4023cb30..025fc2d45 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -1,6 +1,6 @@ { "name": "sw", - "version": "13.0.0-preview5", + "version": "13.0.0-preview6", "private": true, "scripts": { "watch": "node build.js watch", diff --git a/yarn.lock b/yarn.lock index e4e95a074..0de579795 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3717,8 +3717,8 @@ __metadata: cli-highlight: 2.1.11 color-convert: 2.0.1 content-disposition: 0.5.4 - cross-env: 7.0.3 date-fns: 2.28.0 + decompress: 4.2.1 deep-email-validator: 0.1.21 escape-regexp: 0.0.1 eslint: ^8.29.0 @@ -3794,7 +3794,6 @@ __metadata: twemoji-parser: 14.0.0 typeorm: 0.3.7 typescript: ^4.9.4 - unzipper: 0.10.11 uuid: 8.3.2 web-push: 3.5.0 ws: 8.8.0 @@ -3861,13 +3860,6 @@ __metadata: languageName: node linkType: hard -"big-integer@npm:^1.6.17": - version: 1.6.51 - resolution: "big-integer@npm:1.6.51" - checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518 - languageName: node - linkType: hard - "big.js@npm:^5.2.2": version: 5.2.2 resolution: "big.js@npm:5.2.2" @@ -3882,16 +3874,6 @@ __metadata: languageName: node linkType: hard -"binary@npm:~0.3.0": - version: 0.3.0 - resolution: "binary@npm:0.3.0" - dependencies: - buffers: ~0.1.1 - chainsaw: ~0.1.0 - checksum: b4699fda9e2c2981e74a46b0115cf0d472eda9b68c0e9d229ef494e92f29ce81acf0a834415094cffcc340dfee7c4ef8ce5d048c65c18067a7ed850323f777af - languageName: node - linkType: hard - "binaryextensions@npm:^2.2.0": version: 2.3.0 resolution: "binaryextensions@npm:2.3.0" @@ -3899,6 +3881,16 @@ __metadata: languageName: node linkType: hard +"bl@npm:^1.0.0": + version: 1.2.3 + resolution: "bl@npm:1.2.3" + dependencies: + readable-stream: ^2.3.5 + safe-buffer: ^5.1.1 + checksum: 123f097989ce2fa9087ce761cd41176aaaec864e28f7dfe5c7dab8ae16d66d9844f849c3ad688eb357e3c5e4f49b573e3c0780bb8bc937206735a3b6f8569a5f + languageName: node + linkType: hard + "bl@npm:^4.0.3": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -3924,13 +3916,6 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:~3.4.1": - version: 3.4.7 - resolution: "bluebird@npm:3.4.7" - checksum: bffa9dee7d3a41ab15c4f3f24687b49959b4e64e55c058a062176feb8ccefc2163414fb4e1a0f3053bf187600936509660c3ebd168fd9f0e48c7eba23b019466 - languageName: node - linkType: hard - "blurhash@npm:1.1.5": version: 1.1.5 resolution: "blurhash@npm:1.1.5" @@ -4078,6 +4063,23 @@ __metadata: languageName: node linkType: hard +"buffer-alloc-unsafe@npm:^1.1.0": + version: 1.1.0 + resolution: "buffer-alloc-unsafe@npm:1.1.0" + checksum: c5e18bf51f67754ec843c9af3d4c005051aac5008a3992938dda1344e5cfec77c4b02b4ca303644d1e9a6e281765155ce6356d85c6f5ccc5cd21afc868def396 + languageName: node + linkType: hard + +"buffer-alloc@npm:^1.2.0": + version: 1.2.0 + resolution: "buffer-alloc@npm:1.2.0" + dependencies: + buffer-alloc-unsafe: ^1.1.0 + buffer-fill: ^1.0.0 + checksum: 560cd27f3cbe73c614867da373407d4506309c62fe18de45a1ce191f3785ec6ca2488d802ff82065798542422980ca25f903db078c57822218182c37c3576df5 + languageName: node + linkType: hard + "buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -4099,6 +4101,13 @@ __metadata: languageName: node linkType: hard +"buffer-fill@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-fill@npm:1.0.0" + checksum: c29b4723ddeab01e74b5d3b982a0c6828f2ded49cef049ddca3dac661c874ecdbcecb5dd8380cf0f4adbeb8cff90a7de724126750a1f1e5ebd4eb6c59a1315b1 + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.1 resolution: "buffer-from@npm:1.1.1" @@ -4106,13 +4115,6 @@ __metadata: languageName: node linkType: hard -"buffer-indexof-polyfill@npm:~1.0.0": - version: 1.0.2 - resolution: "buffer-indexof-polyfill@npm:1.0.2" - checksum: fbfb2d69c6bb2df235683126f9dc140150c08ac3630da149913a9971947b667df816a913b6993bc48f4d611999cb99a1589914d34c02dccd2234afda5cb75bbc - languageName: node - linkType: hard - "buffer-writer@npm:2.0.0": version: 2.0.0 resolution: "buffer-writer@npm:2.0.0" @@ -4131,7 +4133,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -4151,13 +4153,6 @@ __metadata: languageName: node linkType: hard -"buffers@npm:~0.1.1": - version: 0.1.1 - resolution: "buffers@npm:0.1.1" - checksum: ad6f8e483efab39cefd92bdc04edbff6805e4211b002f4d1cfb70c6c472a61cc89fb18c37bcdfdd4ee416ca096e9ff606286698a7d41a18b539bac12fd76d4d5 - languageName: node - linkType: hard - "bull@npm:4.8.4": version: 4.8.4 resolution: "bull@npm:4.8.4" @@ -4385,15 +4380,6 @@ __metadata: languageName: node linkType: hard -"chainsaw@npm:~0.1.0": - version: 0.1.0 - resolution: "chainsaw@npm:0.1.0" - dependencies: - traverse: ">=0.3.0 <0.4" - checksum: 22a96b9fb0cd9fb20813607c0869e61817d1acc81b5d455cc6456b5e460ea1dd52630e0f76b291cf8294bfb6c1fc42e299afb52104af9096242699d6d3aa6d3e - languageName: node - linkType: hard - "chalk-template@npm:0.4.0": version: 0.4.0 resolution: "chalk-template@npm:0.4.0" @@ -5042,7 +5028,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3": +"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3, commander@npm:^2.8.1": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e @@ -5684,6 +5670,69 @@ __metadata: languageName: node linkType: hard +"decompress-tar@npm:^4.0.0, decompress-tar@npm:^4.1.0, decompress-tar@npm:^4.1.1": + version: 4.1.1 + resolution: "decompress-tar@npm:4.1.1" + dependencies: + file-type: ^5.2.0 + is-stream: ^1.1.0 + tar-stream: ^1.5.2 + checksum: 42d5360b558a28dd884e1bf809e3fea92b9910fda5151add004d4a64cc76ac124e8b3e9117e805f2349af9e49c331d873e6fc5ad86a00e575703fee632b0a225 + languageName: node + linkType: hard + +"decompress-tarbz2@npm:^4.0.0": + version: 4.1.1 + resolution: "decompress-tarbz2@npm:4.1.1" + dependencies: + decompress-tar: ^4.1.0 + file-type: ^6.1.0 + is-stream: ^1.1.0 + seek-bzip: ^1.0.5 + unbzip2-stream: ^1.0.9 + checksum: 519c81337730159a1f2d7072a6ee8523ffd76df48d34f14c27cb0a27f89b4e2acf75dad2f761838e5bc63230cea1ac154b092ecb7504be4e93f7d0e32ddd6aff + languageName: node + linkType: hard + +"decompress-targz@npm:^4.0.0": + version: 4.1.1 + resolution: "decompress-targz@npm:4.1.1" + dependencies: + decompress-tar: ^4.1.1 + file-type: ^5.2.0 + is-stream: ^1.1.0 + checksum: 22738f58eb034568dc50d370c03b346c428bfe8292fe56165847376b5af17d3c028fefca82db642d79cb094df4c0a599d40a8f294b02aad1d3ddec82f3fd45d4 + languageName: node + linkType: hard + +"decompress-unzip@npm:^4.0.1": + version: 4.0.1 + resolution: "decompress-unzip@npm:4.0.1" + dependencies: + file-type: ^3.8.0 + get-stream: ^2.2.0 + pify: ^2.3.0 + yauzl: ^2.4.2 + checksum: ba9f3204ab2415bedb18d796244928a18148ef40dbb15174d0d01e5991b39536b03d02800a8a389515a1523f8fb13efc7cd44697df758cd06c674879caefd62b + languageName: node + linkType: hard + +"decompress@npm:4.2.1": + version: 4.2.1 + resolution: "decompress@npm:4.2.1" + dependencies: + decompress-tar: ^4.0.0 + decompress-tarbz2: ^4.0.0 + decompress-targz: ^4.0.0 + decompress-unzip: ^4.0.1 + graceful-fs: ^4.1.10 + make-dir: ^1.0.0 + pify: ^2.3.0 + strip-dirs: ^2.0.0 + checksum: 8247a31c6db7178413715fdfb35a482f019c81dfcd6e8e623d9f0382c9889ce797ce0144de016b256ed03298907a620ce81387cca0e69067a933470081436cb8 + languageName: node + linkType: hard + "dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" @@ -6080,15 +6129,6 @@ __metadata: languageName: node linkType: hard -"duplexer2@npm:~0.1.4": - version: 0.1.4 - resolution: "duplexer2@npm:0.1.4" - dependencies: - readable-stream: ^2.0.2 - checksum: 744961f03c7f54313f90555ac20284a3fb7bf22fdff6538f041a86c22499560eb6eac9d30ab5768054137cb40e6b18b40f621094e0261d7d8c35a37b7a5ad241 - languageName: node - linkType: hard - "duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -7531,6 +7571,27 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^3.8.0": + version: 3.9.0 + resolution: "file-type@npm:3.9.0" + checksum: 1db70b2485ac77c4edb4b8753c1874ee6194123533f43c2651820f96b518f505fa570b093fedd6672eb105ba9fb89c62f84b6492e46788e39c3447aed37afa2d + languageName: node + linkType: hard + +"file-type@npm:^5.2.0": + version: 5.2.0 + resolution: "file-type@npm:5.2.0" + checksum: b2b21c7fc3cfb3c6a3a18b0d5d7233b74d8c17d82757655766573951daf42962a5c809e5fc3637675b237c558ebc67e4958fb2cc5a4ad407bc545aaa40001c74 + languageName: node + linkType: hard + +"file-type@npm:^6.1.0": + version: 6.2.0 + resolution: "file-type@npm:6.2.0" + checksum: 749540cefcd4959121eb83e373ed84e49b2e5a510aa5d598b725bd772dd306ae41fd00d3162ae3f6563b4db5cfafbbd0df321de3f20c17e20a8c56431ae55e58 + languageName: node + linkType: hard + "filelist@npm:^1.0.1": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -7954,18 +8015,6 @@ __metadata: languageName: node linkType: hard -"fstream@npm:^1.0.12": - version: 1.0.12 - resolution: "fstream@npm:1.0.12" - dependencies: - graceful-fs: ^4.1.2 - inherits: ~2.0.0 - mkdirp: ">=0.5 0" - rimraf: 2 - checksum: e6998651aeb85fd0f0a8a68cec4d05a3ada685ecc4e3f56e0d063d0564a4fc39ad11a856f9020f926daf869fc67f7a90e891def5d48e4cadab875dc313094536 - languageName: node - linkType: hard - "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -8087,6 +8136,16 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^2.2.0": + version: 2.3.1 + resolution: "get-stream@npm:2.3.1" + dependencies: + object-assign: ^4.0.1 + pinkie-promise: ^2.0.0 + checksum: d82c86556e131ba7bef00233aa0aa7a51230e6deac11a971ce0f47cd43e2a5e968a3e3914cd082f07cd0d69425653b2f96735b0a7d5c5c03fef3ab857a531367 + languageName: node + linkType: hard + "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -8395,6 +8454,13 @@ __metadata: languageName: node linkType: hard +"graceful-fs@npm:^4.1.10": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + "graceful-fs@npm:^4.2.0": version: 4.2.8 resolution: "graceful-fs@npm:4.2.8" @@ -8402,7 +8468,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da @@ -9063,7 +9129,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.0, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -9494,6 +9560,13 @@ __metadata: languageName: node linkType: hard +"is-natural-number@npm:^4.0.1": + version: 4.0.1 + resolution: "is-natural-number@npm:4.0.1" + checksum: 3e5e3d52e0dfa4fea923b5d2b8a5cdbd9bf110c4598d30304b98528b02f40c9058a2abf1bae10bcbaf2bac18ace41cff7bc9673aff339f8c8297fae74ae0e75d + languageName: node + linkType: hard + "is-negated-glob@npm:^1.0.0": version: 1.0.0 resolution: "is-negated-glob@npm:1.0.0" @@ -9619,6 +9692,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^1.1.0": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.0 resolution: "is-stream@npm:2.0.0" @@ -11183,13 +11263,6 @@ __metadata: languageName: node linkType: hard -"listenercount@npm:~1.0.1": - version: 1.0.1 - resolution: "listenercount@npm:1.0.1" - checksum: 0f1c9077cdaf2ebc16473c7d72eb7de6d983898ca42500f03da63c3914b6b312dd5f7a90d2657691ea25adf3fe0ac5a43226e8b2c673fd73415ed038041f4757 - languageName: node - linkType: hard - "listr2@npm:^3.8.3": version: 3.11.0 resolution: "listr2@npm:3.11.0" @@ -11502,6 +11575,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^1.0.0": + version: 1.3.0 + resolution: "make-dir@npm:1.3.0" + dependencies: + pify: ^3.0.0 + checksum: c564f6e7bb5ace1c02ad56b3a5f5e07d074af0c0b693c55c7b2c2b148882827c8c2afc7b57e43338a9f90c125b58d604e8cf3e6990a48bf949dfea8c79668c0b + languageName: node + linkType: hard + "make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -11947,7 +12029,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:>=0.5 0, mkdirp@npm:^0.5.4": +"mkdirp@npm:^0.5.4": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" dependencies: @@ -13316,13 +13398,20 @@ __metadata: languageName: node linkType: hard -"pify@npm:^2.0.0, pify@npm:^2.2.0": +"pify@npm:^2.0.0, pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" checksum: 9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba languageName: node linkType: hard +"pify@npm:^3.0.0": + version: 3.0.0 + resolution: "pify@npm:3.0.0" + checksum: 6cdcbc3567d5c412450c53261a3f10991665d660961e06605decf4544a61a97a54fefe70a68d5c37080ff9d6f4cf51444c90198d1ba9f9309a6c0d6e9f5c4fde + languageName: node + linkType: hard + "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -14426,6 +14515,21 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^2.3.0": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.3 + isarray: ~1.0.0 + process-nextick-args: ~2.0.0 + safe-buffer: ~5.1.1 + string_decoder: ~1.1.1 + util-deprecate: ~1.0.1 + checksum: 65645467038704f0c8aaf026a72fbb588a9e2ef7a75cd57a01702ee9db1c4a1e4b03aaad36861a6a0926546a74d174149c8c207527963e0c2d3eee2f37678a42 + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" @@ -14922,17 +15026,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:2": - version: 2.7.1 - resolution: "rimraf@npm:2.7.1" - dependencies: - glob: ^7.1.3 - bin: - rimraf: ./bin.js - checksum: cdc7f6eacb17927f2a075117a823e1c5951792c6498ebcce81ca8203454a811d4cf8900314154d3259bb8f0b42ab17f67396a8694a54cae3283326e57ad250cd - languageName: node - linkType: hard - "rimraf@npm:3.0.2, rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -15002,7 +15095,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.1.1": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -15123,6 +15216,18 @@ __metadata: languageName: node linkType: hard +"seek-bzip@npm:^1.0.5": + version: 1.0.6 + resolution: "seek-bzip@npm:1.0.6" + dependencies: + commander: ^2.8.1 + bin: + seek-bunzip: bin/seek-bunzip + seek-table: bin/seek-bzip-table + checksum: c2ab3291e7085558499efd4e99d1466ee6782f6c4a4e4c417aa859e1cd2f5117fb3b5444f3d27c38ec5908c0f0312e2a0bc69dff087751f97b3921b5bde4f9ed + languageName: node + linkType: hard + "semver-greatest-satisfied-range@npm:^1.1.0": version: 1.1.0 resolution: "semver-greatest-satisfied-range@npm:1.1.0" @@ -15211,7 +15316,7 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.5, setimmediate@npm:~1.0.4": +"setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd @@ -15935,6 +16040,15 @@ __metadata: languageName: node linkType: hard +"strip-dirs@npm:^2.0.0": + version: 2.1.0 + resolution: "strip-dirs@npm:2.1.0" + dependencies: + is-natural-number: ^4.0.1 + checksum: 9465547d71d8819daa7a5c9d4d783289ed8eac72eb06bd687bed382ce62af8ab8e6ffbda229805f5d2e71acce2ca4915e781c94190d284994cbc0b7cdc8303cc + languageName: node + linkType: hard + "strip-final-newline@npm:^2.0.0": version: 2.0.0 resolution: "strip-final-newline@npm:2.0.0" @@ -16175,6 +16289,21 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^1.5.2": + version: 1.6.2 + resolution: "tar-stream@npm:1.6.2" + dependencies: + bl: ^1.0.0 + buffer-alloc: ^1.2.0 + end-of-stream: ^1.0.0 + fs-constants: ^1.0.0 + readable-stream: ^2.3.0 + to-buffer: ^1.1.1 + xtend: ^4.0.0 + checksum: a5d49e232d3e33321bbd150381b6a4e5046bf12b1c2618acb95435b7871efde4d98bd1891eb2200478a7142ef7e304e033eb29bbcbc90451a2cdfa1890e05245 + languageName: node + linkType: hard + "tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" @@ -16372,6 +16501,13 @@ __metadata: languageName: node linkType: hard +"to-buffer@npm:^1.1.1": + version: 1.1.1 + resolution: "to-buffer@npm:1.1.1" + checksum: 6c897f58c2bdd8b8b1645ea515297732fec6dafb089bf36d12370c102ff5d64abf2be9410e0b1b7cfc707bada22d9a4084558010bfc78dd7023748dc5dd9a1ce + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -16506,13 +16642,6 @@ __metadata: languageName: node linkType: hard -"traverse@npm:>=0.3.0 <0.4": - version: 0.3.9 - resolution: "traverse@npm:0.3.9" - checksum: 982982e4e249e9bbf063732a41fe5595939892758524bbef5d547c67cdf371b13af72b5434c6a61d88d4bb4351d6dabc6e22d832e0d16bc1bc684ef97a1cc59e - languageName: node - linkType: hard - "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -16964,6 +17093,16 @@ __metadata: languageName: node linkType: hard +"unbzip2-stream@npm:^1.0.9": + version: 1.4.3 + resolution: "unbzip2-stream@npm:1.4.3" + dependencies: + buffer: ^5.2.1 + through: ^2.3.8 + checksum: 0e67c4a91f4fa0fc7b4045f8b914d3498c2fc2e8c39c359977708ec85ac6d6029840e97f508675fdbdf21fcb8d276ca502043406f3682b70f075e69aae626d1d + languageName: node + linkType: hard + "unc-path-regex@npm:^0.1.2": version: 0.1.2 resolution: "unc-path-regex@npm:0.1.2" @@ -17111,24 +17250,6 @@ __metadata: languageName: node linkType: hard -"unzipper@npm:0.10.11": - version: 0.10.11 - resolution: "unzipper@npm:0.10.11" - dependencies: - big-integer: ^1.6.17 - binary: ~0.3.0 - bluebird: ~3.4.1 - buffer-indexof-polyfill: ~1.0.0 - duplexer2: ~0.1.4 - fstream: ^1.0.12 - graceful-fs: ^4.2.2 - listenercount: ~1.0.1 - readable-stream: ~2.3.6 - setimmediate: ~1.0.4 - checksum: 006cd43ec4d6df47d86aa6b15044a606f50cdcd6a3d6f96f64f54ca0b663c09abb221f76edca0e9592511036d37ea094b1d76ce92c5bf10d7c6eb56f0be678f8 - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.0.5": version: 1.0.5 resolution: "update-browserslist-db@npm:1.0.5" @@ -18066,7 +18187,7 @@ __metadata: languageName: node linkType: hard -"yauzl@npm:^2.10.0": +"yauzl@npm:^2.10.0, yauzl@npm:^2.4.2": version: 2.10.0 resolution: "yauzl@npm:2.10.0" dependencies: