Merge branch 'main' into mk.absturztau.be

This commit is contained in:
Puniko 2023-05-21 16:39:43 +02:00
commit 7e808d663a
117 changed files with 748 additions and 887 deletions

View file

@ -108,11 +108,6 @@ redis:
#deliverJobMaxAttempts: 12 #deliverJobMaxAttempts: 12
#inboxJobMaxAttempts: 8 #inboxJobMaxAttempts: 8
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS outgoing connections # Proxy for HTTP/HTTPS outgoing connections
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128

View file

@ -8,15 +8,15 @@ clone:
pipeline: pipeline:
install: install:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn install - yarn install
build: build:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn build - yarn build

View file

@ -8,15 +8,15 @@ clone:
pipeline: pipeline:
install: install:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn install - yarn install
lint: lint:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn workspace backend run lint - yarn workspace backend run lint

View file

@ -8,15 +8,15 @@ clone:
pipeline: pipeline:
install: install:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn install - yarn install
lint: lint:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn workspace client run lint - yarn workspace client run lint

View file

@ -8,15 +8,15 @@ clone:
pipeline: pipeline:
install: install:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn install - yarn install
lint: lint:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn workspace foundkey-js run lint - yarn workspace foundkey-js run lint

View file

@ -8,15 +8,15 @@ clone:
pipeline: pipeline:
install: install:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn install - yarn install
lint: lint:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn workspace sw run lint - yarn workspace sw run lint

View file

@ -5,11 +5,14 @@ clone:
depth: 1 # CI does not need commit history depth: 1 # CI does not need commit history
recursive: true recursive: true
depends_on:
- build
pipeline: pipeline:
build: build:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn install - yarn install
@ -18,15 +21,15 @@ pipeline:
- yarn build - yarn build
mocha: mocha:
when: when:
event: branch: main
- pull_request event: push
image: node:18.6.0 image: node:18.6.0
commands: commands:
- yarn mocha - yarn mocha
e2e: e2e:
when: when:
event: branch: main
- pull_request event: push
image: cypress/included:10.3.0 image: cypress/included:10.3.0
commands: commands:
- npm run start:test & - npm run start:test &

View file

@ -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. 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: 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. 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. 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. 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. 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. 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. Be sure to only import emoji from trusted sources, ideally only ones you exported yourself.
:::
### Packed emoji format ### 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. 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. 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. 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. The emoji will no longer be rendered correctly.
:::
Note that remote emoji can not be edited or deleted. Note that remote emoji can not be edited or deleted.

View file

@ -20,7 +20,7 @@ cd packages/backend
LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n nsfwDetection1655368940105 | cut -d ':' -f 1)" 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)" 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 npx typeorm migration:revert -d ormconfig.js
done done
``` ```

View file

@ -844,6 +844,7 @@ _ffVisibility:
public: "Public" public: "Public"
followers: "Visible to followers only" followers: "Visible to followers only"
private: "Private" private: "Private"
nobody: "Nobody (not even you)"
_signup: _signup:
almostThere: "Almost there" almostThere: "Almost there"
emailAddressInfo: "Please enter your email address. It will not be made public." emailAddressInfo: "Please enter your email address. It will not be made public."

View file

@ -854,6 +854,18 @@ _mfm:
spin: Animación (Spin) spin: Animación (Spin)
shakeDescription: Brinda al contenido una animación temblorosa. shakeDescription: Brinda al contenido una animación temblorosa.
inlineMath: Función matemática (Inline) 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: _instanceTicker:
none: "No mostrar" none: "No mostrar"
remote: "Mostrar a usuarios remotos" remote: "Mostrar a usuarios remotos"
@ -1318,3 +1330,9 @@ unlikeConfirm: ¿En verdad quieres remover tu like?
breakFollow: Quitar seguidor breakFollow: Quitar seguidor
reporter: Reportero reporter: Reportero
continueThread: Ver la continuación del hilo 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

View file

@ -842,8 +842,8 @@ _ffVisibility:
private: "비공개" private: "비공개"
_signup: _signup:
almostThere: "거의 다 끝났습니다" almostThere: "거의 다 끝났습니다"
emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다." emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다."
emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요." emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요."
_accountDelete: _accountDelete:
accountDelete: "계정 삭제" accountDelete: "계정 삭제"
mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다." mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다."

View file

@ -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"`);
}
}

View file

@ -69,7 +69,7 @@
"koa-views": "7.0.2", "koa-views": "7.0.2",
"mfm-js": "0.22.1", "mfm-js": "0.22.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"mocha": "10.0.0", "mocha": "10.2.0",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.2.6", "node-fetch": "3.2.6",
@ -100,7 +100,6 @@
"stringz": "2.1.0", "stringz": "2.1.0",
"style-loader": "3.3.1", "style-loader": "3.3.1",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.22", "systeminformation": "5.11.22",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tmp": "0.2.1", "tmp": "0.2.1",
@ -158,7 +157,6 @@
"@types/sinon": "^10.0.13", "@types/sinon": "^10.0.13",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/syslog-pro": "^1.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",

View file

@ -8,7 +8,7 @@ import chalkTemplate from 'chalk-template';
import semver from 'semver'; import semver from 'semver';
import Logger from '@/services/logger.js'; 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 { Config } from '@/config/types.js';
import { showMachineInfo } from '@/misc/show-machine-info.js'; import { showMachineInfo } from '@/misc/show-machine-info.js';
import { envOption } from '@/env.js'; import { envOption } from '@/env.js';
@ -41,7 +41,7 @@ function greet(): void {
} }
bootLogger.info('Welcome to FoundKey!'); 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<void> {
config = loadConfigBoot(); config = loadConfigBoot();
await connectDb(); await connectDb();
} catch (e) { } catch (e) {
bootLogger.error('Fatal error occurred during initialization', {}, true); bootLogger.error('Fatal error occurred during initialization', true);
process.exit(1); process.exit(1);
} }
@ -69,7 +69,7 @@ export async function masterMain(): Promise<void> {
await spawnWorkers(config.clusterLimits); 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) { if (!envOption.noDaemons) {
import('../daemons/server-stats.js').then(x => x.serverStats()); import('../daemons/server-stats.js').then(x => x.serverStats());
@ -84,7 +84,7 @@ function showEnvironment(): void {
if (env !== 'production') { if (env !== 'production') {
logger.warn('The environment is not in production mode.'); 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) { } catch (exception) {
const e = exception as Partial<NodeJS.ErrnoException> | Error; const e = exception as Partial<NodeJS.ErrnoException> | Error;
if ('code' in e && e.code === 'ENOENT') { if ('code' in e && e.code === 'ENOENT') {
configLogger.error('Configuration file not found', {}, true); configLogger.error('Configuration file not found', true);
process.exit(1); process.exit(1);
} else if (e instanceof Error) { } else if (e instanceof Error) {
configLogger.error(e.message); configLogger.error(e.message);
@ -133,7 +133,7 @@ async function connectDb(): Promise<void> {
const v = await db.query('SHOW server_version').then(x => x[0].server_version); const v = await db.query('SHOW server_version').then(x => x[0].server_version);
dbLogger.succ(`Connected: v${v}`); dbLogger.succ(`Connected: v${v}`);
} catch (e) { } catch (e) {
dbLogger.error('Cannot connect', {}, true); dbLogger.error('Cannot connect', true);
dbLogger.error(e as Error | string); dbLogger.error(e as Error | string);
process.exit(1); process.exit(1);
} }
@ -160,12 +160,24 @@ function spawnWorker(mode: 'web' | 'queue'): Promise<void> {
return new Promise(res => { return new Promise(res => {
const worker = cluster.fork({ mode }); const worker = cluster.fork({ mode });
worker.on('message', message => { worker.on('message', message => {
if (message === 'listenFailed') { switch (message) {
bootLogger.error('The server Listen failed due to the previous error.'); case 'listenFailed':
process.exit(1); 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();
}); });
}); });
} }

View file

@ -1,3 +1,3 @@
import load from './load.js'; import { loadConfig } from './load.js';
export default load(); export default loadConfig();

View file

@ -23,7 +23,7 @@ const path = process.env.NODE_ENV === 'test'
? `${dir}/test.yml` ? `${dir}/test.yml`
: `${dir}/default.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 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')); 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; let config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;

View file

@ -59,11 +59,6 @@ export type Source = {
deliverJobMaxAttempts?: number; deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number; inboxJobMaxAttempts?: number;
syslog?: {
host: string;
port: number;
};
mediaProxy?: string; mediaProxy?: string;
proxyRemoteFiles?: boolean; proxyRemoteFiles?: boolean;
internalStoragePath?: string; internalStoragePath?: string;

View file

@ -1,3 +1,4 @@
import process from 'node:process';
import push from 'web-push'; import push from 'web-push';
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { Meta } from '@/models/entities/meta.js'; import { Meta } from '@/models/entities/meta.js';
@ -17,9 +18,20 @@ export async function setMeta(meta: Meta): Promise<void> {
cache = meta; 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(); 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. * Performs the primitive database operation to fetch server configuration.
* If there is no entry yet, inserts a new one. * If there is no entry yet, inserts a new one.

View file

@ -260,9 +260,3 @@ export interface IRemoteUser extends User {
host: string; host: string;
token: null; token: null;
} }
export type CacheableLocalUser = ILocalUser;
export type CacheableRemoteUser = IRemoteUser;
export type CacheableUser = CacheableLocalUser | CacheableRemoteUser;

View file

@ -230,6 +230,36 @@ export const UserRepository = db.getRepository(User).extend({
return `${config.url}/identicon/${userId}`; 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<boolean> {
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<ExpectsMe extends boolean | null = null, D extends boolean = false>( async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
src: User['id'] | User, src: User['id'] | User,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
@ -270,15 +300,13 @@ export const UserRepository = db.getRepository(User).extend({
.getMany() : []; .getMany() : [];
const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null; const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null;
const followingCount = profile == null ? null : const ffVisible = await this.areFollowersVisibleTo(user, me);
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
(profile.ffVisibility === 'followers') && relation?.isFollowing ? user.followingCount :
null;
const followersCount = profile == null ? null : const followingCount = opts.detail ? null :
(profile.ffVisibility === 'public') || isMe ? user.followersCount : ffVisible ? user.followingCount : null;
(profile.ffVisibility === 'followers') && relation?.isFollowing ? user.followersCount :
null; const followersCount = opts.detail ? null :
ffVisible ? user.followersCount : null;
const packed = { const packed = {
id: user.id, id: user.id,

View file

@ -40,8 +40,8 @@ systemQueue
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) 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('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`))
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`))
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
deliverQueue deliverQueue
@ -49,31 +49,31 @@ deliverQueue
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .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('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('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}`)); .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
inboxQueue inboxQueue
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${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('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}`, { job, e: renderError(err) })) .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'}`)); .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
dbQueue dbQueue
.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) 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('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`))
.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`))
.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
objectStorageQueue objectStorageQueue
.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) 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('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`))
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`))
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
webhookDeliverQueue webhookDeliverQueue
@ -81,7 +81,7 @@ webhookDeliverQueue
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .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('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('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}`)); .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
export async function deliver(content: IActivity|IActivity[], to: string | null) { export async function deliver(content: IActivity|IActivity[], to: string | null) {

View file

@ -71,7 +71,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
try { try {
await downloadUrl(emoji.originalUrl, emojiPath); await downloadUrl(emoji.originalUrl, emojiPath);
downloaded = true; downloaded = true;
} catch (e) { // TODO: 何度か再試行 } catch (e) { // TODO: retry
logger.error(e instanceof Error ? e : new Error(e as string)); logger.error(e instanceof Error ? e : new Error(e as string));
} }

View file

@ -1,5 +1,5 @@
import promiseLimit from 'promise-limit'; 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 { unique, concat } from '@/prelude/array.js';
import { resolvePerson } from './models/person.js'; import { resolvePerson } from './models/person.js';
import { Resolver } from './resolver.js'; import { Resolver } from './resolver.js';
@ -9,20 +9,20 @@ type Visibility = 'public' | 'home' | 'followers' | 'specified';
type AudienceInfo = { type AudienceInfo = {
visibility: Visibility, visibility: Visibility,
mentionedUsers: CacheableUser[], mentionedUsers: User[],
visibleUsers: CacheableUser[], visibleUsers: User[],
}; };
export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> { export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
const toGroups = groupingAudience(getApIds(to), actor); const toGroups = groupingAudience(getApIds(to), actor);
const ccGroups = groupingAudience(getApIds(cc), actor); const ccGroups = groupingAudience(getApIds(cc), actor);
const others = unique(concat([toGroups.other, ccGroups.other])); const others = unique(concat([toGroups.other, ccGroups.other]));
const limit = promiseLimit<CacheableUser | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null))), 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) { if (toGroups.public.length > 0) {
return { 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 = { const groups = {
public: [] as string[], public: [] as string[],
followers: [] as string[], followers: [] as string[],
@ -85,7 +85,7 @@ function isPublic(id: string) {
].includes(id); ].includes(id);
} }
function isFollowers(id: string, actor: CacheableRemoteUser) { function isFollowers(id: string, actor: IRemoteUser) {
return ( return (
id === (actor.followersUri || `${actor.uri}/followers`) id === (actor.followersUri || `${actor.uri}/followers`)
); );

View file

@ -1,7 +1,7 @@
import escapeRegexp from 'escape-regexp'; import escapeRegexp from 'escape-regexp';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { Note } from '@/models/entities/note.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 { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, MessagingMessages } from '@/models/index.js'; import { Notes, MessagingMessages } from '@/models/index.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
@ -89,7 +89,7 @@ export class DbResolver {
/** /**
* AP Person => FoundKey User in DB * AP Person => FoundKey User in DB
*/ */
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> { public async getUserFromApId(value: string | IObject): Promise<User | null> {
const parsed = parseUri(value); const parsed = parseUri(value);
if (parsed.local) { if (parsed.local) {

View file

@ -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 { acceptFollowRequest } from '@/services/following/requests/accept.js';
import { relayAccepted } from '@/services/relay.js'; import { relayAccepted } from '@/services/relay.js';
import { IFollow } from '@/remote/activitypub/type.js'; import { IFollow } from '@/remote/activitypub/type.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // activity is a follow request started by this server, so activity.actor must be an existing local user.
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.actor); const follower = await dbResolver.getUserFromApId(activity.actor);

View file

@ -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 { apLogger } from '@/remote/activitypub/logger.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js'; import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js';
import acceptFollow from './follow.js'; import acceptFollow from './follow.js';
export default async (actor: CacheableRemoteUser, activity: IAccept, resolver: Resolver): Promise<string> => { export default async (actor: IRemoteUser, activity: IAccept, resolver: Resolver): Promise<string> => {
const uri = activity.id || activity; const uri = activity.id || activity;
apLogger.info(`Accept: ${uri}`); apLogger.info(`Accept: ${uri}`);

View file

@ -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 { addPinned } from '@/services/i/pin.js';
import { resolveNote } from '@/remote/activitypub/models/note.js'; import { resolveNote } from '@/remote/activitypub/models/note.js';
import { IAdd } from '@/remote/activitypub/type.js'; import { IAdd } from '@/remote/activitypub/type.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
export default async (actor: CacheableRemoteUser, activity: IAdd, resolver: Resolver): Promise<void> => { export default async (actor: IRemoteUser, activity: IAdd, resolver: Resolver): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -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 { apLogger } from '@/remote/activitypub/logger.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js';
import announceNote from './note.js'; import announceNote from './note.js';
export default async (actor: CacheableRemoteUser, activity: IAnnounce, resolver: Resolver): Promise<void> => { export default async (actor: IRemoteUser, activity: IAnnounce, resolver: Resolver): Promise<void> => {
const uri = getApId(activity); const uri = getApId(activity);
apLogger.info(`Announce: ${uri}`); apLogger.info(`Announce: ${uri}`);

View file

@ -1,5 +1,5 @@
import post from '@/services/note/create.js'; 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 { extractDbHost } from '@/misc/convert-host.js';
import { getApLock } from '@/misc/app-lock.js'; import { getApLock } from '@/misc/app-lock.js';
import { StatusError } from '@/misc/fetch.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 { IAnnounce, getApId } from '@/remote/activitypub/type.js';
import { shouldBlockInstance } from '@/misc/should-block-instance.js'; import { shouldBlockInstance } from '@/misc/should-block-instance.js';
export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> { export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
const uri = getApId(activity); const uri = getApId(activity);
if (actor.isSuspended) { if (actor.isSuspended) {

View file

@ -1,11 +1,11 @@
import block from '@/services/blocking/create.js'; 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 { Users } from '@/models/index.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js';
import { IBlock } from '@/remote/activitypub/type.js'; import { IBlock } from '@/remote/activitypub/type.js';
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => { export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず // There is a block target in activity.object, which should be a local user that exists.
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const blockee = await dbResolver.getUserFromApId(activity.object); const blockee = await dbResolver.getUserFromApId(activity.object);
@ -15,7 +15,7 @@ export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<str
} }
if (blockee.host != null) { if (blockee.host != null) {
return 'skip: ブロックしようとしているユーザーはローカルユーザーではありません'; return 'skip: blockee is not local';
} }
await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id })); await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));

View file

@ -1,11 +1,11 @@
import { CacheableRemoteUser } from '@/models/entities/user.js'; import { IRemoteUser } from '@/models/entities/user.js';
import { toArray, concat, unique } from '@/prelude/array.js'; import { toArray, concat, unique } from '@/prelude/array.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
import { ICreate, getApId, isPost, getApType } from '../../type.js'; import { ICreate, getApId, isPost, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import createNote from './note.js'; import createNote from './note.js';
export default async (actor: CacheableRemoteUser, activity: ICreate, resolver: Resolver): Promise<void> => { export default async (actor: IRemoteUser, activity: ICreate, resolver: Resolver): Promise<void> => {
const uri = getApId(activity); const uri = getApId(activity);
apLogger.info(`Create: ${uri}`); apLogger.info(`Create: ${uri}`);

View file

@ -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 { getApLock } from '@/misc/app-lock.js';
import { extractDbHost } from '@/misc/convert-host.js'; import { extractDbHost } from '@/misc/convert-host.js';
import { StatusError } from '@/misc/fetch.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<string> { export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false): Promise<string> {
const uri = getApId(note); const uri = getApId(note);
if (typeof note === 'object') { if (typeof note === 'object') {

View file

@ -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 { Users } from '@/models/index.js';
import { apLogger } from '@/remote/activitypub/logger.js'; import { apLogger } from '@/remote/activitypub/logger.js';
import { deleteAccount } from '@/services/delete-account.js'; import { deleteAccount } from '@/services/delete-account.js';
export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> { export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
apLogger.info(`Deleting the Actor: ${uri}`); apLogger.info(`Deleting the Actor: ${uri}`);
if (actor.uri !== uri) { if (actor.uri !== uri) {

View file

@ -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 { toSingle } from '@/prelude/array.js';
import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '@/remote/activitypub/type.js'; import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '@/remote/activitypub/type.js';
import { deleteActor } from './actor.js'; import { deleteActor } from './actor.js';
@ -7,7 +7,7 @@ import deleteNote from './note.js';
/** /**
* *
*/ */
export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<string> => { export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -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 { deleteNotes } from '@/services/note/delete.js';
import { getApLock } from '@/misc/app-lock.js'; import { getApLock } from '@/misc/app-lock.js';
import { deleteMessage } from '@/services/messages/delete.js'; import { deleteMessage } from '@/services/messages/delete.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js';
import { apLogger } from '@/remote/activitypub/logger.js'; import { apLogger } from '@/remote/activitypub/logger.js';
export default async function(actor: CacheableRemoteUser, uri: string): Promise<string> { export default async function(actor: IRemoteUser, uri: string): Promise<string> {
apLogger.info(`Deleting the Note: ${uri}`); apLogger.info(`Deleting the Note: ${uri}`);
const unlock = await getApLock(uri); const unlock = await getApLock(uri);

View file

@ -1,13 +1,14 @@
import { In } from 'typeorm'; import { In } from 'typeorm';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { genId } from '@/misc/gen-id.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 { AbuseUserReports, Users } from '@/models/index.js';
import { IFlag, getApIds } from '@/remote/activitypub/type.js'; import { IFlag, getApIds } from '@/remote/activitypub/type.js';
export default async (actor: CacheableRemoteUser, activity: IFlag): Promise<string> => { export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // 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 uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!); const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!);

View file

@ -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 follow from '@/services/following/create.js';
import { IFollow } from '../type.js'; import { IFollow } from '../type.js';
import { DbResolver } from '../db-resolver.js'; import { DbResolver } from '../db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object); const followee = await dbResolver.getUserFromApId(activity.object);

View file

@ -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 { toArray } from '@/prelude/array.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
import { extractDbHost } from '@/misc/convert-host.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 flag from './flag/index.js';
import { move } from './move/index.js'; import { move } from './move/index.js';
export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> { export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
if (isCollectionOrOrderedCollection(activity)) { if (isCollectionOrOrderedCollection(activity)) {
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item); 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<void> { async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
if (actor.isSuspended) return; if (actor.isSuspended) return;
if (typeof activity.id !== 'undefined') { if (typeof activity.id !== 'undefined') {

View file

@ -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 { createReaction } from '@/services/note/reaction/create.js';
import { ILike, getApId } from '../type.js'; import { ILike, getApId } from '../type.js';
import { fetchNote, extractEmojis } from '../models/note.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 targetUri = getApId(activity.object);
const note = await fetchNote(targetUri); const note = await fetchNote(targetUri);

View file

@ -1,12 +1,12 @@
import { IsNull } from 'typeorm'; 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 { resolvePerson } from '@/remote/activitypub/models/person.js';
import { Followings, Users } from '@/models/index.js'; import { Followings, Users } from '@/models/index.js';
import { createNotification } from '@/services/create-notification.js'; import { createNotification } from '@/services/create-notification.js';
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import { IMove, isActor, getApId } from '../../type.js'; import { IMove, isActor, getApId } from '../../type.js';
export async function move(actor: CacheableRemoteUser, activity: IMove, resolver: Resolver): Promise<void> { export async function move(actor: IRemoteUser, activity: IMove, resolver: Resolver): Promise<void> {
// actor is not move origin // actor is not move origin
if (activity.object == null || getApId(activity.object) !== actor.uri) return; if (activity.object == null || getApId(activity.object) !== actor.uri) return;

View file

@ -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 { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
import { MessagingMessages } from '@/models/index.js'; import { MessagingMessages } from '@/models/index.js';
import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js'; import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js';
import { IRead, getApId } from '../type.js'; import { IRead, getApId } from '../type.js';
export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise<string> => { export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
const id = await getApId(activity.object); const id = await getApId(activity.object);
if (!isSelfHost(extractDbHost(id))) { if (!isSelfHost(extractDbHost(id))) {

View file

@ -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 { remoteReject } from '@/services/following/reject.js';
import { relayRejected } from '@/services/relay.js'; import { relayRejected } from '@/services/relay.js';
import { Users } from '@/models/index.js'; import { Users } from '@/models/index.js';
import { IFollow } from '../../type.js'; import { IFollow } from '../../type.js';
import { DbResolver } from '../../db-resolver.js'; import { DbResolver } from '../../db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // activity is a follow request started by this server, so activity.actor must be an existing local user.
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.actor); const follower = await dbResolver.getUserFromApId(activity.actor);

View file

@ -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 { Resolver } from '@/remote/activitypub/resolver.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import { IReject, isFollow, getApType } from '../../type.js'; import { IReject, isFollow, getApType } from '../../type.js';
import rejectFollow from './follow.js'; import rejectFollow from './follow.js';
export default async (actor: CacheableRemoteUser, activity: IReject, resolver: Resolver): Promise<string> => { export default async (actor: IRemoteUser, activity: IReject, resolver: Resolver): Promise<string> => {
const uri = activity.id || activity; const uri = activity.id || activity;
apLogger.info(`Reject: ${uri}`); apLogger.info(`Reject: ${uri}`);

View file

@ -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 { removePinned } from '@/services/i/pin.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
import { IRemove } from '../../type.js'; import { IRemove } from '../../type.js';
import { resolveNote } from '../../models/note.js'; import { resolveNote } from '../../models/note.js';
export default async (actor: CacheableRemoteUser, activity: IRemove, resolver: Resolver): Promise<void> => { export default async (actor: IRemoteUser, activity: IRemove, resolver: Resolver): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -1,10 +1,10 @@
import unfollow from '@/services/following/delete.js'; 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 { Followings } from '@/models/index.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js';
import { IAccept } from '@/remote/activitypub/type.js'; import { IAccept } from '@/remote/activitypub/type.js';
export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => { export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object); const follower = await dbResolver.getUserFromApId(activity.object);

View file

@ -1,9 +1,9 @@
import { Notes } from '@/models/index.js'; 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 { deleteNotes } from '@/services/note/delete.js';
import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; import { IAnnounce, getApId } from '@/remote/activitypub/type.js';
export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<string> => { export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
const uri = getApId(activity); const uri = getApId(activity);
const note = await Notes.findOneBy({ const note = await Notes.findOneBy({

View file

@ -1,10 +1,10 @@
import unblock from '@/services/blocking/delete.js'; 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 { Users } from '@/models/index.js';
import { IBlock } from '@/remote/activitypub/type.js'; import { IBlock } from '@/remote/activitypub/type.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => { export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const blockee = await dbResolver.getUserFromApId(activity.object); const blockee = await dbResolver.getUserFromApId(activity.object);

View file

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js'; import unfollow from '@/services/following/delete.js';
import { cancelFollowRequest } from '@/services/following/requests/cancel.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 { FollowRequests, Followings } from '@/models/index.js';
import { IFollow } from '@/remote/activitypub/type.js'; import { IFollow } from '@/remote/activitypub/type.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js'; import { DbResolver } from '@/remote/activitypub/db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object); const followee = await dbResolver.getUserFromApId(activity.object);

View file

@ -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 { apLogger } from '@/remote/activitypub/logger.js';
import { Resolver } from '@/remote/activitypub/resolver.js'; import { Resolver } from '@/remote/activitypub/resolver.js';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '@/remote/activitypub/type.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 undoAccept from './accept.js';
import { undoAnnounce } from './announce.js'; import { undoAnnounce } from './announce.js';
export default async (actor: CacheableRemoteUser, activity: IUndo, resolver: Resolver): Promise<string> => { export default async (actor: IRemoteUser, activity: IUndo, resolver: Resolver): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -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 { deleteReaction } from '@/services/note/reaction/delete.js';
import { ILike, getApId } from '@/remote/activitypub/type.js'; import { ILike, getApId } from '@/remote/activitypub/type.js';
import { fetchNote } from '@/remote/activitypub/models/note.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 * Process Undo.Like activity
*/ */
export default async (actor: CacheableRemoteUser, activity: ILike) => { export default async (actor: IRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object); const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri); const note = await fetchNote(targetUri);

View file

@ -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 { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js';
import { apLogger } from '@/remote/activitypub/logger.js'; import { apLogger } from '@/remote/activitypub/logger.js';
import { updateQuestion } from '@/remote/activitypub/models/question.js'; import { updateQuestion } from '@/remote/activitypub/models/question.js';
@ -8,7 +8,7 @@ import { updatePerson } from '@/remote/activitypub/models/person.js';
/** /**
* Updateアクティビティを捌きます * Updateアクティビティを捌きます
*/ */
export default async (actor: CacheableRemoteUser, activity: IUpdate, resolver: Resolver): Promise<string> => { export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
return 'skip: invalid actor'; return 'skip: invalid actor';
} }

View file

@ -1,12 +1,12 @@
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { UserPublickeys } from '@/models/index.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 { UserPublickey } from '@/models/entities/user-publickey.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { createPerson } from '@/remote/activitypub/models/person.js'; import { createPerson } from '@/remote/activitypub/models/person.js';
export type AuthUser = { export type AuthUser = {
user: CacheableRemoteUser; user: IRemoteUser;
key: UserPublickey; key: UserPublickey;
}; };

View file

@ -1,5 +1,5 @@
import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; 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 { fetchMeta } from '@/misc/fetch-meta.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles } from '@/models/index.js'; import { DriveFiles } from '@/models/index.js';
@ -11,7 +11,7 @@ import { apLogger } from '../logger.js';
/** /**
* Imageを作成します * Imageを作成します
*/ */
export async function createImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise<DriveFile> { export async function createImage(actor: IRemoteUser, value: any, resolver: Resolver): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); 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. * 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. * 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<DriveFile> { export async function resolveImage(actor: IRemoteUser, value: any, resolver: Resolver): Promise<DriveFile> {
// TODO // TODO
// Fetch from remote server and register it. // Fetch from remote server and register it.

View file

@ -1,17 +1,17 @@
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import { toArray, unique } from '@/prelude/array.js'; 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 { Resolver } from '@/remote/activitypub/resolver.js';
import { IObject, isMention, IApMention } from '../type.js'; import { IObject, isMention, IApMention } from '../type.js';
import { resolvePerson } from './person.js'; import { resolvePerson } from './person.js';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<CacheableUser[]> { export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
const limit = promiseLimit<CacheableUser | null>(2); const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), 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; return mentionedUsers;
} }

View file

@ -2,7 +2,7 @@ import promiseLimit from 'promise-limit';
import config from '@/config/index.js'; import config from '@/config/index.js';
import post from '@/services/note/create.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 { unique, toArray, toSingle } from '@/prelude/array.js';
import { vote } from '@/services/note/polls/vote.js'; import { vote } from '@/services/note/polls/vote.js';
import { DriveFile } from '@/models/entities/drive-file.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); const err = validateNote(object);
if (err) { if (err) {
apLogger.error(`${err.message}`, { apLogger.error(`${err.message}`);
resolver: {
history: resolver.getHistory(),
},
value,
object,
});
throw new Error('invalid note'); 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}`); 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) { if (actor.isSuspended) {

View file

@ -6,7 +6,7 @@ import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instanc
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { updateUsertags } from '@/services/update-hashtag.js'; import { updateUsertags } from '@/services/update-hashtag.js';
import { Users, Instances, Followings, UserProfiles, UserPublickeys } from '@/models/index.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 { Emoji } from '@/models/entities/emoji.js';
import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { UserNotePining } from '@/models/entities/user-note-pining.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
@ -121,7 +121,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
* *
* If the target Person is registered in FoundKey, it is returned. * If the target Person is registered in FoundKey, it is returned.
*/ */
export async function fetchPerson(uri: string): Promise<CacheableUser | null> { export async function fetchPerson(uri: string): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = uriPersonCache.get(uri); const cached = uriPersonCache.get(uri);
@ -217,7 +217,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
} catch (e) { } catch (e) {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { 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({ const u = await Users.findOneBy({
uri: person.id, 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. * 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. * 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<CacheableUser> { export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise<User> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す

View file

@ -1,11 +1,11 @@
import { DAY } from '@/const.js'; 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 { Resolver } from '@/remote/activitypub/resolver.js';
import { IObject } from './type.js'; import { IObject } from './type.js';
import { performActivity } from './kernel/index.js'; import { performActivity } from './kernel/index.js';
import { updatePerson } from './models/person.js'; import { updatePerson } from './models/person.js';
export async function perform(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> { export async function perform(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
await performActivity(actor, activity, resolver); await performActivity(actor, activity, resolver);
// And while I'm at it, I'll update the remote user information if it's out of date. // And while I'm at it, I'll update the remote user information if it's out of date.

View file

@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.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 { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js'; import { setResponseType } from '../activitypub.js';
@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => {
return; return;
} }
//#region Check ff visibility const ffVisible = await Users.areFollowersVisibleTo(user, null);
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); if (!ffVisible) {
if (profile.ffVisibility === 'private') {
ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
ctx.status = 403; ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30'); ctx.set('Cache-Control', 'public, max-age=30');
return; return;
} }
//#endregion
const limit = 10; const limit = 10;
const partOf = `${config.url}/users/${userId}/followers`; const partOf = `${config.url}/users/${userId}/followers`;

View file

@ -6,7 +6,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.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 { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js'; import { setResponseType } from '../activitypub.js';
@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => {
return; return;
} }
//#region Check ff visibility const ffVisible = await Users.areFollowersVisibleTo(user, null);
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); if (!ffVisible) {
if (profile.ffVisibility === 'private') {
ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
ctx.status = 403; ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30'); ctx.set('Cache-Control', 'public, max-age=30');
return; return;
} }
//#endregion
const limit = 10; const limit = 10;
const partOf = `${config.url}/users/${userId}/following`; const partOf = `${config.url}/users/${userId}/following`;

View file

@ -1,7 +1,7 @@
import Koa from 'koa'; import Koa from 'koa';
import { IEndpoint } from './endpoints.js'; import { IEndpoint } from './endpoints.js';
import authenticate, { AuthenticationError } from './authenticate.js'; import { authenticate, AuthenticationError } from './authenticate.js';
import call from './call.js'; import call from './call.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';

View file

@ -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 { Users, AccessTokens } from '@/models/index.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.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; let maybeToken: string | null = null;
// check if there is an authorization header set // check if there is an authorization header set
@ -66,4 +66,4 @@ export default async (authorization: string | null | undefined, bodyToken: strin
return [user, accessToken]; return [user, accessToken];
} }
}; }

View file

@ -1,14 +1,14 @@
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import Koa from 'koa'; 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 { AccessToken } from '@/models/entities/access-token.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import { limiter } from './limiter.js'; import { limiter } from './limiter.js';
import endpoints, { IEndpointMeta } from './endpoints.js'; import { endpoints, IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import { apiLogger } from './logger.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 isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin); 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) { if (e instanceof ApiError) {
throw e; throw e;
} else { } else {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { 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,
},
});
throw new ApiError('INTERNAL_ERROR', { throw new ApiError('INTERNAL_ERROR', {
e: { e: {
message: e.message, message: e.message,

View file

@ -1,6 +1,6 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import Ajv from 'ajv'; 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 { Schema, SchemaType } from '@/misc/schema.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import { IEndpointMeta } from './endpoints.js'; import { IEndpointMeta } from './endpoints.js';
@ -10,7 +10,7 @@ export type Response = Record<string, any> | void;
// TODO: paramsの型をT['params']のスキーマ定義から推論する // TODO: paramsの型をT['params']のスキーマ定義から推論する
type executor<T extends IEndpointMeta, Ps extends Schema> = type executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => (params: SchemaType<Ps>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
const ajv = new Ajv({ const ajv = new Ajv({
@ -20,10 +20,10 @@ const ajv = new Ajv({
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>) export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> { : (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
const validate = ajv.compile(paramDef); 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() { function cleanup() {
fs.unlink(file.path, () => {}); fs.unlink(file.path, () => {});
} }

View file

@ -713,7 +713,7 @@ export interface IEndpoint {
params: Schema; params: Schema;
} }
const endpoints: IEndpoint[] = eps.map(([name, ep]) => { export const endpoints: IEndpoint[] = eps.map(([name, ep]) => {
return { return {
name, name,
exec: ep.default, exec: ep.default,
@ -721,5 +721,3 @@ const endpoints: IEndpoint[] = eps.map(([name, ep]) => {
params: ep.paramDef, params: ep.paramDef,
}; };
}); });
export default endpoints;

View file

@ -5,7 +5,7 @@ import { Resolver } from '@/remote/activitypub/resolver.js';
import { extractDbHost } from '@/misc/convert-host.js'; import { extractDbHost } from '@/misc/convert-host.js';
import { Users, Notes } from '@/models/index.js'; import { Users, Notes } from '@/models/index.js';
import { Note } from '@/models/entities/note.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 { isActor, isPost } from '@/remote/activitypub/type.js';
import { SchemaType } from '@/misc/schema.js'; import { SchemaType } from '@/misc/schema.js';
import { HOUR } from '@/const.js'; import { HOUR } from '@/const.js';
@ -85,7 +85,7 @@ export default define(meta, paramDef, async (ps, me) => {
/*** /***
* URIからUserかNoteを解決する * URIからUserかNoteを解決する
*/ */
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { async function fetchAny(uri: string, me: ILocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// Stop if the host is blocked. // Stop if the host is blocked.
const host = extractDbHost(uri); const host = extractDbHost(uri);
if (await shouldBlockInstance(host)) { 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<SchemaType<typeof meta.res> | null> { async function mergePack(me: ILocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
if (user != null) { if (user != null) {
return { return {
type: 'User', type: 'User',

View file

@ -1,5 +1,5 @@
import define from '@/server/api/define.js'; import define from '@/server/api/define.js';
import endpoints from '@/server/api/endpoints.js'; import { endpoints } from '@/server/api/endpoints.js';
export const meta = { export const meta = {
requireCredential: false, requireCredential: false,

View file

@ -1,5 +1,5 @@
import define from '@/server/api/define.js'; import define from '@/server/api/define.js';
import endpoints from '@/server/api/endpoints.js'; import { endpoints } from '@/server/api/endpoints.js';
export const meta = { export const meta = {
requireCredential: false, requireCredential: false,

View file

@ -1,6 +1,6 @@
import RE2 from 're2'; import RE2 from 're2';
import * as mfm from 'mfm-js'; 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 { publishMainStream, publishUserEvent } from '@/services/stream.js';
import { acceptAllFollowRequests } from '@/services/following/requests/accept-all.js'; import { acceptAllFollowRequests } from '@/services/following/requests/accept-all.js';
import { publishToFollowers } from '@/services/i/update.js'; import { publishToFollowers } from '@/services/i/update.js';
@ -67,7 +67,7 @@ export const paramDef = {
injectFeaturedNote: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, ffVisibility: { type: 'string', enum: ffVisibility },
pinnedPageId: { type: 'array', items: { pinnedPageId: { type: 'array', items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },

View file

@ -1,5 +1,5 @@
import { IsNull } from 'typeorm'; 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 { toPunyNullable } from '@/misc/convert-host.js';
import define from '@/server/api/define.js'; import define from '@/server/api/define.js';
import { ApiError } from '@/server/api/error.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'); if (user == null) throw new ApiError('NO_SUCH_USER');
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const ffVisible = await Users.areFollowersVisibleTo(user, me);
if (!ffVisible) throw new ApiError('ACCESS_DENIED');
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 query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
.andWhere('following.followeeId = :userId', { userId: user.id }) .andWhere('following.followeeId = :userId', { userId: user.id })

View file

@ -1,5 +1,5 @@
import { IsNull } from 'typeorm'; 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 { toPunyNullable } from '@/misc/convert-host.js';
import define from '@/server/api/define.js'; import define from '@/server/api/define.js';
import { ApiError } from '@/server/api/error.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'); if (user == null) throw new ApiError('NO_SUCH_USER');
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const ffVisible = await Users.areFollowersVisibleTo(user, me);
if (!ffVisible) throw new ApiError('ACCESS_DENIED');
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 query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
.andWhere('following.followerId = :userId', { userId: user.id }) .andWhere('following.followerId = :userId', { userId: user.id })

View file

@ -46,27 +46,27 @@ export const meta = {
}, },
localFollowingCount: { localFollowingCount: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: true, nullable: false,
}, },
remoteFollowingCount: { remoteFollowingCount: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: true, nullable: false,
}, },
localFollowersCount: { localFollowersCount: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: true, nullable: false,
}, },
remoteFollowersCount: { remoteFollowersCount: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: true, nullable: false,
}, },
followingCount: { followingCount: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: true, nullable: false,
}, },
followersCount: { followersCount: {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: true, nullable: false,
}, },
sentReactionsCount: { sentReactionsCount: {
type: 'integer', type: 'integer',
@ -110,7 +110,7 @@ export const paramDef = {
} as const; } as const;
// eslint-disable-next-line import/no-default-export // 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 }); const user = await Users.findOneBy({ id: ps.userId });
if (user == null) { if (user == null) {
throw new ApiError('NO_SUCH_USER'); throw new ApiError('NO_SUCH_USER');
@ -141,22 +141,6 @@ export default define(meta, paramDef, async (ps) => {
.innerJoin('vote.note', 'note') .innerJoin('vote.note', 'note')
.where('note.userId = :userId', { userId: user.id }) .where('note.userId = :userId', { userId: user.id })
.getCount(), .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') sentReactionsCount: NoteReactions.createQueryBuilder('reaction')
.where('reaction.userId = :userId', { userId: user.id }) .where('reaction.userId = :userId', { userId: user.id })
.getCount(), .getCount(),
@ -180,8 +164,32 @@ export default define(meta, paramDef, async (ps) => {
driveUsage: DriveFiles.calcDriveUsageOf(user.id), driveUsage: DriveFiles.calcDriveUsageOf(user.id),
}); });
result.followingCount = result.localFollowingCount + result.remoteFollowingCount; const ffVisible = await Users.areFollowersVisibleTo(user, me);
result.followersCount = result.localFollowersCount + result.remoteFollowersCount; 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; return result;
}); });

View file

@ -10,7 +10,7 @@ import cors from '@koa/cors';
import { Instances, AccessTokens, Users } from '@/models/index.js'; import { Instances, AccessTokens, Users } from '@/models/index.js';
import config from '@/config/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 { handler } from './api-handler.js';
import signup from './private/signup.js'; import signup from './private/signup.js';
import signin from './private/signin.js'; import signin from './private/signin.js';

View file

@ -2,7 +2,7 @@ import config from '@/config/index.js';
import { kinds } from '@/misc/api-permissions.js'; import { kinds } from '@/misc/api-permissions.js';
import { I18n } from '@/misc/i18n.js'; import { I18n } from '@/misc/i18n.js';
import { errors as errorDefinitions } from '@/server/api/error.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 { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
import { httpCodes } from './http-codes.js'; import { httpCodes } from './http-codes.js';

View file

@ -6,7 +6,7 @@ import { SECOND, MINUTE } from '@/const.js';
import { subscriber as redisClient } from '@/db/redis.js'; import { subscriber as redisClient } from '@/db/redis.js';
import { Users } from '@/models/index.js'; import { Users } from '@/models/index.js';
import { Connection } from './stream/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 => { export const initializeStreamingServer = (server: http.Server): void => {
// Init websocket server // Init websocket server

View file

@ -8,7 +8,7 @@ import { dirname } from 'node:path';
import Koa from 'koa'; import Koa from 'koa';
import cors from '@koa/cors'; import cors from '@koa/cors';
import Router from '@koa/router'; 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 _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);

View file

@ -27,8 +27,7 @@ const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void =>
ctx.set('Cache-Control', 'max-age=300'); ctx.set('Cache-Control', 'max-age=300');
}; };
// eslint-disable-next-line import/no-default-export export async function sendDriveFile(ctx: Koa.Context) {
export default async function(ctx: Koa.Context) {
const key = ctx.params.key; const key = ctx.params.key;
// Fetch drive file // Fetch drive file
@ -49,7 +48,7 @@ export default async function(ctx: Koa.Context) {
const isWebpublic = file.webpublicAccessKey === key; const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) { if (!file.storedInternal) {
if (file.isLink && file.uri) { // 期限切れリモートファイル if (file.isLink && file.uri) { // expired remote file
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();
try { try {

View file

@ -4,7 +4,7 @@ import config from '@/config/index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js'; import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js';
export default async function(user: User) { export async function packFeed(user: User) {
const author = { const author = {
link: `${config.url}/@${user.username}`, link: `${config.url}/@${user.username}`,
name: user.name || user.username, name: user.name || user.username,

View file

@ -26,7 +26,7 @@ import { MINUTE, DAY } from '@/const.js';
import { genOpenapiSpec } from '../api/openapi/gen-spec.js'; import { genOpenapiSpec } from '../api/openapi/gen-spec.js';
import { urlPreviewHandler } from './url-preview.js'; import { urlPreviewHandler } from './url-preview.js';
import { manifestHandler } from './manifest.js'; import { manifestHandler } from './manifest.js';
import packFeed from './feed.js'; import { packFeed } from './feed.js';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);

View file

@ -2,13 +2,13 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import { renderBlock } from '@/remote/activitypub/renderer/block.js'; import { renderBlock } from '@/remote/activitypub/renderer/block.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js';
import { deliver } from '@/queue/index.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 { Blockings, Users } from '@/models/index.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('blocking/delete'); 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({ const blocking = await Blockings.findOneBy({
blockerId: blocker.id, blockerId: blocker.id,
blockeeId: blockee.id, blockeeId: blockee.id,

View file

@ -60,10 +60,7 @@ export async function uploadFromUrl({
logger.succ(`Got: ${driveFile.id}`); logger.succ(`Got: ${driveFile.id}`);
return driveFile; return driveFile;
} catch (e) { } catch (e) {
logger.error(`Failed to create drive file: ${e}`, { logger.error(`Failed to create drive file: ${e}`);
url,
e,
});
throw e; throw e;
} finally { } finally {
cleanup(); cleanup();

View file

@ -2,7 +2,6 @@ import cluster from 'node:cluster';
import chalk from 'chalk'; import chalk from 'chalk';
import convertColor from 'color-convert'; import convertColor from 'color-convert';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import * as SyslogPro from 'syslog-pro';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { envOption } from '@/env.js'; import { envOption } from '@/env.js';
import type { KEYWORD } from 'color-convert/conversions.js'; import type { KEYWORD } from 'color-convert/conversions.js';
@ -12,16 +11,26 @@ type Domain = {
color?: KEYWORD; 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 { export default class Logger {
private domain: Domain; private domain: Domain;
private parentLogger: Logger | null = null; private parentLogger: Logger | null = null;
private store: boolean; private store: boolean;
private syslogClient: SyslogPro.RFC5424 | null = null; /**
* Messages below this level will be discarded.
*/
private minLevel: Level;
/** /**
* Create a logger instance. * Create a logger instance.
@ -29,26 +38,13 @@ export default class Logger {
* @param color Log message color * @param color Log message color
* @param store Whether to store messages * @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 = { this.domain = {
name: domain, name: domain,
color, color,
}; };
this.store = store; this.store = store;
this.minLevel = minLevel;
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,
},
});
}
} }
/** /**
@ -58,69 +54,90 @@ export default class Logger {
* @param store Whether to store messages * @param store Whether to store messages
* @returns A Logger instance whose parent logger is this instance. * @returns A Logger instance whose parent logger is this instance.
*/ */
public createSubLogger(domain: string, color?: KEYWORD, store = true): Logger { public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info): Logger {
const logger = new Logger(domain, color, store); const logger = new Logger(domain, color, store, minLevel);
logger.parentLogger = this; logger.parentLogger = this;
return logger; return logger;
} }
private log(level: Level, message: string, data?: Record<string, any> | 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; 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) { 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; return;
} }
const time = dateFormat(new Date(), 'HH:mm:ss'); const time = dateFormat(new Date(), 'HH:mm:ss');
const worker = cluster.isPrimary ? '*' : cluster.worker?.id; 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 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; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : 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. * Log an error message.
* Use in situations where execution cannot be continued. * Use in situations where execution cannot be continued.
* @param err Error or string containing an error message * @param err Error or string containing an error message
* @param data Data relating to the error
* @param important Whether this error is important * @param important Whether this error is important
*/ */
public error(err: string | Error, data: Record<string, any> = {}, important = false): void { public error(err: string | Error, important = false): void {
if (err instanceof Error) { if (err instanceof Error) {
data.e = err; this.log(LEVELS.error, err.toString(), important);
this.log('error', err.toString(), data, important);
} else if (typeof err === 'object') { } 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 { } 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. * Log a warning message.
* Use in situations where execution can continue but needs to be improved. * Use in situations where execution can continue but needs to be improved.
* @param message Warning message * @param message Warning message
* @param data Data relating to the warning
* @param important Whether this warning is important * @param important Whether this warning is important
*/ */
public warn(message: string, data?: Record<string, any> | null, important = false): void { public warn(message: string, important = false): void {
this.log('warning', message, data, important); this.log(LEVELS.warning, message, important);
} }
/** /**
* Log a success message. * Log a success message.
* Use in situations where something has been successfully done. * Use in situations where something has been successfully done.
* @param message Success message * @param message Success message
* @param data Data relating to the success
* @param important Whether this success message is important * @param important Whether this success message is important
*/ */
public succ(message: string, data?: Record<string, any> | null, important = false): void { public succ(message: string, important = false): void {
this.log('success', message, data, important); this.log(LEVELS.success, message, important);
} }
/** /**
* Log a debug message. * Log a debug message.
* Use for debugging (information needed by developers but not required by users). * Use for debugging (information needed by developers but not required by users).
* @param message Debug message * @param message Debug message
* @param data Data relating to the debug message
* @param important Whether this debug message is important * @param important Whether this debug message is important
*/ */
public debug(message: string, data?: Record<string, any> | null, important = false): void { public debug(message: string, important = false): void {
if (process.env.NODE_ENV !== 'production' || envOption.verbose) { this.log(LEVELS.debug, message, important);
this.log('debug', message, data, important);
}
} }
/** /**
* Log an informational message. * Log an informational message.
* Use when something needs to be logged but doesn't fit into other levels. * Use when something needs to be logged but doesn't fit into other levels.
* @param message Info message * @param message Info message
* @param data Data relating to the info message
* @param important Whether this info message is important * @param important Whether this info message is important
*/ */
public info(message: string, data?: Record<string, any> | null, important = false): void { public info(message: string, important = false): void {
this.log('info', message, data, important); this.log(LEVELS.info, message, important);
} }
} }

View file

@ -1,5 +1,5 @@
import { Not } from 'typeorm'; 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 { UserGroup } from '@/models/entities/user-group.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.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 { renderActivity } from '@/remote/activitypub/renderer/index.js';
import { deliver } from '@/queue/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 = { const message = {
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),

View file

@ -1,12 +1,12 @@
import { ArrayOverlap, Not } from 'typeorm'; import { ArrayOverlap, Not } from 'typeorm';
import { publishNoteStream } from '@/services/stream.js'; 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 { Note } from '@/models/entities/note.js';
import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@/models/index.js'; import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { createNotification } from '@/services/create-notification.js'; import { createNotification } from '@/services/create-notification.js';
export async function vote(user: CacheableUser, note: Note, choice: number): Promise<void> { export async function vote(user: User, note: Note, choice: number): Promise<void> {
const poll = await Polls.findOneBy({ noteId: note.id }); const poll = await Polls.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('poll not found'); if (poll == null) throw new Error('poll not found');

View file

@ -95,9 +95,7 @@ class Publisher {
}; };
} }
const publisher = new Publisher(); export const publisher = new Publisher();
export default publisher;
export const publishInternalEvent = publisher.publishInternalEvent; export const publishInternalEvent = publisher.publishInternalEvent;
export const publishUserEvent = publisher.publishUserEvent; export const publishUserEvent = publisher.publishUserEvent;

View file

@ -1,5 +1,5 @@
import { IsNull } from 'typeorm'; 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 { Users } from '@/models/index.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { subscriber } from '@/db/redis.js'; import { subscriber } from '@/db/redis.js';
@ -8,7 +8,7 @@ export const userByIdCache = new Cache<User>(
Infinity, Infinity,
async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined, async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined,
); );
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser>( export const localUserByNativeTokenCache = new Cache<ILocalUser>(
Infinity, Infinity,
async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: false }) as ILocalUser | null ?? undefined, async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: false }) as ILocalUser | null ?? undefined,
); );

View file

@ -8,6 +8,7 @@ describe('API visibility', () => {
let p: childProcess.ChildProcess; let p: childProcess.ChildProcess;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
}); });
@ -17,15 +18,15 @@ describe('API visibility', () => {
describe('Note visibility', async () => { describe('Note visibility', async () => {
//#region vars //#region vars
/** ヒロイン */ /** protagonist */
let alice: any; let alice: any;
/** フォロワー */ /** follower */
let follower: any; let follower: any;
/** 非フォロワー */ /** non-follower */
let other: any; let other: any;
/** 非フォロワーでもリプライやメンションをされた人 */ /** non-follower who has been replied to or mentioned */
let target: any; let target: any;
/** specified mentionでmentionを飛ばされる人 */ /** actor for which a specified visibility was set */
let target2: any; let target2: any;
/** public-post */ /** public-post */
@ -100,90 +101,90 @@ describe('API visibility', () => {
//#region show post //#region show post
// public // 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); const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pub.id, other);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pub.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
})); }));
// home // 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); const res = await show(home.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(home.id, follower);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(home.id, other);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(home.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
})); }));
// followers // 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); const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(fol.id, other);
assert.strictEqual(res.status, 404); 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); const res = await show(fol.id, null);
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
})); }));
// specified // 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); 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); const res = await show(spe.id, target);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(spe.id, follower);
assert.strictEqual(res.status, 404); 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); const res = await show(spe.id, other);
assert.strictEqual(res.status, 404); 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); const res = await show(spe.id, null);
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
})); }));
@ -191,110 +192,105 @@ describe('API visibility', () => {
//#region show reply //#region show reply
// public // 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); const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
})); }));
// home // 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); const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, 'x'); assert.strictEqual(res.body.text, 'x');
})); }));
// followers // 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); const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(folR.id, target);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(folR.id, other);
assert.strictEqual(res.status, 404); 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); const res = await show(folR.id, null);
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
})); }));
// specified // 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); const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, 'x'); 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); const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x'); 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, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをフォロワーが見れない', async(async () => {
const res = await show(speR.id, follower); const res = await show(speR.id, follower);
assert.strictEqual(res.status, 404); 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); const res = await show(speR.id, other);
assert.strictEqual(res.status, 404); 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); const res = await show(speR.id, null);
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
})); }));
@ -302,131 +298,131 @@ describe('API visibility', () => {
//#region show mention //#region show mention
// public // 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); const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
})); }));
// home // 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); const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, '@target x'); assert.strictEqual(res.body.text, '@target x');
})); }));
// followers // 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); const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(folM.id, target);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, '@target x'); 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); const res = await show(folM.id, other);
assert.strictEqual(res.status, 404); 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); const res = await show(folM.id, null);
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
})); }));
// specified // 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); const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, '@target2 x'); 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); const res = await show(speM.id, target);
assert.strictEqual(res.body.text, '@target2 x'); 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); const res = await show(speM.id, target2);
assert.strictEqual(res.status, 404); 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); const res = await show(speM.id, follower);
assert.strictEqual(res.status, 404); 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); const res = await show(speM.id, other);
assert.strictEqual(res.status, 404); 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); const res = await show(speM.id, null);
assert.strictEqual(res.status, 404); assert.strictEqual(res.status, 404);
})); }));
//#endregion //#endregion
//#region HTL //#region Home Timeline
it('[HTL] public-post が 自分が見れる', async(async () => { it('[TL] public post on author home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice); const res = await request('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id); const notes = res.body.filter((n: any) => n.id == pub.id);
assert.strictEqual(notes[0].text, 'x'); 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); const res = await request('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id); const notes = res.body.filter((n: any) => n.id == pub.id);
assert.strictEqual(notes.length, 0); 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); const res = await request('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == fol.id); const notes = res.body.filter((n: any) => n.id == fol.id);
@ -434,22 +430,22 @@ describe('API visibility', () => {
})); }));
//#endregion //#endregion
//#region RTL //#region replies timeline
it('[replies] followers-reply が フォロワーから見れる', async(async () => { it('[TL] followers reply on follower reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id); const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x'); 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); const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id); const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes.length, 0); 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); const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id); const notes = res.body.filter((n: any) => n.id == folR.id);
@ -458,14 +454,14 @@ describe('API visibility', () => {
//#endregion //#endregion
//#region MTL //#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); const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id); const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x'); 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); const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folM.id); const notes = res.body.filter((n: any) => n.id == folM.id);

View file

@ -10,7 +10,8 @@ describe('API', () => {
let bob: any; let bob: any;
let carol: any; let carol: any;
before(async () => { before(async function() {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });

View file

@ -13,6 +13,8 @@ describe('Block', () => {
let carol: any; let carol: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
@ -23,7 +25,7 @@ describe('Block', () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('Block作成', async(async () => { it('can block someone', async(async () => {
const res = await request('/blocking/create', { const res = await request('/blocking/create', {
userId: bob.id, userId: bob.id,
}, alice); }, alice);
@ -31,45 +33,45 @@ describe('Block', () => {
assert.strictEqual(res.status, 200); 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); const res = await request('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400); 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 note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400); 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 note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); 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 note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400); 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 aliceNote = await post(alice);
const bobNote = await post(bob); const bobNote = await post(bob);
const carolNote = await post(carol); const carolNote = await post(carol);

View file

@ -23,6 +23,7 @@ describe('Fetch resource', () => {
let alicesPost: any; let alicesPost: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
alicesPost = await post(alice, { alicesPost = await post(alice, {

View file

@ -9,159 +9,110 @@ describe('FF visibility', () => {
let alice: any; let alice: any;
let bob: any; let bob: any;
let carol: any; let follower: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); 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 () => { after(async () => {
await shutdownServer(p); await shutdownServer(p);
}); });
it('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async(async () => { const visible = (user) => {
await request('/i/update', { return async () => {
ffVisibility: 'public', const followingRes = await request('/users/following', {
}, alice); userId: alice.id,
}, user);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, user);
const followingRes = await request('/users/following', { assert.strictEqual(followingRes.status, 200);
userId: alice.id, assert.ok(Array.isArray(followingRes.body));
}, bob); assert.strictEqual(followersRes.status, 200);
const followersRes = await request('/users/followers', { assert.ok(Array.isArray(followersRes.body));
userId: alice.id, };
}, bob); };
assert.strictEqual(followingRes.status, 200); const hidden = (user) => {
assert.strictEqual(Array.isArray(followingRes.body), true); return async () => {
assert.strictEqual(followersRes.status, 200); const followingRes = await request('/users/following', {
assert.strictEqual(Array.isArray(followersRes.body), true); userId: alice.id,
})); }, user);
const followersRes = await request('/users/followers', {
userId: alice.id,
}, user);
it('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async(async () => { assert.strictEqual(followingRes.status, 403);
await request('/i/update', { assert.strictEqual(followersRes.status, 403);
ffVisibility: 'followers', };
}, alice); };
const followingRes = await request('/users/following', { describe('public visibility', () => {
userId: alice.id, before(async () => {
}, alice); await request('/i/update', {
const followersRes = await request('/users/followers', { ffVisibility: 'public',
userId: alice.id, }, alice);
}, alice); });
assert.strictEqual(followingRes.status, 200); it('shows followers and following to self', visible(alice));
assert.strictEqual(Array.isArray(followingRes.body), true); it('shows followers and following to a follower', visible(follower));
assert.strictEqual(followersRes.status, 200); it('shows followers and following to a non-follower', visible(bob));
assert.strictEqual(Array.isArray(followersRes.body), true); it('shows followers and following when unauthenticated', visible(null));
}));
it('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async(async () => { it('provides followers in ActivityPub representation', async () => {
await request('/i/update', { const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
ffVisibility: 'followers', const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
}, alice); assert.strictEqual(followingRes.status, 200);
assert.strictEqual(followersRes.status, 200);
});
});
const followingRes = await request('/users/following', { describe('followers visibility', () => {
userId: alice.id, before(async () => {
}, bob); await request('/i/update', {
const followersRes = await request('/users/followers', { ffVisibility: 'followers',
userId: alice.id, }, alice);
}, bob); });
assert.strictEqual(followingRes.status, 400); it('shows followers and following to self', visible(alice));
assert.strictEqual(followersRes.status, 400); 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 () => { it('hides followers from ActivityPub representation', async () => {
await request('/i/update', { const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
ffVisibility: 'followers', const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
}, alice); assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
});
});
await request('/following/create', { describe('private visibility', () => {
userId: alice.id, before(async () => {
}, bob); await request('/i/update', {
ffVisibility: 'private',
}, alice);
});
const followingRes = await request('/users/following', { it('shows followers and following to self', visible(alice));
userId: alice.id, it('hides followers and following from a follower', hidden(follower));
}, bob); it('hides followers and following from a non-follower', hidden(bob));
const followersRes = await request('/users/followers', { it('hides followers and following when unauthenticated', hidden(null));
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200); it('hides followers from ActivityPub representation', async () => {
assert.strictEqual(Array.isArray(followingRes.body), true); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
assert.strictEqual(followersRes.status, 200); const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode }));
assert.strictEqual(Array.isArray(followersRes.body), true); assert.strictEqual(followingRes.status, 403);
})); assert.strictEqual(followersRes.status, 403);
});
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);
}
}));
}); });
}); });

View file

@ -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'; import { IObject } from '../../src/remote/activitypub/type.js';
type MockResponse = { type MockResponse = {

View file

@ -13,6 +13,8 @@ describe('Mute', () => {
let carol: any; let carol: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });

View file

@ -13,6 +13,8 @@ describe('Note', () => {
let bob: any; let bob: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
const connection = await initTestDb(true); const connection = await initTestDb(true);
Notes = connection.getRepository(Note); Notes = connection.getRepository(Note);
@ -158,7 +160,7 @@ describe('Note', () => {
replyId: '000000000000000000000000', replyId: '000000000000000000000000',
}; };
const res = await request('/notes/create', post, alice); const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 404);
})); }));
it('存在しないrenote対象で怒られる', async(async () => { it('存在しないrenote対象で怒られる', async(async () => {
@ -166,7 +168,7 @@ describe('Note', () => {
renoteId: '000000000000000000000000', renoteId: '000000000000000000000000',
}; };
const res = await request('/notes/create', post, alice); const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 404);
})); }));
it('不正なリプライ先IDで怒られる', async(async () => { it('不正なリプライ先IDで怒られる', async(async () => {
@ -175,7 +177,7 @@ describe('Note', () => {
replyId: 'foo', replyId: 'foo',
}; };
const res = await request('/notes/create', post, alice); const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 404);
})); }));
it('不正なrenote対象IDで怒られる', async(async () => { it('不正なrenote対象IDで怒られる', async(async () => {
@ -183,7 +185,7 @@ describe('Note', () => {
renoteId: 'foo', renoteId: 'foo',
}; };
const res = await request('/notes/create', post, alice); const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 404);
})); }));
it('存在しないユーザーにメンションできる', async(async () => { it('存在しないユーザーにメンションできる', async(async () => {
@ -286,7 +288,7 @@ describe('Note', () => {
choice: 2, choice: 2,
}, alice); }, alice);
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 409);
})); }));
it('許可されている場合は複数投票できる', async(async () => { it('許可されている場合は複数投票できる', async(async () => {

View file

@ -14,6 +14,8 @@ describe('Creating a block activity', () => {
let carol: any; let carol: any;
before(async () => { before(async () => {
this.timeout(0);
await initTestDb(); await initTestDb();
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });

View file

@ -38,6 +38,8 @@ describe('Streaming', () => {
let list: any; let list: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
const connection = await initTestDb(true); const connection = await initTestDb(true);
Followings = connection.getRepository(Following); Followings = connection.getRepository(Following);

View file

@ -12,6 +12,8 @@ describe('Note thread mute', () => {
let carol: any; let carol: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });

View file

@ -13,6 +13,8 @@ describe('users/notes', () => {
let jpgPngNote: any; let jpgPngNote: any;
before(async () => { before(async () => {
this.timeout(0);
p = await startServer(); p = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');

View file

@ -31,16 +31,15 @@ export const async = (fn: Function) => (done: Function) => {
export const api = async (endpoint: string, params: any, me?: any) => { export const api = async (endpoint: string, params: any, me?: any) => {
endpoint = endpoint.replace(/^\//, ''); endpoint = endpoint.replace(/^\//, '');
const auth = me ? { const auth = me ? { authorization: `Bearer ${me.token}` } : {};
i: me.token
} : {};
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, { const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
...auth,
}, },
body: JSON.stringify(Object.assign(auth, params)), body: JSON.stringify(params),
retry: { retry: {
limit: 0, 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 }> => { export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
const auth = me ? { const auth = me ? { authorization: `Bearer ${me.token}` } : {};
i: me.token,
} : {};
const res = await fetch(`http://localhost:${port}/api${endpoint}`, { const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...auth,
}, },
body: JSON.stringify(Object.assign(auth, params)), body: JSON.stringify(params),
}); });
const status = res.status; const status = res.status;

View file

@ -3,9 +3,9 @@ import * as foundkey from 'foundkey-js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { del, get, set } from '@/scripts/idb-proxy'; import { del, get, set } from '@/scripts/idb-proxy';
import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os'; import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { MenuItem } from '@/types/menu';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -22,7 +22,7 @@ export async function signout() {
waiting(); waiting();
localStorage.removeItem('account'); localStorage.removeItem('account');
await removeAccount($i.id); if ($i) await removeAccount($i!.id);
const accounts = await getAccounts(); const accounts = await getAccounts();
@ -99,14 +99,18 @@ function fetchAccount(token: string): Promise<Account> {
} }
export function updateAccount(accountData) { export function updateAccount(accountData) {
if (!$i) return;
for (const [key, value] of Object.entries(accountData)) { 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() { 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) { export async function login(token: Account['token'], redirect?: string) {
@ -134,7 +138,39 @@ export async function openAccountMenu(opts: {
active?: foundkey.entities.UserDetailed['id']; active?: foundkey.entities.UserDetailed['id'];
onChoose?: (account: foundkey.entities.UserDetailed) => void; onChoose?: (account: foundkey.entities.UserDetailed) => void;
}, ev: MouseEvent) { }, 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<MenuItem[]> = 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')), {}, { popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
done: res => { done: res => {
addAccount(res.id, res.i); addAccount(res.id, res.i);
@ -143,50 +179,14 @@ export async function openAccountMenu(opts: {
}, 'closed'); }, 'closed');
} }
function createAccount() { const createAccount = () => {
popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, { popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
done: res => { done: res => {
addAccount(res.id, res.i); addAccount(res.id, res.i);
switchAccountWithToken(res.i); login(res.i);
}, },
}, 'closed'); }, '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) { if (opts.withExtraOperation) {
popupMenu([...[{ popupMenu([...[{
@ -194,16 +194,16 @@ export async function openAccountMenu(opts: {
text: i18n.ts.profile, text: i18n.ts.profile,
to: `/@${ $i.username }`, to: `/@${ $i.username }`,
avatar: $i, avatar: $i,
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { }, null, ...(opts.includeCurrentAccount && $i ? [createItem($i)] : []), ...accountItemPromises, {
icon: 'fas fa-plus', icon: 'fas fa-plus',
text: i18n.ts.addAccount, text: i18n.ts.addAccount,
action: () => { action: () => {
popupMenu([{ popupMenu([{
text: i18n.ts.existingAccount, text: i18n.ts.existingAccount,
action: () => { showSigninDialog(); }, action: showSigninDialog,
}, { }, {
text: i18n.ts.createAccount, text: i18n.ts.createAccount,
action: () => { createAccount(); }, action: createAccount,
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
}, },
}, { }, {
@ -211,11 +211,11 @@ export async function openAccountMenu(opts: {
icon: 'fas fa-users', icon: 'fas fa-users',
text: i18n.ts.manageAccounts, text: i18n.ts.manageAccounts,
to: '/settings/accounts', to: '/settings/accounts',
}]], ev.currentTarget ?? ev.target, { }]], ev.currentTarget ?? ev.target ?? undefined, {
align: 'left', align: 'left',
}); });
} else { } 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', align: 'left',
}); });
} }

Some files were not shown because too many files have changed in this diff Show more