diff --git a/.config/example.yml b/.config/example.yml index 4146881b1..8d4a162cb 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -108,11 +108,6 @@ redis: #deliverJobMaxAttempts: 12 #inboxJobMaxAttempts: 8 -# Syslog option -#syslog: -# host: localhost -# port: 514 - # Proxy for HTTP/HTTPS outgoing connections #proxy: http://127.0.0.1:3128 diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index e93473fe8..f4f2805c2 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -8,15 +8,15 @@ clone: pipeline: install: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn install build: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn build diff --git a/.woodpecker/lint-backend.yml b/.woodpecker/lint-backend.yml index 5690d99ae..096989cb7 100644 --- a/.woodpecker/lint-backend.yml +++ b/.woodpecker/lint-backend.yml @@ -8,15 +8,15 @@ clone: pipeline: install: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn install lint: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn workspace backend run lint diff --git a/.woodpecker/lint-client.yml b/.woodpecker/lint-client.yml index feb910081..6b60ac448 100644 --- a/.woodpecker/lint-client.yml +++ b/.woodpecker/lint-client.yml @@ -8,15 +8,15 @@ clone: pipeline: install: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn install lint: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn workspace client run lint diff --git a/.woodpecker/lint-foundkey-js.yml b/.woodpecker/lint-foundkey-js.yml index ce8162209..ddff1b46e 100644 --- a/.woodpecker/lint-foundkey-js.yml +++ b/.woodpecker/lint-foundkey-js.yml @@ -8,15 +8,15 @@ clone: pipeline: install: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn install lint: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn workspace foundkey-js run lint diff --git a/.woodpecker/lint-sw.yml b/.woodpecker/lint-sw.yml index 4a20add2e..aad1f4ad4 100644 --- a/.woodpecker/lint-sw.yml +++ b/.woodpecker/lint-sw.yml @@ -8,15 +8,15 @@ clone: pipeline: install: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn install lint: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn workspace sw run lint diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 30eb04ee6..e42d68c73 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -5,11 +5,14 @@ clone: depth: 1 # CI does not need commit history recursive: true +depends_on: + - build + pipeline: build: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn install @@ -18,15 +21,15 @@ pipeline: - yarn build mocha: when: - event: - - pull_request + branch: main + event: push image: node:18.6.0 commands: - yarn mocha e2e: when: - event: - - pull_request + branch: main + event: push image: cypress/included:10.3.0 commands: - npm run start:test & diff --git a/docs/emoji.md b/docs/emoji.md index 257cbc39d..abfd13632 100644 --- a/docs/emoji.md +++ b/docs/emoji.md @@ -18,12 +18,11 @@ Please note that Emoji may be subject to copyright and you are responsible for c If you have an image file that you would like to turn into a custom emoji you can import the image as an emoji. This works just like attaching files to a note: -You can choose to upload a new file, pick a file from your Misskey drive or upload a file from another URL. +You can choose to upload a new file, pick a file from your Foundkey drive or upload a file from another URL. -::: danger +**Warning:** When you import emoji from your drive, the file will remain inside your drive. -Misskey does not make a copy of this file so if you delete it, the emoji will be broken. -::: +Foundkey does not make a copy of this file so if you delete it, the emoji will be broken. The emoji will be added to the instance and you will then be able to edit or delete it as usual. @@ -32,10 +31,9 @@ The emoji will be added to the instance and you will then be able to edit or del Emojis can be imported in bulk as packed ZIP files with a special format. This ability can be found in the three dots menu in the top right corner of the custom emoji menu. -::: warning +**Warning:** Bulk emoji import may overwrite existing emoji or otherwise mess up your instance. Be sure to only import emoji from trusted sources, ideally only ones you exported yourself. -::: ### Packed emoji format @@ -89,10 +87,9 @@ The properties of an emoji can be edited by clicking it in the list of local emo When you click on a custom emoji, a dialog for editing the properties will open. This dialog will also allow you to delete an emoji. -::: danger +**Warning:** When you delete a custom emoji, old notes that contain it will still have the text name of the emoji in it. The emoji will no longer be rendered correctly. -::: Note that remote emoji can not be edited or deleted. diff --git a/docs/migrating.md b/docs/migrating.md index 6308f667b..a44742508 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -20,7 +20,7 @@ cd packages/backend LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n nsfwDetection1655368940105 | cut -d ':' -f 1)" NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)" -for i in $(seq 1 $NUM_MIGRAIONS); do +for i in $(seq 1 $NUM_MIGRATIONS); do npx typeorm migration:revert -d ormconfig.js done ``` diff --git a/locales/en-US.yml b/locales/en-US.yml index 024f93770..59936d440 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -844,6 +844,7 @@ _ffVisibility: public: "Public" followers: "Visible to followers only" private: "Private" + nobody: "Nobody (not even you)" _signup: almostThere: "Almost there" emailAddressInfo: "Please enter your email address. It will not be made public." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 803f90418..fbfcd9352 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -854,6 +854,18 @@ _mfm: spin: Animación (Spin) shakeDescription: Brinda al contenido una animación temblorosa. inlineMath: Función matemática (Inline) + rainbow: Arcoíris + x4Description: Muestra el contenido de la manera más grandemente posible. + blurDescription: Muestra borroso el contenido. Se mostrará con claridad cuando se + cubra. + spinDescription: Da al contenido una animación de girar. + x2: Grande + x2Description: Muestra en grande el contenido. + x3Description: Muestra más grande el contenido. + x4: Increíblemente grande + blur: Borroso + fontDescription: Agrega la fuente para mostrar contenido. + x3: Muy grande _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" @@ -1318,3 +1330,9 @@ unlikeConfirm: ¿En verdad quieres remover tu like? breakFollow: Quitar seguidor reporter: Reportero continueThread: Ver la continuación del hilo +uploadFailedSize: El archivo es muy grande para subirse. +uploadFailed: Subida fallida +uploadFailedDescription: No se pudo subir el archivo. +movedTo: Este usuario se ha movido a {handle}. +attachedToNotes: Notas del archivo +showAttachedNotes: Mostrar notas del archivo diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 69a2ad1af..a729d0a3a 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -842,8 +842,8 @@ _ffVisibility: private: "비공개" _signup: almostThere: "거의 다 끝났습니다" - emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다." - emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요." + emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다." + emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요." _accountDelete: accountDelete: "계정 삭제" mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다." diff --git a/packages/backend/migration/1684536337602-ffVisibilityNobody.js b/packages/backend/migration/1684536337602-ffVisibilityNobody.js new file mode 100644 index 000000000..8998e7d24 --- /dev/null +++ b/packages/backend/migration/1684536337602-ffVisibilityNobody.js @@ -0,0 +1,21 @@ +export class ffVisibilityNobody1684536337602 { + name = 'ffVisibilityNobody1684536337602'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TYPE "public"."user_profile_ffvisibility_enum" RENAME TO "user_profile_ffvisibility_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private', 'nobody')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" TYPE "public"."user_profile_ffvisibility_enum" USING "ffVisibility"::"text"::"public"."user_profile_ffvisibility_enum"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" SET DEFAULT 'public'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum_old"`); + } + + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum_old" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" TYPE "public"."user_profile_ffvisibility_enum_old" USING "ffVisibility"::"text"::"public"."user_profile_ffvisibility_enum_old"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "ffVisibility" SET DEFAULT 'public'`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); + await queryRunner.query(`ALTER TYPE "public"."user_profile_ffvisibility_enum_old" RENAME TO "user_profile_ffvisibility_enum"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 66afa14c3..d9c830baf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -69,7 +69,7 @@ "koa-views": "7.0.2", "mfm-js": "0.22.1", "mime-types": "2.1.35", - "mocha": "10.0.0", + "mocha": "10.2.0", "multer": "1.4.5-lts.1", "nested-property": "4.0.0", "node-fetch": "3.2.6", @@ -100,7 +100,6 @@ "stringz": "2.1.0", "style-loader": "3.3.1", "summaly": "2.7.0", - "syslog-pro": "1.0.0", "systeminformation": "5.11.22", "tinycolor2": "1.4.2", "tmp": "0.2.1", @@ -158,7 +157,6 @@ "@types/sinon": "^10.0.13", "@types/sinonjs__fake-timers": "8.1.2", "@types/speakeasy": "2.0.7", - "@types/syslog-pro": "^1.0.0", "@types/tinycolor2": "1.4.3", "@types/tmp": "0.2.3", "@types/uuid": "8.3.4", diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 4e5327641..7a27deb9a 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -8,7 +8,7 @@ import chalkTemplate from 'chalk-template'; import semver from 'semver'; import Logger from '@/services/logger.js'; -import loadConfig from '@/config/load.js'; +import { loadConfig } from '@/config/load.js'; import { Config } from '@/config/types.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; import { envOption } from '@/env.js'; @@ -41,7 +41,7 @@ function greet(): void { } bootLogger.info('Welcome to FoundKey!'); - bootLogger.info(`FoundKey v${meta.version}`, null, true); + bootLogger.info(`FoundKey v${meta.version}`, true); } /** @@ -59,7 +59,7 @@ export async function masterMain(): Promise { config = loadConfigBoot(); await connectDb(); } catch (e) { - bootLogger.error('Fatal error occurred during initialization', {}, true); + bootLogger.error('Fatal error occurred during initialization', true); process.exit(1); } @@ -69,7 +69,7 @@ export async function masterMain(): Promise { await spawnWorkers(config.clusterLimits); } - bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); + bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true); if (!envOption.noDaemons) { import('../daemons/server-stats.js').then(x => x.serverStats()); @@ -84,7 +84,7 @@ function showEnvironment(): void { if (env !== 'production') { logger.warn('The environment is not in production mode.'); - logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', {}, true); + logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', true); } } @@ -109,7 +109,7 @@ function loadConfigBoot(): Config { } catch (exception) { const e = exception as Partial | Error; if ('code' in e && e.code === 'ENOENT') { - configLogger.error('Configuration file not found', {}, true); + configLogger.error('Configuration file not found', true); process.exit(1); } else if (e instanceof Error) { configLogger.error(e.message); @@ -133,7 +133,7 @@ async function connectDb(): Promise { const v = await db.query('SHOW server_version').then(x => x[0].server_version); dbLogger.succ(`Connected: v${v}`); } catch (e) { - dbLogger.error('Cannot connect', {}, true); + dbLogger.error('Cannot connect', true); dbLogger.error(e as Error | string); process.exit(1); } @@ -160,12 +160,24 @@ function spawnWorker(mode: 'web' | 'queue'): Promise { return new Promise(res => { const worker = cluster.fork({ mode }); worker.on('message', message => { - if (message === 'listenFailed') { - bootLogger.error('The server Listen failed due to the previous error.'); - process.exit(1); + switch (message) { + case 'listenFailed': + bootLogger.error('The server Listen failed due to the previous error.'); + process.exit(1); + break; + case 'ready': + res(); + break; + case 'metaUpdate': + // forward new instance metadata to all workers + for (const otherWorker of Object.values(cluster.workers)) { + // don't forward the message to the worker that sent it + if (worker.id === otherWorker.id) continue; + + otherWorker.send(message); + } + break; } - if (message !== 'ready') return; - res(); }); }); } diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts index 3e53b0003..6b407f269 100644 --- a/packages/backend/src/config/index.ts +++ b/packages/backend/src/config/index.ts @@ -1,3 +1,3 @@ -import load from './load.js'; +import { loadConfig } from './load.js'; -export default load(); +export default loadConfig(); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index a6431162d..f16fb850b 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -23,7 +23,7 @@ const path = process.env.NODE_ENV === 'test' ? `${dir}/test.yml` : `${dir}/default.yml`; -export default function load(): Config { +export function loadConfig(): Config { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); let config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 55226ca47..686a8c242 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -59,11 +59,6 @@ export type Source = { deliverJobMaxAttempts?: number; inboxJobMaxAttempts?: number; - syslog?: { - host: string; - port: number; - }; - mediaProxy?: string; proxyRemoteFiles?: boolean; internalStoragePath?: string; diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts index ab8c81eef..da71b1d4b 100644 --- a/packages/backend/src/misc/fetch-meta.ts +++ b/packages/backend/src/misc/fetch-meta.ts @@ -1,3 +1,4 @@ +import process from 'node:process'; import push from 'web-push'; import { db } from '@/db/postgre.js'; import { Meta } from '@/models/entities/meta.js'; @@ -17,9 +18,20 @@ export async function setMeta(meta: Meta): Promise { cache = meta; + /* + The meta is not included here because another process may have updated + the content before the other process receives it. + */ + process.send!('metaUpdated'); + unlock(); } +// the primary will forward this message +process.on('message', async message => { + if (message === 'metaUpdated') await getMeta(); +}); + /** * Performs the primitive database operation to fetch server configuration. * If there is no entry yet, inserts a new one. diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 6cfca187a..0c1e8092f 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -260,9 +260,3 @@ export interface IRemoteUser extends User { host: string; token: null; } - -export type CacheableLocalUser = ILocalUser; - -export type CacheableRemoteUser = IRemoteUser; - -export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 10969a034..aea591229 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -230,6 +230,36 @@ export const UserRepository = db.getRepository(User).extend({ return `${config.url}/identicon/${userId}`; }, + /** + * Determines whether the followers/following of user `user` are visibile to user `me`. + */ + async areFollowersVisibleTo(user: User, me: { id: User['id'] } | null | undefined): Promise { + const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + + switch (profile.ffVisibility) { + case 'public': + return true; + case 'followers': + if (me == null) { + return false; + } else if (me.id === user.id) { + return true; + } else { + return await Followings.count({ + where: { + followerId: me.id, + followeeId: user.id, + }, + take: 1, + }).then(n => n > 0); + } + case 'private': + return me?.id === user.id; + case 'nobody': + return false; + } + } + async pack( src: User['id'] | User, me?: { id: User['id'] } | null | undefined, @@ -270,15 +300,13 @@ export const UserRepository = db.getRepository(User).extend({ .getMany() : []; const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null; - const followingCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followingCount : - (profile.ffVisibility === 'followers') && relation?.isFollowing ? user.followingCount : - null; + const ffVisible = await this.areFollowersVisibleTo(user, me); - const followersCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followersCount : - (profile.ffVisibility === 'followers') && relation?.isFollowing ? user.followersCount : - null; + const followingCount = opts.detail ? null : + ffVisible ? user.followingCount : null; + + const followersCount = opts.detail ? null : + ffVisible ? user.followersCount : null; const packed = { id: user.id, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 6a3a02d8e..2046e2fd0 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -40,8 +40,8 @@ systemQueue .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`)) + .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`)) .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); deliverQueue @@ -49,31 +49,31 @@ deliverQueue .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`)) .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); inboxQueue .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)) + .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`)) .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); dbQueue .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`)) + .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`)) .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); objectStorageQueue .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`)) + .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`)) .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); webhookDeliverQueue @@ -81,7 +81,7 @@ webhookDeliverQueue .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`)) .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); export async function deliver(content: IActivity|IActivity[], to: string | null) { diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts index f31531db4..9aa503e43 100644 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts @@ -71,7 +71,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi try { await downloadUrl(emoji.originalUrl, emojiPath); downloaded = true; - } catch (e) { // TODO: 何度か再試行 + } catch (e) { // TODO: retry logger.error(e instanceof Error ? e : new Error(e as string)); } diff --git a/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts index 9c04ecb6d..9125660f3 100644 --- a/packages/backend/src/remote/activitypub/audience.ts +++ b/packages/backend/src/remote/activitypub/audience.ts @@ -1,5 +1,5 @@ import promiseLimit from 'promise-limit'; -import { CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js'; +import { IRemoteUser, User } from '@/models/entities/user.js'; import { unique, concat } from '@/prelude/array.js'; import { resolvePerson } from './models/person.js'; import { Resolver } from './resolver.js'; @@ -9,20 +9,20 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified'; type AudienceInfo = { visibility: Visibility, - mentionedUsers: CacheableUser[], - visibleUsers: CacheableUser[], + mentionedUsers: User[], + visibleUsers: User[], }; -export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { +export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise { const toGroups = groupingAudience(getApIds(to), actor); const ccGroups = groupingAudience(getApIds(cc), actor); const others = unique(concat([toGroups.other, ccGroups.other])); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); if (toGroups.public.length > 0) { return { @@ -55,7 +55,7 @@ export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, c }; } -function groupingAudience(ids: string[], actor: CacheableRemoteUser) { +function groupingAudience(ids: string[], actor: IRemoteUser) { const groups = { public: [] as string[], followers: [] as string[], @@ -85,7 +85,7 @@ function isPublic(id: string) { ].includes(id); } -function isFollowers(id: string, actor: CacheableRemoteUser) { +function isFollowers(id: string, actor: IRemoteUser) { return ( id === (actor.followersUri || `${actor.uri}/followers`) ); diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index dc1b47fef..4684b8931 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -1,7 +1,7 @@ import escapeRegexp from 'escape-regexp'; import config from '@/config/index.js'; import { Note } from '@/models/entities/note.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { Notes, MessagingMessages } from '@/models/index.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; @@ -89,7 +89,7 @@ export class DbResolver { /** * AP Person => FoundKey User in DB */ - public async getUserFromApId(value: string | IObject): Promise { + public async getUserFromApId(value: string | IObject): Promise { const parsed = parseUri(value); if (parsed.local) { diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts index 037f660c6..05c621e6d 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts @@ -1,11 +1,11 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { acceptFollowRequest } from '@/services/following/requests/accept.js'; import { relayAccepted } from '@/services/relay.js'; import { IFollow } from '@/remote/activitypub/type.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + // activity is a follow request started by this server, so activity.actor must be an existing local user. const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.actor); diff --git a/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts index be9b80096..1a61011e6 100644 --- a/packages/backend/src/remote/activitypub/kernel/accept/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/accept/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js'; import acceptFollow from './follow.js'; -export default async (actor: CacheableRemoteUser, activity: IAccept, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IAccept, resolver: Resolver): Promise => { const uri = activity.id || activity; apLogger.info(`Accept: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts index 3fd5f4723..3d685dae4 100644 --- a/packages/backend/src/remote/activitypub/kernel/add/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/add/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { addPinned } from '@/services/i/pin.js'; import { resolveNote } from '@/remote/activitypub/models/note.js'; import { IAdd } from '@/remote/activitypub/type.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IAdd, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IAdd, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts index e4d77e1c5..25bc8c73c 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import announceNote from './note.js'; -export default async (actor: CacheableRemoteUser, activity: IAnnounce, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IAnnounce, resolver: Resolver): Promise => { const uri = getApId(activity); apLogger.info(`Announce: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts index 254ef2727..5c6196588 100644 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts @@ -1,5 +1,5 @@ import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { getApLock } from '@/misc/app-lock.js'; import { StatusError } from '@/misc/fetch.js'; @@ -11,7 +11,7 @@ import { Resolver } from '@/remote/activitypub/resolver.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js'; -export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise { const uri = getApId(activity); if (actor.isSuspended) { diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts index 7095a36a5..08b7ee517 100644 --- a/packages/backend/src/remote/activitypub/kernel/block/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/block/index.ts @@ -1,11 +1,11 @@ import block from '@/services/blocking/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { IBlock } from '@/remote/activitypub/type.js'; -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { - // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず +export default async (actor: IRemoteUser, activity: IBlock): Promise => { + // There is a block target in activity.object, which should be a local user that exists. const dbResolver = new DbResolver(); const blockee = await dbResolver.getUserFromApId(activity.object); @@ -15,7 +15,7 @@ export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { +export default async (actor: IRemoteUser, activity: ICreate, resolver: Resolver): Promise => { const uri = getApId(activity); apLogger.info(`Create: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts index 892dbb26a..6fc7b6c2d 100644 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { getApLock } from '@/misc/app-lock.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { StatusError } from '@/misc/fetch.js'; @@ -9,7 +9,7 @@ import { getApId, IObject } from '@/remote/activitypub/type.js'; /** * 投稿作成アクティビティを捌きます */ -export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false): Promise { +export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false): Promise { const uri = getApId(note); if (typeof note === 'object') { diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts index ea75a9739..9513ea22f 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts @@ -1,9 +1,9 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { deleteAccount } from '@/services/delete-account.js'; -export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise { +export async function deleteActor(actor: IRemoteUser, uri: string): Promise { apLogger.info(`Deleting the Actor: ${uri}`); if (actor.uri !== uri) { diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts index ee05e5327..9e05c9e48 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { toSingle } from '@/prelude/array.js'; import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '@/remote/activitypub/type.js'; import { deleteActor } from './actor.js'; @@ -7,7 +7,7 @@ import deleteNote from './note.js'; /** * 削除アクティビティを捌きます */ -export default async (actor: CacheableRemoteUser, activity: IDelete): Promise => { +export default async (actor: IRemoteUser, activity: IDelete): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts index 9f9a5cea6..d855f7f92 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts @@ -1,11 +1,11 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { deleteNotes } from '@/services/note/delete.js'; import { getApLock } from '@/misc/app-lock.js'; import { deleteMessage } from '@/services/messages/delete.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { apLogger } from '@/remote/activitypub/logger.js'; -export default async function(actor: CacheableRemoteUser, uri: string): Promise { +export default async function(actor: IRemoteUser, uri: string): Promise { apLogger.info(`Deleting the Note: ${uri}`); const unlock = await getApLock(uri); diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts index e50bcc2bd..cadb7436d 100644 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts @@ -1,13 +1,14 @@ import { In } from 'typeorm'; import config from '@/config/index.js'; import { genId } from '@/misc/gen-id.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { AbuseUserReports, Users } from '@/models/index.js'; import { IFlag, getApIds } from '@/remote/activitypub/type.js'; -export default async (actor: CacheableRemoteUser, activity: IFlag): Promise => { - // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので - // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する +export default async (actor: IRemoteUser, activity: IFlag): Promise => { + // The object is `(User|Note) | (User|Note)[]`, but since the database schema + // cannot be made to handle every possible case, the target user is the first user + // and everything else is stored by URL. const uris = getApIds(activity.object); const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!); diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts index 8125b4606..99dbb369c 100644 --- a/packages/backend/src/remote/activitypub/kernel/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/follow.ts @@ -1,9 +1,9 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import follow from '@/services/following/create.js'; import { IFollow } from '../type.js'; import { DbResolver } from '../db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { +export default async (actor: IRemoteUser, activity: IFollow): Promise => { const dbResolver = new DbResolver(); const followee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts index 46a972a7e..2a0918a4d 100644 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +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'; @@ -21,7 +21,7 @@ import block from './block/index.js'; import flag from './flag/index.js'; import { move } from './move/index.js'; -export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { +export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise { if (isCollectionOrOrderedCollection(activity)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { const act = await resolver.resolve(item); @@ -38,7 +38,7 @@ export async function performActivity(actor: CacheableRemoteUser, activity: IObj } } -async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { +async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise { if (actor.isSuspended) return; if (typeof activity.id !== 'undefined') { diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts index 9650312b3..51270cc80 100644 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/like.ts @@ -1,9 +1,9 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { createReaction } from '@/services/note/reaction/create.js'; import { ILike, getApId } from '../type.js'; import { fetchNote, extractEmojis } from '../models/note.js'; -export default async (actor: CacheableRemoteUser, activity: ILike) => { +export default async (actor: IRemoteUser, activity: ILike) => { const targetUri = getApId(activity.object); const note = await fetchNote(targetUri); diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts index e64656e09..8f233d869 100644 --- a/packages/backend/src/remote/activitypub/kernel/move/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/move/index.ts @@ -1,12 +1,12 @@ import { IsNull } from 'typeorm'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { resolvePerson } from '@/remote/activitypub/models/person.js'; import { Followings, Users } from '@/models/index.js'; import { createNotification } from '@/services/create-notification.js'; import Resolver from '../../resolver.js'; import { IMove, isActor, getApId } from '../../type.js'; -export async function move(actor: CacheableRemoteUser, activity: IMove, resolver: Resolver): Promise { +export async function move(actor: IRemoteUser, activity: IMove, resolver: Resolver): Promise { // actor is not move origin if (activity.object == null || getApId(activity.object) !== actor.uri) return; diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts index d367fb669..cb147f2af 100644 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ b/packages/backend/src/remote/activitypub/kernel/read.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { isSelfHost, extractDbHost } 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'; -export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise => { +export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise => { const id = await getApId(activity.object); if (!isSelfHost(extractDbHost(id))) { diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts index bd3ad1660..2606b8a5e 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts @@ -1,12 +1,12 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { remoteReject } from '@/services/following/reject.js'; import { relayRejected } from '@/services/relay.js'; import { Users } from '@/models/index.js'; import { IFollow } from '../../type.js'; import { DbResolver } from '../../db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { - // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある +export default async (actor: IRemoteUser, activity: IFollow): Promise => { + // activity is a follow request started by this server, so activity.actor must be an existing local user. const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.actor); diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts index 3a91c8ec7..3eb748f6b 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { apLogger } from '../../logger.js'; import { IReject, isFollow, getApType } from '../../type.js'; import rejectFollow from './follow.js'; -export default async (actor: CacheableRemoteUser, activity: IReject, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IReject, resolver: Resolver): Promise => { const uri = activity.id || activity; apLogger.info(`Reject: ${uri}`); diff --git a/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts index 6591f82b1..3f7ad494e 100644 --- a/packages/backend/src/remote/activitypub/kernel/remove/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/remove/index.ts @@ -1,10 +1,10 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { removePinned } from '@/services/i/pin.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IRemove } from '../../type.js'; import { resolveNote } from '../../models/note.js'; -export default async (actor: CacheableRemoteUser, activity: IRemove, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IRemove, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts index fa4eea44c..b14cec889 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts @@ -1,10 +1,10 @@ import unfollow from '@/services/following/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Followings } from '@/models/index.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { IAccept } from '@/remote/activitypub/type.js'; -export default async (actor: CacheableRemoteUser, activity: IAccept): Promise => { +export default async (actor: IRemoteUser, activity: IAccept): Promise => { const dbResolver = new DbResolver(); const follower = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts index 78981b542..05e0edbb6 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts @@ -1,9 +1,9 @@ import { Notes } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { deleteNotes } from '@/services/note/delete.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; -export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise => { +export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise => { const uri = getApId(activity); const note = await Notes.findOneBy({ diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts index ae1c9c0b6..f4e0513fb 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/block.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/block.ts @@ -1,10 +1,10 @@ import unblock from '@/services/blocking/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { IBlock } from '@/remote/activitypub/type.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { +export default async (actor: IRemoteUser, activity: IBlock): Promise => { const dbResolver = new DbResolver(); const blockee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts index c7f99bcf2..172ee8460 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts @@ -1,11 +1,11 @@ import unfollow from '@/services/following/delete.js'; import { cancelFollowRequest } from '@/services/following/requests/cancel.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { FollowRequests, Followings } from '@/models/index.js'; import { IFollow } from '@/remote/activitypub/type.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -export default async (actor: CacheableRemoteUser, activity: IFollow): Promise => { +export default async (actor: IRemoteUser, activity: IFollow): Promise => { const dbResolver = new DbResolver(); const followee = await dbResolver.getUserFromApId(activity.object); diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts index 05382f0f5..139711129 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '@/remote/activitypub/type.js'; @@ -8,7 +8,7 @@ import undoLike from './like.js'; import undoAccept from './accept.js'; import { undoAnnounce } from './announce.js'; -export default async (actor: CacheableRemoteUser, activity: IUndo, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IUndo, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { throw new Error('invalid actor'); } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts index 6c7b8d18b..717c8aa2a 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/like.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { deleteReaction } from '@/services/note/reaction/delete.js'; import { ILike, getApId } from '@/remote/activitypub/type.js'; import { fetchNote } from '@/remote/activitypub/models/note.js'; @@ -6,7 +6,7 @@ import { fetchNote } from '@/remote/activitypub/models/note.js'; /** * Process Undo.Like activity */ -export default async (actor: CacheableRemoteUser, activity: ILike) => { +export default async (actor: IRemoteUser, activity: ILike) => { const targetUri = getApId(activity.object); const note = await fetchNote(targetUri); diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts index 73085b181..d34965db2 100644 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts @@ -1,4 +1,4 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js'; import { apLogger } from '@/remote/activitypub/logger.js'; import { updateQuestion } from '@/remote/activitypub/models/question.js'; @@ -8,7 +8,7 @@ import { updatePerson } from '@/remote/activitypub/models/person.js'; /** * Updateアクティビティを捌きます */ -export default async (actor: CacheableRemoteUser, activity: IUpdate, resolver: Resolver): Promise => { +export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver): Promise => { if ('actor' in activity && actor.uri !== activity.actor) { return 'skip: invalid actor'; } diff --git a/packages/backend/src/remote/activitypub/misc/auth-user.ts b/packages/backend/src/remote/activitypub/misc/auth-user.ts index 4705bb791..e140d2fa1 100644 --- a/packages/backend/src/remote/activitypub/misc/auth-user.ts +++ b/packages/backend/src/remote/activitypub/misc/auth-user.ts @@ -1,12 +1,12 @@ import { Cache } from '@/misc/cache.js'; import { UserPublickeys } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +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'; export type AuthUser = { - user: CacheableRemoteUser; + user: IRemoteUser; key: UserPublickey; }; diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index 281cbdf9a..aaf4b90d7 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -1,5 +1,5 @@ import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles } from '@/models/index.js'; @@ -11,7 +11,7 @@ import { apLogger } from '../logger.js'; /** * Imageを作成します。 */ -export async function createImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise { +export async function createImage(actor: IRemoteUser, value: any, resolver: Resolver): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); @@ -58,7 +58,7 @@ export async function createImage(actor: CacheableRemoteUser, value: any, resolv * If the target Image is registered in FoundKey, return it; otherwise, fetch it from the remote server and return it. * Fetch the image from the remote server, register it in FoundKey and return it. */ -export async function resolveImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise { +export async function resolveImage(actor: IRemoteUser, value: any, resolver: Resolver): Promise { // TODO // Fetch from remote server and register it. diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts index 183ab841a..c42fd197a 100644 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ b/packages/backend/src/remote/activitypub/models/mention.ts @@ -1,17 +1,17 @@ import promiseLimit from 'promise-limit'; import { toArray, unique } from '@/prelude/array.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IObject, isMention, IApMention } from '../type.js'; import { resolvePerson } from './person.js'; -export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { +export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); - const limit = promiseLimit(2); + const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), - )).filter((x): x is CacheableUser => x != null); + )).filter((x): x is User => x != null); return mentionedUsers; } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 30245a67f..3607ad62b 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -2,7 +2,7 @@ import promiseLimit from 'promise-limit'; import config from '@/config/index.js'; import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { 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'; @@ -74,13 +74,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si const err = validateNote(object); if (err) { - apLogger.error(`${err.message}`, { - resolver: { - history: resolver.getHistory(), - }, - value, - object, - }); + apLogger.error(`${err.message}`); throw new Error('invalid note'); } @@ -91,7 +85,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si apLogger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; + const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 17d9858e4..a5e268005 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -6,7 +6,7 @@ import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instanc import { Note } from '@/models/entities/note.js'; import { updateUsertags } from '@/services/update-hashtag.js'; import { Users, Instances, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; -import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js'; +import { User, IRemoteUser, User } from '@/models/entities/user.js'; import { Emoji } from '@/models/entities/emoji.js'; import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { genId } from '@/misc/gen-id.js'; @@ -121,7 +121,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise { * * If the target Person is registered in FoundKey, it is returned. */ -export async function fetchPerson(uri: string): Promise { +export async function fetchPerson(uri: string): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); const cached = uriPersonCache.get(uri); @@ -217,7 +217,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver): } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + // Fix an error when the input is an alias like /users/@a -> /users/:id const u = await Users.findOneBy({ uri: person.id, }); @@ -394,7 +394,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver): * If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it. * Fetch the person from the remote server, register it in FoundKey, and return it. */ -export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise { +export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); //#region このサーバーに既に登録されていたらそれを返す diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts index 8622d43df..23999213c 100644 --- a/packages/backend/src/remote/activitypub/perform.ts +++ b/packages/backend/src/remote/activitypub/perform.ts @@ -1,11 +1,11 @@ import { DAY } from '@/const.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; +import { IRemoteUser } from '@/models/entities/user.js'; import { Resolver } from '@/remote/activitypub/resolver.js'; import { IObject } from './type.js'; import { performActivity } from './kernel/index.js'; import { updatePerson } from './models/person.js'; -export async function perform(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { +export async function perform(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise { await performActivity(actor, activity, resolver); // And while I'm at it, I'll update the remote user information if it's out of date. diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index beb48713a..2c2b6cfb4 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { Following } from '@/models/entities/following.js'; import { setResponseType } from '../activitypub.js'; @@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => { return; } - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } else if (profile.ffVisibility === 'followers') { + const ffVisible = await Users.areFollowersVisibleTo(user, null); + if (!ffVisible) { ctx.status = 403; ctx.set('Cache-Control', 'public, max-age=30'); return; } - //#endregion const limit = 10; const partOf = `${config.url}/users/${userId}/followers`; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index 3a25a6316..4e156a19f 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { Following } from '@/models/entities/following.js'; import { setResponseType } from '../activitypub.js'; @@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => { return; } - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - ctx.status = 403; - ctx.set('Cache-Control', 'public, max-age=30'); - return; - } else if (profile.ffVisibility === 'followers') { + const ffVisible = await Users.areFollowersVisibleTo(user, null); + if (!ffVisible) { ctx.status = 403; ctx.set('Cache-Control', 'public, max-age=30'); return; } - //#endregion const limit = 10; const partOf = `${config.url}/users/${userId}/following`; diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index de9af0890..3bde09602 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -1,7 +1,7 @@ import Koa from 'koa'; import { IEndpoint } from './endpoints.js'; -import authenticate, { AuthenticationError } from './authenticate.js'; +import { authenticate, AuthenticationError } from './authenticate.js'; import call from './call.js'; import { ApiError } from './error.js'; diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts index 25e87b75e..7daa91de4 100644 --- a/packages/backend/src/server/api/authenticate.ts +++ b/packages/backend/src/server/api/authenticate.ts @@ -1,4 +1,4 @@ -import { CacheableLocalUser } from '@/models/entities/user.js'; +import { ILocalUser } from '@/models/entities/user.js'; import { Users, AccessTokens } from '@/models/index.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; @@ -11,7 +11,7 @@ export class AuthenticationError extends Error { } } -export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { +export async function authenticate(authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[ILocalUser | null | undefined, AccessToken | null | undefined]> { let maybeToken: string | null = null; // check if there is an authorization header set @@ -66,4 +66,4 @@ export default async (authorization: string | null | undefined, bodyToken: strin return [user, accessToken]; } -}; +} diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index dc0e790bd..ea8c6086e 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,14 +1,14 @@ import { performance } from 'perf_hooks'; import Koa from 'koa'; -import { CacheableLocalUser } from '@/models/entities/user.js'; +import { ILocalUser } from '@/models/entities/user.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import { limiter } from './limiter.js'; -import endpoints, { IEndpointMeta } from './endpoints.js'; +import { endpoints, IEndpointMeta } from './endpoints.js'; import { ApiError } from './error.js'; import { apiLogger } from './logger.js'; -export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { +export default async (endpoint: string, user: ILocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; const isModerator = user != null && (user.isModerator || user.isAdmin); @@ -82,15 +82,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi if (e instanceof ApiError) { throw e; } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { - ep: ep.name, - ps: data, - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); + apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`); throw new ApiError('INTERNAL_ERROR', { e: { message: e.message, diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts index 243b105ae..811a10ae3 100644 --- a/packages/backend/src/server/api/define.ts +++ b/packages/backend/src/server/api/define.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import Ajv from 'ajv'; -import { CacheableLocalUser } from '@/models/entities/user.js'; +import { ILocalUser } from '@/models/entities/user.js'; import { Schema, SchemaType } from '@/misc/schema.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { IEndpointMeta } from './endpoints.js'; @@ -10,7 +10,7 @@ export type Response = Record | void; // TODO: paramsの型をT['params']のスキーマ定義から推論する type executor = - (params: SchemaType, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => + (params: SchemaType, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => Promise>>; const ajv = new Ajv({ @@ -20,10 +20,10 @@ const ajv = new Ajv({ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); export default function (meta: T, paramDef: Ps, cb: executor) - : (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise { + : (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => Promise { const validate = ajv.compile(paramDef); - return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => { + return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 55763d875..a7757c2b5 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -713,7 +713,7 @@ export interface IEndpoint { params: Schema; } -const endpoints: IEndpoint[] = eps.map(([name, ep]) => { +export const endpoints: IEndpoint[] = eps.map(([name, ep]) => { return { name, exec: ep.default, @@ -721,5 +721,3 @@ const endpoints: IEndpoint[] = eps.map(([name, ep]) => { params: ep.paramDef, }; }); - -export default endpoints; diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 9492cc60a..31b0f1266 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -5,7 +5,7 @@ import { Resolver } from '@/remote/activitypub/resolver.js'; import { extractDbHost } from '@/misc/convert-host.js'; import { Users, Notes } from '@/models/index.js'; import { Note } from '@/models/entities/note.js'; -import { CacheableLocalUser, User } from '@/models/entities/user.js'; +import { ILocalUser, User } from '@/models/entities/user.js'; import { isActor, isPost } from '@/remote/activitypub/type.js'; import { SchemaType } from '@/misc/schema.js'; import { HOUR } from '@/const.js'; @@ -85,7 +85,7 @@ export default define(meta, paramDef, async (ps, me) => { /*** * URIからUserかNoteを解決する */ -async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { +async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise | null> { // Stop if the host is blocked. const host = extractDbHost(uri); if (await shouldBlockInstance(host)) { @@ -122,7 +122,7 @@ async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): ); } -async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { +async function mergePack(me: ILocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise | null> { if (user != null) { return { type: 'User', diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index f380a5287..b5894c10d 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -1,5 +1,5 @@ import define from '@/server/api/define.js'; -import endpoints from '@/server/api/endpoints.js'; +import { endpoints } from '@/server/api/endpoints.js'; export const meta = { requireCredential: false, diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 184f74e79..976109887 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -1,5 +1,5 @@ import define from '@/server/api/define.js'; -import endpoints from '@/server/api/endpoints.js'; +import { endpoints } from '@/server/api/endpoints.js'; export const meta = { requireCredential: false, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c7f7d50c8..1a73e31df 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,6 +1,6 @@ import RE2 from 're2'; import * as mfm from 'mfm-js'; -import { notificationTypes } from 'foundkey-js'; +import { ffVisibility, notificationTypes } from 'foundkey-js'; import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import { acceptAllFollowRequests } from '@/services/following/requests/accept-all.js'; import { publishToFollowers } from '@/services/i/update.js'; @@ -67,7 +67,7 @@ export const paramDef = { injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, - ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + ffVisibility: { type: 'string', enum: ffVisibility }, pinnedPageId: { type: 'array', items: { type: 'string', format: 'misskey:id', } }, diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 2595fbff5..e93851cd8 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -1,5 +1,5 @@ import { IsNull } from 'typeorm'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { toPunyNullable } from '@/misc/convert-host.js'; import define from '@/server/api/define.js'; import { ApiError } from '@/server/api/error.js'; @@ -61,25 +61,8 @@ export default define(meta, paramDef, async (ps, me) => { if (user == null) throw new ApiError('NO_SUCH_USER'); - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError('ACCESS_DENIED'); - } else if (me.id !== user.id) { - const following = await Followings.countBy({ - followeeId: user.id, - followerId: me.id, - }); - if (!following) { - throw new ApiError('ACCESS_DENIED'); - } - } - } + const ffVisible = await Users.areFollowersVisibleTo(user, me); + if (!ffVisible) throw new ApiError('ACCESS_DENIED'); const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) .andWhere('following.followeeId = :userId', { userId: user.id }) diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 0bf60c079..406853423 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -1,5 +1,5 @@ import { IsNull } from 'typeorm'; -import { Users, Followings, UserProfiles } from '@/models/index.js'; +import { Users, Followings } from '@/models/index.js'; import { toPunyNullable } from '@/misc/convert-host.js'; import define from '@/server/api/define.js'; import { ApiError } from '@/server/api/error.js'; @@ -61,25 +61,8 @@ export default define(meta, paramDef, async (ps, me) => { if (user == null) throw new ApiError('NO_SUCH_USER'); - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError('ACCESS_DENIED'); - } else if (me.id !== user.id) { - const following = await Followings.countBy({ - followeeId: user.id, - followerId: me.id, - }); - if (!following) { - throw new ApiError('ACCESS_DENIED'); - } - } - } + const ffVisible = await Users.areFollowersVisibleTo(user, me); + if (!ffVisible) throw new ApiError('ACCESS_DENIED'); const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) .andWhere('following.followerId = :userId', { userId: user.id }) diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index 970308f78..7197219fb 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -46,27 +46,27 @@ export const meta = { }, localFollowingCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, remoteFollowingCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, localFollowersCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, remoteFollowersCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, followingCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, followersCount: { type: 'integer', - optional: false, nullable: false, + optional: true, nullable: false, }, sentReactionsCount: { type: 'integer', @@ -110,7 +110,7 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { const user = await Users.findOneBy({ id: ps.userId }); if (user == null) { throw new ApiError('NO_SUCH_USER'); @@ -141,22 +141,6 @@ export default define(meta, paramDef, async (ps) => { .innerJoin('vote.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - localFollowingCount: Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NULL') - .getCount(), - remoteFollowingCount: Followings.createQueryBuilder('following') - .where('following.followerId = :userId', { userId: user.id }) - .andWhere('following.followeeHost IS NOT NULL') - .getCount(), - localFollowersCount: Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NULL') - .getCount(), - remoteFollowersCount: Followings.createQueryBuilder('following') - .where('following.followeeId = :userId', { userId: user.id }) - .andWhere('following.followerHost IS NOT NULL') - .getCount(), sentReactionsCount: NoteReactions.createQueryBuilder('reaction') .where('reaction.userId = :userId', { userId: user.id }) .getCount(), @@ -180,8 +164,32 @@ export default define(meta, paramDef, async (ps) => { driveUsage: DriveFiles.calcDriveUsageOf(user.id), }); - result.followingCount = result.localFollowingCount + result.remoteFollowingCount; - result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + const ffVisible = await Users.areFollowersVisibleTo(user, me); + if (ffVisible) { + const follows = await awaitAll({ + localFollowingCount: Followings.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NULL') + .getCount(), + remoteFollowingCount: Followings.createQueryBuilder('following') + .where('following.followerId = :userId', { userId: user.id }) + .andWhere('following.followeeHost IS NOT NULL') + .getCount(), + localFollowersCount: Followings.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NULL') + .getCount(), + remoteFollowersCount: Followings.createQueryBuilder('following') + .where('following.followeeId = :userId', { userId: user.id }) + .andWhere('following.followerHost IS NOT NULL') + .getCount(), + }); + + Object.assign(result, follows); + + result.followingCount = result.localFollowingCount + result.remoteFollowingCount; + result.followersCount = result.localFollowersCount + result.remoteFollowersCount; + } return result; }); diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index fe0c4450a..554596e3a 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -10,7 +10,7 @@ import cors from '@koa/cors'; import { Instances, AccessTokens, Users } from '@/models/index.js'; import config from '@/config/index.js'; -import endpoints from './endpoints.js'; +import { endpoints } from './endpoints.js'; import { handler } from './api-handler.js'; import signup from './private/signup.js'; import signin from './private/signin.js'; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 0a7fcf667..9dc1c89cf 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -2,7 +2,7 @@ import config from '@/config/index.js'; import { kinds } from '@/misc/api-permissions.js'; import { I18n } from '@/misc/i18n.js'; import { errors as errorDefinitions } from '@/server/api/error.js'; -import endpoints from '@/server/api/endpoints.js'; +import { endpoints } from '@/server/api/endpoints.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; import { httpCodes } from './http-codes.js'; diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index 797443afb..eefbcc216 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -6,7 +6,7 @@ import { SECOND, MINUTE } from '@/const.js'; import { subscriber as redisClient } from '@/db/redis.js'; import { Users } from '@/models/index.js'; import { Connection } from './stream/index.js'; -import authenticate from './authenticate.js'; +import { authenticate } from './authenticate.js'; export const initializeStreamingServer = (server: http.Server): void => { // Init websocket server diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts index 4c4707e61..56bf14f9a 100644 --- a/packages/backend/src/server/file/index.ts +++ b/packages/backend/src/server/file/index.ts @@ -8,7 +8,7 @@ import { dirname } from 'node:path'; import Koa from 'koa'; import cors from '@koa/cors'; import Router from '@koa/router'; -import sendDriveFile from './send-drive-file.js'; +import { sendDriveFile } from './send-drive-file.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 0ae16d94c..990dd98fe 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -27,8 +27,7 @@ const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => ctx.set('Cache-Control', 'max-age=300'); }; -// eslint-disable-next-line import/no-default-export -export default async function(ctx: Koa.Context) { +export async function sendDriveFile(ctx: Koa.Context) { const key = ctx.params.key; // Fetch drive file @@ -49,7 +48,7 @@ export default async function(ctx: Koa.Context) { const isWebpublic = file.webpublicAccessKey === key; if (!file.storedInternal) { - if (file.isLink && file.uri) { // 期限切れリモートファイル + if (file.isLink && file.uri) { // expired remote file const [path, cleanup] = await createTemp(); try { diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts index b83ccf188..497d677c6 100644 --- a/packages/backend/src/server/web/feed.ts +++ b/packages/backend/src/server/web/feed.ts @@ -4,7 +4,7 @@ import config from '@/config/index.js'; import { User } from '@/models/entities/user.js'; import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js'; -export default async function(user: User) { +export async function packFeed(user: User) { const author = { link: `${config.url}/@${user.username}`, name: user.name || user.username, diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 7fe462d43..7c04077d4 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -26,7 +26,7 @@ import { MINUTE, DAY } from '@/const.js'; import { genOpenapiSpec } from '../api/openapi/gen-spec.js'; import { urlPreviewHandler } from './url-preview.js'; import { manifestHandler } from './manifest.js'; -import packFeed from './feed.js'; +import { packFeed } from './feed.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts index 8b146951d..c37b83313 100644 --- a/packages/backend/src/services/blocking/delete.ts +++ b/packages/backend/src/services/blocking/delete.ts @@ -2,13 +2,13 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; import { deliver } from '@/queue/index.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { Blockings, Users } from '@/models/index.js'; import Logger from '../logger.js'; const logger = new Logger('blocking/delete'); -export default async function(blocker: CacheableUser, blockee: CacheableUser) { +export default async function(blocker: User, blockee: User) { const blocking = await Blockings.findOneBy({ blockerId: blocker.id, blockeeId: blockee.id, diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index e8f3793b7..912ac25f6 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -60,10 +60,7 @@ export async function uploadFromUrl({ logger.succ(`Got: ${driveFile.id}`); return driveFile; } catch (e) { - logger.error(`Failed to create drive file: ${e}`, { - url, - e, - }); + logger.error(`Failed to create drive file: ${e}`); throw e; } finally { cleanup(); diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index a3bfc8a0a..5c24186b6 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -2,7 +2,6 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; import convertColor from 'color-convert'; import { format as dateFormat } from 'date-fns'; -import * as SyslogPro from 'syslog-pro'; import config from '@/config/index.js'; import { envOption } from '@/env.js'; import type { KEYWORD } from 'color-convert/conversions.js'; @@ -12,16 +11,26 @@ type Domain = { color?: KEYWORD; }; -type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; +export const LEVELS = { + error: 0, + warning: 1, + success: 2, + info: 3, + debug: 4, +}; +export type Level = LEVELS[keyof LEVELS]; /** - * Class that facilitates recording log messages to the console and optionally a syslog server. + * Class that facilitates recording log messages to the console. */ export default class Logger { private domain: Domain; private parentLogger: Logger | null = null; private store: boolean; - private syslogClient: SyslogPro.RFC5424 | null = null; + /** + * Messages below this level will be discarded. + */ + private minLevel: Level; /** * Create a logger instance. @@ -29,26 +38,13 @@ export default class Logger { * @param color Log message color * @param store Whether to store messages */ - constructor(domain: string, color?: KEYWORD, store = true) { + constructor(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info) { this.domain = { name: domain, color, }; this.store = store; - - if (config.syslog) { - this.syslogClient = new SyslogPro.RFC5424({ - applicationName: 'FoundKey', - timestamp: true, - includeStructuredData: true, - color: true, - extendedColor: true, - server: { - target: config.syslog.host, - port: config.syslog.port, - }, - }); - } + this.minLevel = minLevel; } /** @@ -58,69 +54,90 @@ export default class Logger { * @param store Whether to store messages * @returns A Logger instance whose parent logger is this instance. */ - public createSubLogger(domain: string, color?: KEYWORD, store = true): Logger { - const logger = new Logger(domain, color, store); + public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info): Logger { + const logger = new Logger(domain, color, store, minLevel); logger.parentLogger = this; return logger; } - private log(level: Level, message: string, data?: Record | null, important = false, subDomains: Domain[] = [], _store = true): void { + /** + * Log a message. + * @param level Indicates the level of this particular message. If it is + * less than the minimum level configured, the message will be discarded. + * @param message The message to be logged. + * @param important Whether to highlight this message as especially important. + * @param subDomains Names of sub-loggers to be added. + */ + private log(level: Level, message: string, important = false, subDomains: Domain[] = [], _store = true): void { if (envOption.quiet) return; - const store = _store && this.store && (level !== 'debug'); + const store = _store && this.store; + // Check against the configured log level. + if (level < this.minLevel) return; + + // If this logger has a parent logger, delegate the actual logging to it, + // so the parent domain(s) will be logged properly. if (this.parentLogger) { - this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store); + this.parentLogger.log(level, message, important, [this.domain].concat(subDomains), store); return; } const time = dateFormat(new Date(), 'HH:mm:ss'); const worker = cluster.isPrimary ? '*' : cluster.worker?.id; - const l = - level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : - level === 'warning' ? chalk.yellow('WARN') : - level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') : - level === 'debug' ? chalk.gray('VERB') : - chalk.blue('INFO'); const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.rgb(...convertColor.keyword.rgb(d.color))(d.name) : chalk.white(d.name)); - const m = - level === 'error' ? chalk.red(message) : - level === 'warning' ? chalk.yellow(message) : - level === 'success' ? chalk.green(message) : - level === 'debug' ? chalk.gray(message) : - message; - let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`; + let levelDisplay; + let messageDisplay; + switch (level) { + case LEVELS.error: + if (important) { + levelDisplay = chalk.bgRed.white('ERR '); + } else { + levelDisplay = chalk.red('ERR '); + } + messageDisplay = chalk.red(message); + break; + case LEVELS.warning: + levelDisplay = chalk.yellow('WARN'); + messageDisplay = chalk.yellow(message); + break; + case LEVELS.success: + if (important) { + levelDisplay = chalk.bgGreen.white('DONE'); + } else { + levelDisplay = chalk.green('DONE'); + } + messageDisplay = chalk.green(message); + break; + case LEVELS.info: + levelDisplay = chalk.blue('INFO'); + messageDisplay = message; + break; + case LEVELS.debug: default: + levelDisplay = chalk.gray('VERB'); + messageDisplay = chalk.gray(message); + break; + } + + let log = `${levelDisplay} ${worker}\t[${domains.join(' ')}]\t${messageDisplay}`; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; console.log(important ? chalk.bold(log) : log); - - if (store) { - if (this.syslogClient) { - const send = - level === 'error' ? this.syslogClient.error : - level === 'warning' ? this.syslogClient.warning : - this.syslogClient.info; - - send.bind(this.syslogClient)(message).catch(() => {}); - } - } } /** * Log an error message. * Use in situations where execution cannot be continued. * @param err Error or string containing an error message - * @param data Data relating to the error * @param important Whether this error is important */ - public error(err: string | Error, data: Record = {}, important = false): void { + public error(err: string | Error, important = false): void { if (err instanceof Error) { - data.e = err; - this.log('error', err.toString(), data, important); + this.log(LEVELS.error, err.toString(), important); } else if (typeof err === 'object') { - this.log('error', `${(err as any).message || (err as any).name || err}`, data, important); + this.log(LEVELS.error, `${(err as any).message || (err as any).name || err}`, important); } else { - this.log('error', `${err}`, data, important); + this.log(LEVELS.error, `${err}`, important); } } @@ -128,45 +145,39 @@ export default class Logger { * Log a warning message. * Use in situations where execution can continue but needs to be improved. * @param message Warning message - * @param data Data relating to the warning * @param important Whether this warning is important */ - public warn(message: string, data?: Record | null, important = false): void { - this.log('warning', message, data, important); + public warn(message: string, important = false): void { + this.log(LEVELS.warning, message, important); } /** * Log a success message. * Use in situations where something has been successfully done. * @param message Success message - * @param data Data relating to the success * @param important Whether this success message is important */ - public succ(message: string, data?: Record | null, important = false): void { - this.log('success', message, data, important); + public succ(message: string, important = false): void { + this.log(LEVELS.success, message, important); } /** * Log a debug message. * Use for debugging (information needed by developers but not required by users). * @param message Debug message - * @param data Data relating to the debug message * @param important Whether this debug message is important */ - public debug(message: string, data?: Record | null, important = false): void { - if (process.env.NODE_ENV !== 'production' || envOption.verbose) { - this.log('debug', message, data, important); - } + public debug(message: string, important = false): void { + this.log(LEVELS.debug, message, important); } /** * Log an informational message. * Use when something needs to be logged but doesn't fit into other levels. * @param message Info message - * @param data Data relating to the info message * @param important Whether this info message is important */ - public info(message: string, data?: Record | null, important = false): void { - this.log('info', message, data, important); + public info(message: string, important = false): void { + this.log(LEVELS.info, message, important); } } diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index 631f23863..2f51858ca 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -1,5 +1,5 @@ import { Not } from 'typeorm'; -import { CacheableUser, User } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { UserGroup } from '@/models/entities/user-group.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js'; @@ -13,7 +13,7 @@ import renderCreate from '@/remote/activitypub/renderer/create.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { deliver } from '@/queue/index.js'; -export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { +export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) { const message = { id: genId(), createdAt: new Date(), diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts index b86e7107d..cdd27da50 100644 --- a/packages/backend/src/services/note/polls/vote.ts +++ b/packages/backend/src/services/note/polls/vote.ts @@ -1,12 +1,12 @@ import { ArrayOverlap, Not } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; -import { CacheableUser } from '@/models/entities/user.js'; +import { User } from '@/models/entities/user.js'; import { Note } from '@/models/entities/note.js'; import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { createNotification } from '@/services/create-notification.js'; -export async function vote(user: CacheableUser, note: Note, choice: number): Promise { +export async function vote(user: User, note: Note, choice: number): Promise { const poll = await Polls.findOneBy({ noteId: note.id }); if (poll == null) throw new Error('poll not found'); diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts index 0119d7fdf..87ddee19d 100644 --- a/packages/backend/src/services/stream.ts +++ b/packages/backend/src/services/stream.ts @@ -95,9 +95,7 @@ class Publisher { }; } -const publisher = new Publisher(); - -export default publisher; +export const publisher = new Publisher(); export const publishInternalEvent = publisher.publishInternalEvent; export const publishUserEvent = publisher.publishUserEvent; diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index b4bbb6f1a..77c5d0541 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -1,5 +1,5 @@ import { IsNull } from 'typeorm'; -import { CacheableLocalUser, ILocalUser, User } from '@/models/entities/user.js'; +import { ILocalUser, User } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { subscriber } from '@/db/redis.js'; @@ -8,7 +8,7 @@ export const userByIdCache = new Cache( Infinity, async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined, ); -export const localUserByNativeTokenCache = new Cache( +export const localUserByNativeTokenCache = new Cache( Infinity, async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: false }) as ILocalUser | null ?? undefined, ); diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.ts index cde3cd2d0..6f1d51a67 100644 --- a/packages/backend/test/api-visibility.ts +++ b/packages/backend/test/api-visibility.ts @@ -8,6 +8,7 @@ describe('API visibility', () => { let p: childProcess.ChildProcess; before(async () => { + this.timeout(0); p = await startServer(); }); @@ -17,15 +18,15 @@ describe('API visibility', () => { describe('Note visibility', async () => { //#region vars - /** ヒロイン */ + /** protagonist */ let alice: any; - /** フォロワー */ + /** follower */ let follower: any; - /** 非フォロワー */ + /** non-follower */ let other: any; - /** 非フォロワーでもリプライやメンションをされた人 */ + /** non-follower who has been replied to or mentioned */ let target: any; - /** specified mentionでmentionを飛ばされる人 */ + /** actor for which a specified visibility was set */ let target2: any; /** public-post */ @@ -100,90 +101,90 @@ describe('API visibility', () => { //#region show post // public - it('[show] public-postを自分が見れる', async(async () => { + it('[show] public post can be seen by author', async(async () => { const res = await show(pub.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-postをフォロワーが見れる', async(async () => { + it('[show] public post can be seen by follower', async(async () => { const res = await show(pub.id, follower); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-postを非フォロワーが見れる', async(async () => { + it('[show] public post can be seen by non-follower', async(async () => { const res = await show(pub.id, other); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-postを未認証が見れる', async(async () => { + it('[show] public post can be seen unauthenticated', async(async () => { const res = await show(pub.id, null); assert.strictEqual(res.body.text, 'x'); })); // home - it('[show] home-postを自分が見れる', async(async () => { + it('[show] home post can be seen by author', async(async () => { const res = await show(home.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-postをフォロワーが見れる', async(async () => { + it('[show] home post can be seen by follower', async(async () => { const res = await show(home.id, follower); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-postを非フォロワーが見れる', async(async () => { + it('[show] home post can be seen by non-follower', async(async () => { const res = await show(home.id, other); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-postを未認証が見れる', async(async () => { + it('[show] home post can be seen unauthenticated', async(async () => { const res = await show(home.id, null); assert.strictEqual(res.body.text, 'x'); })); // followers - it('[show] followers-postを自分が見れる', async(async () => { + it('[show] followers post can be seen by author', async(async () => { const res = await show(fol.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] followers-postをフォロワーが見れる', async(async () => { + it('[show] followers post can be seen by follower', async(async () => { const res = await show(fol.id, follower); assert.strictEqual(res.body.text, 'x'); })); - it('[show] followers-postを非フォロワーが見れない', async(async () => { + it('[show] followers post is hidden from non-follower', async(async () => { const res = await show(fol.id, other); assert.strictEqual(res.status, 404); })); - it('[show] followers-postを未認証が見れない', async(async () => { + it('[show] followers post is hidden when unathenticated', async(async () => { const res = await show(fol.id, null); assert.strictEqual(res.status, 404); })); // specified - it('[show] specified-postを自分が見れる', async(async () => { + it('[show] specified post can be seen by author', async(async () => { const res = await show(spe.id, alice); - assert.strictEqual(res.status, 404); + assert.strictEqual(res.body.text, 'x'); })); - it('[show] specified-postを指定ユーザーが見れる', async(async () => { + it('[show] specified post can be seen by designated user', async(async () => { const res = await show(spe.id, target); assert.strictEqual(res.body.text, 'x'); })); - it('[show] specified-postをフォロワーが見れない', async(async () => { + it('[show] specified post is hidden from non-specified follower', async(async () => { const res = await show(spe.id, follower); assert.strictEqual(res.status, 404); })); - it('[show] specified-postを非フォロワーが見れない', async(async () => { + it('[show] specified post is hidden from non-follower', async(async () => { const res = await show(spe.id, other); assert.strictEqual(res.status, 404); })); - it('[show] specified-postを未認証が見れない', async(async () => { + it('[show] specified post is hidden when unauthenticated', async(async () => { const res = await show(spe.id, null); assert.strictEqual(res.status, 404); })); @@ -191,110 +192,105 @@ describe('API visibility', () => { //#region show reply // public - it('[show] public-replyを自分が見れる', async(async () => { + it('[show] public reply can be seen by author', async(async () => { const res = await show(pubR.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-replyをされた人が見れる', async(async () => { + it('[show] public reply can be seen by replied to author', async(async () => { const res = await show(pubR.id, target); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-replyをフォロワーが見れる', async(async () => { + it('[show] public reply can be seen by follower', async(async () => { const res = await show(pubR.id, follower); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-replyを非フォロワーが見れる', async(async () => { + it('[show] public reply can be seen by non-follower', async(async () => { const res = await show(pubR.id, other); assert.strictEqual(res.body.text, 'x'); })); - it('[show] public-replyを未認証が見れる', async(async () => { + it('[show] public reply can be seen unauthenticated', async(async () => { const res = await show(pubR.id, null); assert.strictEqual(res.body.text, 'x'); })); // home - it('[show] home-replyを自分が見れる', async(async () => { + it('[show] home reply can be seen by author', async(async () => { const res = await show(homeR.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-replyをされた人が見れる', async(async () => { + it('[show] home reply can be seen by replied to author', async(async () => { const res = await show(homeR.id, target); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-replyをフォロワーが見れる', async(async () => { + it('[show] home reply can be seen by follower', async(async () => { const res = await show(homeR.id, follower); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-replyを非フォロワーが見れる', async(async () => { + it('[show] home reply can be seen by non-follower', async(async () => { const res = await show(homeR.id, other); assert.strictEqual(res.body.text, 'x'); })); - it('[show] home-replyを未認証が見れる', async(async () => { + it('[show] home reply can be seen unauthenticated', async(async () => { const res = await show(homeR.id, null); assert.strictEqual(res.body.text, 'x'); })); // followers - it('[show] followers-replyを自分が見れる', async(async () => { + it('[show] followers reply can be seen by author', async(async () => { const res = await show(folR.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => { + it('[show] followers reply can be seen by replied to author', async(async () => { const res = await show(folR.id, target); assert.strictEqual(res.body.text, 'x'); })); - it('[show] followers-replyをフォロワーが見れる', async(async () => { + it('[show] followers reply can be seen by follower', async(async () => { const res = await show(folR.id, follower); assert.strictEqual(res.body.text, 'x'); })); - it('[show] followers-replyを非フォロワーが見れない', async(async () => { + it('[show] followers reply is hidden from non-follower', async(async () => { const res = await show(folR.id, other); assert.strictEqual(res.status, 404); })); - it('[show] followers-replyを未認証が見れない', async(async () => { + it('[show] followers reply is hidden when unauthenticated', async(async () => { const res = await show(folR.id, null); assert.strictEqual(res.status, 404); })); // specified - it('[show] specified-replyを自分が見れる', async(async () => { + it('[show] specified reply can be seen by author', async(async () => { const res = await show(speR.id, alice); assert.strictEqual(res.body.text, 'x'); })); - it('[show] specified-replyを指定ユーザーが見れる', async(async () => { + it('[show] specified reply can be seen by replied to user', async(async () => { const res = await show(speR.id, target); assert.strictEqual(res.body.text, 'x'); })); - it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, 'x'); - })); - - it('[show] specified-replyをフォロワーが見れない', async(async () => { + it('[show] specified reply is hidden from follower', async(async () => { const res = await show(speR.id, follower); assert.strictEqual(res.status, 404); })); - it('[show] specified-replyを非フォロワーが見れない', async(async () => { + it('[show] specified reply is hidden from non-follower', async(async () => { const res = await show(speR.id, other); assert.strictEqual(res.status, 404); })); - it('[show] specified-replyを未認証が見れない', async(async () => { + it('[show] specified reply is hidden when unauthenticated', async(async () => { const res = await show(speR.id, null); assert.strictEqual(res.status, 404); })); @@ -302,131 +298,131 @@ describe('API visibility', () => { //#region show mention // public - it('[show] public-mentionを自分が見れる', async(async () => { + it('[show] public-mention can be seen by author', async(async () => { const res = await show(pubM.id, alice); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] public-mentionをされた人が見れる', async(async () => { + it('[show] public mention can be seen by mentioned', async(async () => { const res = await show(pubM.id, target); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] public-mentionをフォロワーが見れる', async(async () => { + it('[show] public mention can be seen by follower', async(async () => { const res = await show(pubM.id, follower); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] public-mentionを非フォロワーが見れる', async(async () => { + it('[show] public mention can be seen by non-follower', async(async () => { const res = await show(pubM.id, other); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] public-mentionを未認証が見れる', async(async () => { + it('[show] public mention can be seen unauthenticated', async(async () => { const res = await show(pubM.id, null); assert.strictEqual(res.body.text, '@target x'); })); // home - it('[show] home-mentionを自分が見れる', async(async () => { + it('[show] home mention can be seen by author', async(async () => { const res = await show(homeM.id, alice); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] home-mentionをされた人が見れる', async(async () => { + it('[show] home mention can be seen by mentioned', async(async () => { const res = await show(homeM.id, target); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] home-mentionをフォロワーが見れる', async(async () => { + it('[show] home mention can be seen by follower', async(async () => { const res = await show(homeM.id, follower); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] home-mentionを非フォロワーが見れる', async(async () => { + it('[show] home mention can be seen by non-follower', async(async () => { const res = await show(homeM.id, other); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] home-mentionを未認証が見れる', async(async () => { + it('[show] home mention can be seen unauthenticated', async(async () => { const res = await show(homeM.id, null); assert.strictEqual(res.body.text, '@target x'); })); // followers - it('[show] followers-mentionを自分が見れる', async(async () => { + it('[show] followers mention can be seen by author', async(async () => { const res = await show(folM.id, alice); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async(async () => { + it('[show] followers mention can be seen by non-follower mentioned', async(async () => { const res = await show(folM.id, target); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] followers-mentionをフォロワーが見れる', async(async () => { + it('[show] followers mention can be seen by follower', async(async () => { const res = await show(folM.id, follower); assert.strictEqual(res.body.text, '@target x'); })); - it('[show] followers-mentionを非フォロワーが見れない', async(async () => { + it('[show] followers mention is hidden from non-follower', async(async () => { const res = await show(folM.id, other); assert.strictEqual(res.status, 404); })); - it('[show] followers-mentionを未認証が見れない', async(async () => { + it('[show] followers mention is hidden when unauthenticated', async(async () => { const res = await show(folM.id, null); assert.strictEqual(res.status, 404); })); // specified - it('[show] specified-mentionを自分が見れる', async(async () => { + it('[show] specified mention can be seen by author', async(async () => { const res = await show(speM.id, alice); assert.strictEqual(res.body.text, '@target2 x'); })); - it('[show] specified-mentionを指定ユーザーが見れる', async(async () => { + it('[show] specified mention can be seen by specified actor', async(async () => { const res = await show(speM.id, target); assert.strictEqual(res.body.text, '@target2 x'); })); - it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => { + it('[show] specified mention is hidden from mentioned but not specified actor', async(async () => { const res = await show(speM.id, target2); assert.strictEqual(res.status, 404); })); - it('[show] specified-mentionをフォロワーが見れない', async(async () => { + it('[show] specified mention is hidden from follower', async(async () => { const res = await show(speM.id, follower); assert.strictEqual(res.status, 404); })); - it('[show] specified-mentionを非フォロワーが見れない', async(async () => { + it('[show] specified mention is hidden from non-follower', async(async () => { const res = await show(speM.id, other); assert.strictEqual(res.status, 404); })); - it('[show] specified-mentionを未認証が見れない', async(async () => { + it('[show] specified mention is hidden when unauthenticated', async(async () => { const res = await show(speM.id, null); assert.strictEqual(res.status, 404); })); //#endregion - //#region HTL - it('[HTL] public-post が 自分が見れる', async(async () => { + //#region Home Timeline + 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); assert.strictEqual(notes[0].text, 'x'); })); - it('[HTL] public-post が 非フォロワーから見れない', async(async () => { + 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); assert.strictEqual(notes.length, 0); })); - it('[HTL] followers-post が フォロワーから見れる', async(async () => { + 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); @@ -434,22 +430,22 @@ describe('API visibility', () => { })); //#endregion - //#region RTL - it('[replies] followers-reply が フォロワーから見れる', async(async () => { + //#region replies timeline + 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); assert.strictEqual(notes[0].text, 'x'); })); - it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => { + 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); assert.strictEqual(notes.length, 0); })); - it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => { + 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); @@ -458,14 +454,14 @@ describe('API visibility', () => { //#endregion //#region MTL - it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => { + 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); assert.strictEqual(notes[0].text, 'x'); })); - it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => { + 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); diff --git a/packages/backend/test/api.ts b/packages/backend/test/api.ts index b1b2ecafc..222c8d4d4 100644 --- a/packages/backend/test/api.ts +++ b/packages/backend/test/api.ts @@ -10,7 +10,8 @@ describe('API', () => { let bob: any; let carol: any; - before(async () => { + before(async function() { + this.timeout(0); p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); diff --git a/packages/backend/test/block.ts b/packages/backend/test/block.ts index b3343813c..ec5d54ca0 100644 --- a/packages/backend/test/block.ts +++ b/packages/backend/test/block.ts @@ -13,6 +13,8 @@ describe('Block', () => { let carol: any; before(async () => { + this.timeout(0); + p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); @@ -23,7 +25,7 @@ describe('Block', () => { await shutdownServer(p); }); - it('Block作成', async(async () => { + it('can block someone', async(async () => { const res = await request('/blocking/create', { userId: bob.id, }, alice); @@ -31,45 +33,45 @@ describe('Block', () => { assert.strictEqual(res.status, 200); })); - it('ブロックされているユーザーをフォローできない', async(async () => { + it('cannot follow if blocked', async(async () => { const res = await request('/following/create', { userId: alice.id }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); + assert.strictEqual(res.body.error.code, 'BLOCKED'); })); - it('ブロックされているユーザーにリアクションできない', async(async () => { + it('cannot react to blocking users note', async(async () => { const note = await post(alice, { text: 'hello' }); const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); + assert.strictEqual(res.body.error.code, 'BLOCKED'); })); - it('ブロックされているユーザーに返信できない', async(async () => { + it('cannot reply to blocking users note', async(async () => { const note = await post(alice, { text: 'hello' }); const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(res.body.error.code, 'BLOCKED'); })); - it('ブロックされているユーザーのノートをRenoteできない', async(async () => { + it('canot renote blocking users note', async(async () => { const note = await post(alice, { text: 'hello' }); const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(res.body.error.code, 'BLOCKED'); })); - // TODO: ユーザーリストに入れられないテスト + it('cannot include blocked users in user lists'); - // TODO: ユーザーリストから除外されるテスト + it('removes users from user lists'); - it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async(async () => { + it('local timeline does not contain blocked users', async(async () => { const aliceNote = await post(alice); const bobNote = await post(bob); const carolNote = await post(carol); diff --git a/packages/backend/test/fetch-resource.ts b/packages/backend/test/fetch-resource.ts index ddb0e94b8..2bdd7a7e2 100644 --- a/packages/backend/test/fetch-resource.ts +++ b/packages/backend/test/fetch-resource.ts @@ -23,6 +23,7 @@ describe('Fetch resource', () => { let alicesPost: any; before(async () => { + this.timeout(0); p = await startServer(); alice = await signup({ username: 'alice' }); alicesPost = await post(alice, { diff --git a/packages/backend/test/ff-visibility.ts b/packages/backend/test/ff-visibility.ts index 4f6847be6..59b94e135 100644 --- a/packages/backend/test/ff-visibility.ts +++ b/packages/backend/test/ff-visibility.ts @@ -9,159 +9,110 @@ describe('FF visibility', () => { let alice: any; let bob: any; - let carol: any; + let follower: any; before(async () => { + this.timeout(0); + p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); + follower = await signup({ username: 'follower' }); + + await request('/following/create', { userId: alice.id }, follower); }); after(async () => { await shutdownServer(p); }); - it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'public', - }, alice); + const visible = (user) => { + return async () => { + const followingRes = await request('/users/following', { + userId: alice.id, + }, user); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, user); - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); + assert.strictEqual(followingRes.status, 200); + assert.ok(Array.isArray(followingRes.body)); + assert.strictEqual(followersRes.status, 200); + assert.ok(Array.isArray(followersRes.body)); + }; + }; - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); + const hidden = (user) => { + return async () => { + const followingRes = await request('/users/following', { + userId: alice.id, + }, user); + const followersRes = await request('/users/followers', { + userId: alice.id, + }, user); - it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + }; + }; - const followingRes = await request('/users/following', { - userId: alice.id, - }, alice); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, alice); + describe('public visibility', () => { + before(async () => { + await request('/i/update', { + ffVisibility: 'public', + }, alice); + }); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); + it('shows followers and following to self', visible(alice)); + it('shows followers and following to a follower', visible(follower)); + it('shows followers and following to a non-follower', visible(bob)); + it('shows followers and following when unauthenticated', visible(null)); - it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async(async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); + it('provides followers in ActivityPub representation', async () => { + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(followersRes.status, 200); + }); + }); - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); + describe('followers visibility', () => { + before(async () => { + await request('/i/update', { + ffVisibility: 'followers', + }, alice); + }); - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - })); + it('shows followers and following to self', visible(alice)); + it('shows followers and following to a follower', visible(follower)); + it('hides followers and following from a non-follower', hidden(bob)); + it('hides followers and following when unauthenticated', hidden(null)); - it('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); + it('hides followers from ActivityPub representation', async () => { + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + }); + }); - await request('/following/create', { - userId: alice.id, - }, bob); + describe('private visibility', () => { + before(async () => { + await request('/i/update', { + ffVisibility: 'private', + }, alice); + }); - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); + it('shows followers and following to self', visible(alice)); + it('hides followers and following from a follower', hidden(follower)); + it('hides followers and following from a non-follower', hidden(bob)); + it('hides followers and following when unauthenticated', hidden(null)); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, alice); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, alice); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async(async () => { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - })); - - describe('AP', () => { - it('ffVisibility が public 以外ならばAPからは取得できない', async(async () => { - { - await request('/i/update', { - ffVisibility: 'public', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(followersRes.status, 200); - } - { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - })); + it('hides followers from ActivityPub representation', async () => { + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + }); }); }); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index ba89ac329..75b80e98c 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -1,4 +1,4 @@ -import Resolver from '../../src/remote/activitypub/resolver.js'; +import { Resolver } from '../../src/remote/activitypub/resolver.js'; import { IObject } from '../../src/remote/activitypub/type.js'; type MockResponse = { diff --git a/packages/backend/test/mute.ts b/packages/backend/test/mute.ts index 465633973..602623f4b 100644 --- a/packages/backend/test/mute.ts +++ b/packages/backend/test/mute.ts @@ -13,6 +13,8 @@ describe('Mute', () => { let carol: any; before(async () => { + this.timeout(0); + p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); diff --git a/packages/backend/test/note.ts b/packages/backend/test/note.ts index b495d8b7b..1f7cf8f03 100644 --- a/packages/backend/test/note.ts +++ b/packages/backend/test/note.ts @@ -13,6 +13,8 @@ describe('Note', () => { let bob: any; before(async () => { + this.timeout(0); + p = await startServer(); const connection = await initTestDb(true); Notes = connection.getRepository(Note); @@ -158,7 +160,7 @@ describe('Note', () => { replyId: '000000000000000000000000', }; const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); })); it('存在しないrenote対象で怒られる', async(async () => { @@ -166,7 +168,7 @@ describe('Note', () => { renoteId: '000000000000000000000000', }; const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); })); it('不正なリプライ先IDで怒られる', async(async () => { @@ -175,7 +177,7 @@ describe('Note', () => { replyId: 'foo', }; const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); })); it('不正なrenote対象IDで怒られる', async(async () => { @@ -183,7 +185,7 @@ describe('Note', () => { renoteId: 'foo', }; const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); })); it('存在しないユーザーにメンションできる', async(async () => { @@ -286,7 +288,7 @@ describe('Note', () => { choice: 2, }, alice); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 409); })); it('許可されている場合は複数投票できる', async(async () => { diff --git a/packages/backend/test/services/blocking.ts b/packages/backend/test/services/blocking.ts index 41e5ef5be..2f72972f2 100644 --- a/packages/backend/test/services/blocking.ts +++ b/packages/backend/test/services/blocking.ts @@ -14,6 +14,8 @@ describe('Creating a block activity', () => { let carol: any; before(async () => { + this.timeout(0); + await initTestDb(); p = await startServer(); alice = await signup({ username: 'alice' }); diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts index 621d07f9c..dd0e814d4 100644 --- a/packages/backend/test/streaming.ts +++ b/packages/backend/test/streaming.ts @@ -38,6 +38,8 @@ describe('Streaming', () => { let list: any; before(async () => { + this.timeout(0); + p = await startServer(); const connection = await initTestDb(true); Followings = connection.getRepository(Following); diff --git a/packages/backend/test/thread-mute.ts b/packages/backend/test/thread-mute.ts index cd3e51939..d1338fd3b 100644 --- a/packages/backend/test/thread-mute.ts +++ b/packages/backend/test/thread-mute.ts @@ -12,6 +12,8 @@ describe('Note thread mute', () => { let carol: any; before(async () => { + this.timeout(0); + p = await startServer(); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); diff --git a/packages/backend/test/user-notes.ts b/packages/backend/test/user-notes.ts index 4447754d6..31a66dbd2 100644 --- a/packages/backend/test/user-notes.ts +++ b/packages/backend/test/user-notes.ts @@ -13,6 +13,8 @@ describe('users/notes', () => { let jpgPngNote: any; before(async () => { + this.timeout(0); + p = await startServer(); alice = await signup({ username: 'alice' }); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index a366547e6..59a62452c 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -31,16 +31,15 @@ export const async = (fn: Function) => (done: Function) => { export const api = async (endpoint: string, params: any, me?: any) => { endpoint = endpoint.replace(/^\//, ''); - const auth = me ? { - i: me.token - } : {}; + const auth = me ? { authorization: `Bearer ${me.token}` } : {}; const res = await got(`http://localhost:${port}/api/${endpoint}`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + ...auth, }, - body: JSON.stringify(Object.assign(auth, params)), + body: JSON.stringify(params), retry: { limit: 0, }, @@ -65,16 +64,15 @@ 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 }> => { - const auth = me ? { - i: me.token, - } : {}; + const auth = me ? { authorization: `Bearer ${me.token}` } : {}; const res = await fetch(`http://localhost:${port}/api${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', + ...auth, }, - body: JSON.stringify(Object.assign(auth, params)), + body: JSON.stringify(params), }); const status = res.status; diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index e39540e47..491755373 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -3,9 +3,9 @@ import * as foundkey from 'foundkey-js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog'; import { i18n } from '@/i18n'; import { del, get, set } from '@/scripts/idb-proxy'; -import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; +import { MenuItem } from '@/types/menu'; // TODO: 他のタブと永続化されたstateを同期 @@ -22,7 +22,7 @@ export async function signout() { waiting(); localStorage.removeItem('account'); - await removeAccount($i.id); + if ($i) await removeAccount($i!.id); const accounts = await getAccounts(); @@ -99,14 +99,18 @@ function fetchAccount(token: string): Promise { } export function updateAccount(accountData) { + if (!$i) return; + for (const [key, value] of Object.entries(accountData)) { - $i[key] = value; + $i![key] = value; } - localStorage.setItem('account', JSON.stringify($i)); + localStorage.setItem('account', JSON.stringify($i!)); } export function refreshAccount() { - return fetchAccount($i.token).then(updateAccount); + if (!$i) return; + + return fetchAccount($i!.token).then(updateAccount); } export async function login(token: Account['token'], redirect?: string) { @@ -134,7 +138,39 @@ export async function openAccountMenu(opts: { active?: foundkey.entities.UserDetailed['id']; onChoose?: (account: foundkey.entities.UserDetailed) => void; }, ev: MouseEvent) { - function showSigninDialog() { + const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i?.id)); + const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); + + const switchAccount = async (account: foundkey.entities.UserDetailed) => { + const storedAccounts = await getAccounts(); + const token = storedAccounts.find(x => x.id === account.id)?.token; + if (!token) { + // TODO error handling? + } else { + login(token); + } + }; + const createItem = (account: foundkey.entities.UserDetailed): MenuItem => ({ + type: 'user', + user: account, + active: opts.active != null ? opts.active === account.id : false, + action: () => { + if (opts.onChoose) { + opts.onChoose(account); + } else { + switchAccount(account); + } + }, + }); + const accountItemPromises: Promise = storedAccounts.map(a => new Promise(res => { + accountsPromise.then(accounts => { + const account = accounts.find(x => x.id === a.id); + if (account == null) return res(null); + res(createItem(account)); + }); + })); + + const showSigninDialog = () => { popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); @@ -143,50 +179,14 @@ export async function openAccountMenu(opts: { }, 'closed'); } - function createAccount() { + const createAccount = () => { popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); - switchAccountWithToken(res.i); + login(res.i); }, }, 'closed'); - } - - async function switchAccount(account: foundkey.entities.UserDetailed) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - switchAccountWithToken(token); - } - - function switchAccountWithToken(token: string) { - login(token); - } - - const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); - const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); - - function createItem(account: foundkey.entities.UserDetailed) { - return { - type: 'user', - user: account, - active: opts.active != null ? opts.active === account.id : false, - action: () => { - if (opts.onChoose) { - opts.onChoose(account); - } else { - switchAccount(account); - } - }, - }; - } - - const accountItemPromises = storedAccounts.map(a => new Promise(res => { - accountsPromise.then(accounts => { - const account = accounts.find(x => x.id === a.id); - if (account == null) return res(null); - res(createItem(account)); - }); - })); + }; if (opts.withExtraOperation) { popupMenu([...[{ @@ -194,16 +194,16 @@ export async function openAccountMenu(opts: { text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, - }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + }, null, ...(opts.includeCurrentAccount && $i ? [createItem($i)] : []), ...accountItemPromises, { icon: 'fas fa-plus', text: i18n.ts.addAccount, action: () => { popupMenu([{ text: i18n.ts.existingAccount, - action: () => { showSigninDialog(); }, + action: showSigninDialog, }, { text: i18n.ts.createAccount, - action: () => { createAccount(); }, + action: createAccount, }], ev.currentTarget ?? ev.target); }, }, { @@ -211,11 +211,11 @@ export async function openAccountMenu(opts: { icon: 'fas fa-users', text: i18n.ts.manageAccounts, to: '/settings/accounts', - }]], ev.currentTarget ?? ev.target, { + }]], ev.currentTarget ?? ev.target ?? undefined, { align: 'left', }); } else { - popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { + popupMenu([...(opts.includeCurrentAccount && $i ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target ?? undefined, { align: 'left', }); } diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue index 72ab8b9ce..eb0c5f34b 100644 --- a/packages/client/src/components/global/misskey-flavored-markdown.vue +++ b/packages/client/src/components/global/misskey-flavored-markdown.vue @@ -56,6 +56,14 @@ withDefaults(defineProps<{ } } +._mfm_small_ { + opacity: 0.7; +} + +._mfm_small_ ._mfm_small_{ + opacity: initial; +} + @keyframes mfm-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index ef083eb4d..64fe5bdb5 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -44,18 +44,19 @@ export default defineComponent({ const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text, { fnNameList: MFM_TAGS }); - const validTime = (t: string | null | undefined) => { - if (t == null) return null; + const validTime = (t: string | true) => { + if (typeof t !== 'string') return null; + return t.match(/^[0-9.]+s$/) ? t : null; }; - const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode[] => { + const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode | VNode[] => { switch (token.type) { case 'text': { const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!this.plain) { - const res = []; + const res: VNode[] = []; for (const t of text.split('\n')) { res.push(h('br')); res.push(t); @@ -63,16 +64,16 @@ export default defineComponent({ res.shift(); return res; } else { - return [text.replace(/\n/g, ' ')]; + return text.replace(/\n/g, ' '); } } case 'bold': { - return [h('b', genEl(token.children))]; + return h('b', genEl(token.children)); } case 'strike': { - return [h('del', genEl(token.children))]; + return h('del', genEl(token.children)); } case 'italic': { @@ -180,7 +181,7 @@ export default defineComponent({ return h(MkSparkle, {}, genEl(token.children)); } case 'rotate': { - const degrees = parseInt(token.props.args.deg) || '90'; + const degrees = (typeof token.props.args.deg === 'string' ? parseInt(token.props.args.deg) : null) || '90'; style = `transform: rotate(${degrees}deg); transform-origin: center center;`; break; } @@ -195,116 +196,110 @@ export default defineComponent({ } case 'small': { - return [h('small', { - style: 'opacity: 0.7;', - }, genEl(token.children))]; + return h('small', { + class: '_mfm_small_' + }, genEl(token.children)); } case 'center': { - return [h('div', { + return h('div', { style: 'text-align:center;', - }, genEl(token.children))]; + }, genEl(token.children)); } case 'url': { - return [h(MkUrl, { + return h(MkUrl, { key: Math.random(), url: token.props.url, rel: 'nofollow noopener', - })]; + }); } case 'link': { - return [h(MkLink, { + return h(MkLink, { key: Math.random(), url: token.props.url, rel: 'nofollow noopener', - }, genEl(token.children))]; + }, genEl(token.children)); } case 'mention': { - return [h(MkMention, { + return h(MkMention, { key: Math.random(), host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, username: token.props.username, - })]; + }); } case 'hashtag': { - return [h(MkA, { + return h(MkA, { key: Math.random(), to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`, style: 'color:var(--hashtag);', - }, `#${token.props.hashtag}`)]; + }, `#${token.props.hashtag}`); } case 'blockCode': { - return [h(MkCode, { + return h(MkCode, { key: Math.random(), code: token.props.code, lang: token.props.lang, - })]; + }); } case 'inlineCode': { - return [h(MkCode, { + return h(MkCode, { key: Math.random(), code: token.props.code, inline: true, - })]; + }); } case 'quote': { - if (!this.nowrap) { - return [h('div', { - class: 'quote', - }, genEl(token.children))]; - } else { - return [h('span', { - class: 'quote', - }, genEl(token.children))]; - } + return h(this.nowrap ? 'span' : 'div', { + class: 'quote', + }, genEl(token.children)); } case 'emojiCode': { - return [h(MkEmoji, { + return h(MkEmoji, { key: Math.random(), emoji: `:${token.props.name}:`, customEmojis: this.customEmojis, normal: this.plain, - })]; + }); } case 'unicodeEmoji': { - return [h(MkEmoji, { + return h(MkEmoji, { key: Math.random(), emoji: token.props.emoji, customEmojis: this.customEmojis, normal: this.plain, - })]; + }); } case 'mathInline': { - return [h(MkFormula, { + return h(MkFormula, { key: Math.random(), formula: token.props.formula, block: false, - })]; + }); } case 'mathBlock': { - return [h(MkFormula, { + return h(MkFormula, { key: Math.random(), formula: token.props.formula, block: true, - })]; + }); } case 'search': { - return [h(MkSearch, { + return h(MkSearch, { key: Math.random(), q: token.props.query, - })]; + }); } default: { diff --git a/packages/client/src/directives/get-size.ts b/packages/client/src/directives/get-size.ts index 575b4ce3c..74765ae03 100644 --- a/packages/client/src/directives/get-size.ts +++ b/packages/client/src/directives/get-size.ts @@ -43,7 +43,7 @@ export default { calc(src); }, - unmounted(src) { + unmounted(src, binding) { binding.value(0, 0); const info = mountings.get(src); if (!info) return; diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index 44511fb3e..80fb46b1f 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -10,7 +10,7 @@ class TooltipDirective { public text: string | null; private asMfm: boolean; - private _close: null | () => void; + private _close: null | (() => void); private showTimer: null | ReturnType; private hideTimer: null | ReturnType; @@ -23,7 +23,7 @@ class TooltipDirective { this.hideTimer = null; } - private close(): void { + public close(): void { if (this.hideTimer != null) return; // already closed or closing // cancel any pending attempts to show @@ -96,7 +96,7 @@ export default { const end = isTouchUsing ? 'touchend' : 'mouseleave'; el.addEventListener(start, () => self.show(el), { passive: true }); el.addEventListener(end, () => self.close(), { passive: true }); - el.addEventListener('click', self.close()); + el.addEventListener('click', () => self.close()); el.addEventListener('selectstart', ev => ev.preventDefault()); }, diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 24f2659de..c465d3211 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -26,7 +26,7 @@ export const api = ((endpoint: string, data: Record = {}, token?: s const authorizationToken = token ?? $i?.token ?? undefined; const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined; - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { method: 'POST', body: JSON.stringify(data), @@ -66,7 +66,7 @@ export const apiGet = ((endpoint: string, data: Record = {}, token? const authorizationToken = token ?? $i?.token ?? undefined; const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined; - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { // Send request fetch(`${apiUrl}/${endpoint}?${query}`, { method: 'GET', @@ -103,7 +103,7 @@ export const apiWithDialog = (( promiseDialog(promise, null, (err) => { alert({ type: 'error', - text: (err.message + '\n' + (err?.endpoint ?? '') + ' ' + (err?.code ?? '')).trim(), + text: (err.message + '\n' + (err.endpoint ?? '') + ' ' + (err.code ?? '')).trim(), }); }); @@ -293,9 +293,7 @@ export function inputDate(props: { text?: string | null; placeholder?: string | null; default?: Date | null; -}): Promise<{ canceled: true; result: undefined; } | { - canceled: false; result: Date; -}> { +}): Promise<{ canceled: true; } | { canceled: false; result: Date; }> { return new Promise((resolve) => { popup(defineAsyncComponent(() => import('@/components/dialog.vue')), { title: props.title, @@ -351,7 +349,7 @@ export function select(props: { } export function success() { - return new Promise((resolve) => { + return new Promise((resolve) => { const showing = ref(true); window.setTimeout(() => { showing.value = false; @@ -366,7 +364,7 @@ export function success() { } export function waiting() { - return new Promise((resolve) => { + return new Promise((resolve) => { const showing = ref(true); popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), { success: false, @@ -571,17 +569,3 @@ export function post(props: Record = {}) { } export const deckGlobalEvents = new EventEmitter(); - -/* -export function checkExistence(fileData: ArrayBuffer): Promise { - return new Promise((resolve) => { - const data = new FormData(); - data.append('md5', getMD5(fileData)); - - os.api('drive/files/find-by-hash', { - md5: getMD5(fileData) - }).then(resp => { - resolve(resp.length > 0 ? resp[0] : null); - }); - }); -}*/ diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index f8b02efb6..7c99bf83a 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -16,6 +16,7 @@ + diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index 4441c24dc..22688fab9 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -87,7 +87,6 @@ import * as os from '@/os'; import { ColdDeviceStorage, defaultStore } from '@/store'; import { addTheme } from '@/theme-store'; import { i18n } from '@/i18n'; -import { useLeaveGuard } from '@/scripts/use-leave-guard'; import { definePageMetadata } from '@/scripts/page-metadata'; const bgColors = [ @@ -125,9 +124,6 @@ let theme = $ref>({ }); let description = $ref(null); let themeCode = $ref(null); -let changed = $ref(false); - -useLeaveGuard($$(changed)); function showPreview() { os.pageWindow('/preview'); @@ -162,7 +158,6 @@ function setFgColor(color) { function apply() { themeCode = JSON5.stringify(theme, null, '\t'); applyTheme(theme, false); - changed = true; } function applyThemeCode() { @@ -199,7 +194,6 @@ async function saveAs() { } else { ColdDeviceStorage.set('lightTheme', theme); } - changed = false; os.alert({ type: 'success', text: i18n.t('_theme.installed', { name: theme.name }), diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts deleted file mode 100644 index a93b84d1f..000000000 --- a/packages/client/src/scripts/use-leave-guard.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { inject, onUnmounted, Ref } from 'vue'; -import { i18n } from '@/i18n'; -import * as os from '@/os'; - -export function useLeaveGuard(enabled: Ref) { - /* TODO - const setLeaveGuard = inject('setLeaveGuard'); - - if (setLeaveGuard) { - setLeaveGuard(async () => { - if (!enabled.value) return false; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return canceled; - }); - } else { - onBeforeRouteLeave(async (to, from) => { - if (!enabled.value) return true; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return !canceled; - }); - } - */ - - /* - function onBeforeLeave(ev: BeforeUnloadEvent) { - if (enabled.value) { - ev.preventDefault(); - ev.returnValue = ''; - } - } - - window.addEventListener('beforeunload', onBeforeLeave); - onUnmounted(() => { - window.removeEventListener('beforeunload', onBeforeLeave); - }); - */ -} diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 96ea2e725..489cca84d 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -1,6 +1,5 @@ import { markRaw, ref } from 'vue'; import { Storage } from '@/pizzax'; -import { Theme } from '@/scripts/theme'; export const postFormActions = []; export const userActions = []; diff --git a/packages/client/src/ui/_common_/sw-inject.ts b/packages/client/src/ui/_common_/sw-inject.ts index 8676d2d48..a92a06bd3 100644 --- a/packages/client/src/ui/_common_/sw-inject.ts +++ b/packages/client/src/ui/_common_/sw-inject.ts @@ -1,7 +1,5 @@ -import { inject } from 'vue'; import { post } from '@/os'; import { $i, login } from '@/account'; -import { defaultStore } from '@/store'; import { getAccountFromId } from '@/scripts/get-account-from-id'; import { mainRouter } from '@/router'; diff --git a/packages/foundkey-js/src/api.ts b/packages/foundkey-js/src/api.ts index 923fbd304..8a9ff1fce 100644 --- a/packages/foundkey-js/src/api.ts +++ b/packages/foundkey-js/src/api.ts @@ -42,13 +42,15 @@ export class APIClient { constructor(opts: { origin: APIClient['origin']; credential?: APIClient['credential']; - fetch?: APIClient['fetch'] | null | undefined; + fetch?: FetchLike; }) { this.origin = opts.origin; this.credential = opts.credential; - // ネイティブ関数をそのまま変数に代入して使おうとするとChromiumではIllegal invocationエラーが発生するため、 - // 環境で実装されているfetchを使う場合は無名関数でラップして使用する - this.fetch = opts.fetch || ((...args) => fetch(...args)); + // Wrap a native function with an anonymous function when using + // environment-implemented fetch, because Chromium generates an + // Illegal invocation error if you try to use a native function by + // assigning it to a variable as it is. + this.fetch = opts.fetch || (((...args) => fetch(...args)) as FetchLike); } public request( @@ -79,7 +81,7 @@ export class APIClient { cache: 'no-cache', }).then(async (res) => { const body = res.status === 204 ? null : await res.json(); - + if (res.status === 200) { resolve(body); } else if (res.status === 204) { diff --git a/packages/foundkey-js/src/api.types.ts b/packages/foundkey-js/src/api.types.ts index 8670f5cd6..4c6b098f5 100644 --- a/packages/foundkey-js/src/api.types.ts +++ b/packages/foundkey-js/src/api.types.ts @@ -1,6 +1,5 @@ import { - Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, Instance, InstanceMetadata, - LiteInstanceMetadata, + Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, InstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, Instance, MeDetailed, Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, } from './entities.js'; @@ -380,21 +379,7 @@ export type Endpoints = { 'messaging/messages/create': { req: { userId?: User['id']; groupId?: UserGroup['id']; text?: string; fileId?: DriveFile['id']; }; res: MessagingMessage; }; 'messaging/messages/delete': { req: { messageId: MessagingMessage['id']; }; res: null; }; 'messaging/messages/read': { req: { messageId: MessagingMessage['id']; }; res: null; }; - 'meta': { req: { detail?: boolean; }; res: { - $switch: { - $cases: [[ - { detail: true; }, - DetailedInstanceMetadata, - ], [ - { detail: false; }, - LiteInstanceMetadata, - ], [ - { detail: boolean; }, - LiteInstanceMetadata | DetailedInstanceMetadata, - ]]; - $default: LiteInstanceMetadata; - }; - }; }; + 'meta': { req: { detail?: boolean; }; res: InstanceMetadata; }; 'miauth/gen-token': { req: TODO; res: TODO; }; 'mute/create': { req: TODO; res: TODO; }; 'mute/delete': { req: { userId: User['id'] }; res: null; }; diff --git a/packages/foundkey-js/src/consts.ts b/packages/foundkey-js/src/consts.ts index a7ea18d25..ed9fd8e53 100644 --- a/packages/foundkey-js/src/consts.ts +++ b/packages/foundkey-js/src/consts.ts @@ -4,7 +4,7 @@ export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 're export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const ffVisibility = ['public', 'followers', 'private'] as const; +export const ffVisibility = ['public', 'followers', 'private', 'nobody'] as const; export const permissions = [ 'read:account', diff --git a/packages/foundkey-js/src/entities.ts b/packages/foundkey-js/src/entities.ts index 3742630bb..a83a8c123 100644 --- a/packages/foundkey-js/src/entities.ts +++ b/packages/foundkey-js/src/entities.ts @@ -38,7 +38,7 @@ export type UserDetailed = UserLite & { birthday: string | null; createdAt: DateString; description: string | null; - ffVisibility: 'public' | 'followers' | 'private'; + ffVisibility: 'public' | 'followers' | 'private' | 'nobody'; fields: {name: string; value: string}[]; followersCount: number; followingCount: number; @@ -135,9 +135,9 @@ export type Note = { user: User; userId: User['id']; reply?: Note; - replyId: Note['id']; + replyId: Note['id'] | null; renote?: Note; - renoteId: Note['id']; + renoteId: Note['id'] | null; files: DriveFile[]; fileIds: DriveFile['id'][]; visibility: NoteVisibility; @@ -260,7 +260,7 @@ export type CustomEmoji = { aliases: string[]; }; -export type LiteInstanceMetadata = { +export type InstanceMetadata = { maintainerName: string | null; maintainerEmail: string | null; version: string; @@ -290,14 +290,9 @@ export type LiteInstanceMetadata = { notFound: string; info: string; }; -}; - -export type DetailedInstanceMetadata = LiteInstanceMetadata & { features: Record; }; -export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; - export type ServerInfo = { machine: string; cpu: { @@ -388,8 +383,6 @@ export type AuthSession = { token: string; }; -export type Ad = TODO; - export type Clip = TODO; export type NoteFavorite = { @@ -482,9 +475,6 @@ export function isPureRenote(note: Note): boolean { return note.renoteId != null && note.text == null && note.cw == null - && ( - note.fileIds == null - || note.fileIds.length === 0 - ) + && note.fileIds.length === 0 && note.poll == null; } diff --git a/packages/foundkey-js/src/streaming.ts b/packages/foundkey-js/src/streaming.ts index 0892db9d9..4a2ca58e6 100644 --- a/packages/foundkey-js/src/streaming.ts +++ b/packages/foundkey-js/src/streaming.ts @@ -32,16 +32,15 @@ export default class Stream extends EventEmitter { private nonSharedConnections: NonSharedConnection[] = []; private idCounter = 0; - constructor(origin: string, user: { token: string; } | null, options?: { + constructor(origin: string, user: { token: string; } | null, options: { WebSocket?: any; - }) { + } = {}) { super(); - options = options || { }; const query = urlQuery({ i: user?.token, - - // To prevent cache of an HTML such as error screen + + // cache busting parameter _t: Date.now(), }); diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 87748ad6f..f36e66a0b 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -67,6 +67,7 @@ module.exports = { '@typescript-eslint/no-var-requires': ['warn'], '@typescript-eslint/no-inferrable-types': ['warn'], '@typescript-eslint/no-empty-function': ['off'], + '@typescript-eslint/no-explicit-any': ['off'], '@typescript-eslint/no-non-null-assertion': ['warn'], '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], '@typescript-eslint/no-misused-promises': ['error', { diff --git a/yarn.lock b/yarn.lock index 0a679b247..1786f0c51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2443,13 +2443,6 @@ __metadata: languageName: node linkType: hard -"@types/syslog-pro@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/syslog-pro@npm:1.0.0" - checksum: d0dcd87efad8a629bba449f86a617605a3fbffa5c18a8b309c82e7b85036ac21cfd34711fd522f50528dd0f0d07bdb66261a6f9ef20f2a9133e847b2e717c1bc - languageName: node - linkType: hard - "@types/throttle-debounce@npm:5.0.0": version: 5.0.0 resolution: "@types/throttle-debounce@npm:5.0.0" @@ -2685,13 +2678,6 @@ __metadata: languageName: node linkType: hard -"@ungap/promise-all-settled@npm:1.1.2": - version: 1.1.2 - resolution: "@ungap/promise-all-settled@npm:1.1.2" - checksum: 08d37fdfa23a6fe8139f1305313562ebad973f3fac01bcce2773b2bda5bcb0146dfdcf3cb6a722cf0a5f2ca0bc56a827eac8f1e7b3beddc548f654addf1fc34c - languageName: node - linkType: hard - "@vitejs/plugin-vue@npm:^3.1.0": version: 3.1.0 resolution: "@vitejs/plugin-vue@npm:3.1.0" @@ -3709,7 +3695,6 @@ __metadata: "@types/sinon": ^10.0.13 "@types/sinonjs__fake-timers": 8.1.2 "@types/speakeasy": 2.0.7 - "@types/syslog-pro": ^1.0.0 "@types/tinycolor2": 1.4.3 "@types/tmp": 0.2.3 "@types/uuid": 8.3.4 @@ -3767,7 +3752,7 @@ __metadata: koa-views: 7.0.2 mfm-js: 0.22.1 mime-types: 2.1.35 - mocha: 10.0.0 + mocha: 10.2.0 multer: 1.4.5-lts.1 nested-property: 4.0.0 node-fetch: 3.2.6 @@ -3799,7 +3784,6 @@ __metadata: stringz: 2.1.0 style-loader: 3.3.1 summaly: 2.7.0 - syslog-pro: 1.0.0 systeminformation: 5.11.22 tinycolor2: 1.4.2 tmp: 0.2.1 @@ -12012,11 +11996,10 @@ __metadata: languageName: node linkType: hard -"mocha@npm:10.0.0": - version: 10.0.0 - resolution: "mocha@npm:10.0.0" +"mocha@npm:10.2.0": + version: 10.2.0 + resolution: "mocha@npm:10.2.0" dependencies: - "@ungap/promise-all-settled": 1.1.2 ansi-colors: 4.1.1 browser-stdout: 1.3.1 chokidar: 3.5.3 @@ -12041,7 +12024,7 @@ __metadata: bin: _mocha: bin/_mocha mocha: bin/mocha.js - checksum: ba49ddcf8015a467e744b06c396aab361b1281302e38e7c1269af25ba51ff9ab681a9c36e9046bb7491e751cd7d5ce85e276a00ce7e204f96b2c418e4595edfe + checksum: 406c45eab122ffd6ea2003c2f108b2bc35ba036225eee78e0c784b6fa2c7f34e2b13f1dbacef55a4fdf523255d76e4f22d1b5aacda2394bd11666febec17c719 languageName: node linkType: hard @@ -12052,13 +12035,6 @@ __metadata: languageName: node linkType: hard -"moment@npm:^2.22.2": - version: 2.29.4 - resolution: "moment@npm:2.29.4" - checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e - languageName: node - linkType: hard - "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -16149,15 +16125,6 @@ __metadata: languageName: node linkType: hard -"syslog-pro@npm:1.0.0": - version: 1.0.0 - resolution: "syslog-pro@npm:1.0.0" - dependencies: - moment: ^2.22.2 - checksum: 7d6399e4ca3a9305758f77b3e720469b39c156b5a8219ed4ce27b4ad8f960f8e395aebb0ccc84e4438b50a6b2cda2e20251e278307833ed7ac1045ae9516a33c - languageName: node - linkType: hard - "systeminformation@npm:5.11.22": version: 5.11.22 resolution: "systeminformation@npm:5.11.22"