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
#inboxJobMaxAttempts: 8
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS outgoing connections
#proxy: http://127.0.0.1:3128

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.
This works just like attaching files to a note:
You can choose to upload a new file, pick a file from your Misskey drive or upload a file from another URL.
You can choose to upload a new file, pick a file from your Foundkey drive or upload a file from another URL.
::: danger
**Warning:**
When you import emoji from your drive, the file will remain inside your drive.
Misskey does not make a copy of this file so if you delete it, the emoji will be broken.
:::
Foundkey does not make a copy of this file so if you delete it, the emoji will be broken.
The emoji will be added to the instance and you will then be able to edit or delete it as usual.
@ -32,10 +31,9 @@ The emoji will be added to the instance and you will then be able to edit or del
Emojis can be imported in bulk as packed ZIP files with a special format.
This ability can be found in the three dots menu in the top right corner of the custom emoji menu.
::: warning
**Warning:**
Bulk emoji import may overwrite existing emoji or otherwise mess up your instance.
Be sure to only import emoji from trusted sources, ideally only ones you exported yourself.
:::
### Packed emoji format
@ -89,10 +87,9 @@ The properties of an emoji can be edited by clicking it in the list of local emo
When you click on a custom emoji, a dialog for editing the properties will open.
This dialog will also allow you to delete an emoji.
::: danger
**Warning:**
When you delete a custom emoji, old notes that contain it will still have the text name of the emoji in it.
The emoji will no longer be rendered correctly.
:::
Note that remote emoji can not be edited or deleted.

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)"
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
for i in $(seq 1 $NUM_MIGRAIONS); do
for i in $(seq 1 $NUM_MIGRATIONS); do
npx typeorm migration:revert -d ormconfig.js
done
```

View file

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

View file

@ -854,6 +854,18 @@ _mfm:
spin: Animación (Spin)
shakeDescription: Brinda al contenido una animación temblorosa.
inlineMath: Función matemática (Inline)
rainbow: Arcoíris
x4Description: Muestra el contenido de la manera más grandemente posible.
blurDescription: Muestra borroso el contenido. Se mostrará con claridad cuando se
cubra.
spinDescription: Da al contenido una animación de girar.
x2: Grande
x2Description: Muestra en grande el contenido.
x3Description: Muestra más grande el contenido.
x4: Increíblemente grande
blur: Borroso
fontDescription: Agrega la fuente para mostrar contenido.
x3: Muy grande
_instanceTicker:
none: "No mostrar"
remote: "Mostrar a usuarios remotos"
@ -1318,3 +1330,9 @@ unlikeConfirm: ¿En verdad quieres remover tu like?
breakFollow: Quitar seguidor
reporter: Reportero
continueThread: Ver la continuación del hilo
uploadFailedSize: El archivo es muy grande para subirse.
uploadFailed: Subida fallida
uploadFailedDescription: No se pudo subir el archivo.
movedTo: Este usuario se ha movido a {handle}.
attachedToNotes: Notas del archivo
showAttachedNotes: Mostrar notas del archivo

View file

@ -842,8 +842,8 @@ _ffVisibility:
private: "비공개"
_signup:
almostThere: "거의 다 끝났습니다"
emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다."
emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요."
emailAddressInfo: "당신이 사용하고 있는 이메일 주소를 입력해 주세요. 이메일 주소는 다른 유저에게 공개되지 않습니다."
emailSent: "입력하신 메일 주소({email})로 확인 메일을 보내드렸습니다. 가입을 완료하시려면 보내드린 메일에 있는 링크로 접속해 주세요."
_accountDelete:
accountDelete: "계정 삭제"
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",
"mfm-js": "0.22.1",
"mime-types": "2.1.35",
"mocha": "10.0.0",
"mocha": "10.2.0",
"multer": "1.4.5-lts.1",
"nested-property": "4.0.0",
"node-fetch": "3.2.6",
@ -100,7 +100,6 @@
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.7.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.22",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
@ -158,7 +157,6 @@
"@types/sinon": "^10.0.13",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/syslog-pro": "^1.0.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.4",

View file

@ -8,7 +8,7 @@ import chalkTemplate from 'chalk-template';
import semver from 'semver';
import Logger from '@/services/logger.js';
import loadConfig from '@/config/load.js';
import { loadConfig } from '@/config/load.js';
import { Config } from '@/config/types.js';
import { showMachineInfo } from '@/misc/show-machine-info.js';
import { envOption } from '@/env.js';
@ -41,7 +41,7 @@ function greet(): void {
}
bootLogger.info('Welcome to FoundKey!');
bootLogger.info(`FoundKey v${meta.version}`, null, true);
bootLogger.info(`FoundKey v${meta.version}`, true);
}
/**
@ -59,7 +59,7 @@ export async function masterMain(): Promise<void> {
config = loadConfigBoot();
await connectDb();
} catch (e) {
bootLogger.error('Fatal error occurred during initialization', {}, true);
bootLogger.error('Fatal error occurred during initialization', true);
process.exit(1);
}
@ -69,7 +69,7 @@ export async function masterMain(): Promise<void> {
await spawnWorkers(config.clusterLimits);
}
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true);
if (!envOption.noDaemons) {
import('../daemons/server-stats.js').then(x => x.serverStats());
@ -84,7 +84,7 @@ function showEnvironment(): void {
if (env !== 'production') {
logger.warn('The environment is not in production mode.');
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', {}, true);
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', true);
}
}
@ -109,7 +109,7 @@ function loadConfigBoot(): Config {
} catch (exception) {
const e = exception as Partial<NodeJS.ErrnoException> | Error;
if ('code' in e && e.code === 'ENOENT') {
configLogger.error('Configuration file not found', {}, true);
configLogger.error('Configuration file not found', true);
process.exit(1);
} else if (e instanceof Error) {
configLogger.error(e.message);
@ -133,7 +133,7 @@ async function connectDb(): Promise<void> {
const v = await db.query('SHOW server_version').then(x => x[0].server_version);
dbLogger.succ(`Connected: v${v}`);
} catch (e) {
dbLogger.error('Cannot connect', {}, true);
dbLogger.error('Cannot connect', true);
dbLogger.error(e as Error | string);
process.exit(1);
}
@ -160,12 +160,24 @@ function spawnWorker(mode: 'web' | 'queue'): Promise<void> {
return new Promise(res => {
const worker = cluster.fork({ mode });
worker.on('message', message => {
if (message === 'listenFailed') {
bootLogger.error('The server Listen failed due to the previous error.');
process.exit(1);
switch (message) {
case 'listenFailed':
bootLogger.error('The server Listen failed due to the previous error.');
process.exit(1);
break;
case 'ready':
res();
break;
case 'metaUpdate':
// forward new instance metadata to all workers
for (const otherWorker of Object.values(cluster.workers)) {
// don't forward the message to the worker that sent it
if (worker.id === otherWorker.id) continue;
otherWorker.send(message);
}
break;
}
if (message !== 'ready') return;
res();
});
});
}

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}/default.yml`;
export default function load(): Config {
export function loadConfig(): Config {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8'));
let config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;

View file

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

View file

@ -1,3 +1,4 @@
import process from 'node:process';
import push from 'web-push';
import { db } from '@/db/postgre.js';
import { Meta } from '@/models/entities/meta.js';
@ -17,9 +18,20 @@ export async function setMeta(meta: Meta): Promise<void> {
cache = meta;
/*
The meta is not included here because another process may have updated
the content before the other process receives it.
*/
process.send!('metaUpdated');
unlock();
}
// the primary will forward this message
process.on('message', async message => {
if (message === 'metaUpdated') await getMeta();
});
/**
* Performs the primitive database operation to fetch server configuration.
* If there is no entry yet, inserts a new one.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import escapeRegexp from 'escape-regexp';
import config from '@/config/index.js';
import { Note } from '@/models/entities/note.js';
import { CacheableUser } from '@/models/entities/user.js';
import { User } from '@/models/entities/user.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, MessagingMessages } from '@/models/index.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
@ -89,7 +89,7 @@ export class DbResolver {
/**
* AP Person => FoundKey User in DB
*/
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
public async getUserFromApId(value: string | IObject): Promise<User | null> {
const parsed = parseUri(value);
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 { relayAccepted } from '@/services/relay.js';
import { IFollow } from '@/remote/activitypub/type.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// activity is a follow request started by this server, so activity.actor must be an existing local user.
const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.actor);

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 { Resolver } from '@/remote/activitypub/resolver.js';
import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js';
import acceptFollow from './follow.js';
export default async (actor: CacheableRemoteUser, activity: IAccept, resolver: Resolver): Promise<string> => {
export default async (actor: IRemoteUser, activity: IAccept, resolver: Resolver): Promise<string> => {
const uri = activity.id || activity;
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 { resolveNote } from '@/remote/activitypub/models/note.js';
import { IAdd } from '@/remote/activitypub/type.js';
import { Resolver } from '@/remote/activitypub/resolver.js';
export default async (actor: CacheableRemoteUser, activity: IAdd, resolver: Resolver): Promise<void> => {
export default async (actor: IRemoteUser, activity: IAdd, resolver: Resolver): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.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 { Resolver } from '@/remote/activitypub/resolver.js';
import { IAnnounce, getApId } from '@/remote/activitypub/type.js';
import announceNote from './note.js';
export default async (actor: CacheableRemoteUser, activity: IAnnounce, resolver: Resolver): Promise<void> => {
export default async (actor: IRemoteUser, activity: IAnnounce, resolver: Resolver): Promise<void> => {
const uri = getApId(activity);
apLogger.info(`Announce: ${uri}`);

View file

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

View file

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

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 { Resolver } from '@/remote/activitypub/resolver.js';
import { ICreate, getApId, isPost, getApType } from '../../type.js';
import { apLogger } from '../../logger.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);
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 { extractDbHost } from '@/misc/convert-host.js';
import { StatusError } from '@/misc/fetch.js';
@ -9,7 +9,7 @@ import { getApId, IObject } from '@/remote/activitypub/type.js';
/**
* 稿
*/
export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false): Promise<string> {
export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false): Promise<string> {
const uri = getApId(note);
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 { apLogger } from '@/remote/activitypub/logger.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}`);
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 { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '@/remote/activitypub/type.js';
import { deleteActor } from './actor.js';
@ -7,7 +7,7 @@ import deleteNote from './note.js';
/**
*
*/
export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<string> => {
export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.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 { getApLock } from '@/misc/app-lock.js';
import { deleteMessage } from '@/services/messages/delete.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
import { apLogger } from '@/remote/activitypub/logger.js';
export default async function(actor: CacheableRemoteUser, uri: string): Promise<string> {
export default async function(actor: IRemoteUser, uri: string): Promise<string> {
apLogger.info(`Deleting the Note: ${uri}`);
const unlock = await getApLock(uri);

View file

@ -1,13 +1,14 @@
import { In } from 'typeorm';
import config from '@/config/index.js';
import { genId } from '@/misc/gen-id.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { AbuseUserReports, Users } from '@/models/index.js';
import { IFlag, getApIds } from '@/remote/activitypub/type.js';
export default async (actor: CacheableRemoteUser, activity: IFlag): Promise<string> => {
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
// The object is `(User|Note) | (User|Note)[]`, but since the database schema
// cannot be made to handle every possible case, the target user is the first user
// and everything else is stored by URL.
const uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!);

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 { IFollow } from '../type.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 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 { Resolver } from '@/remote/activitypub/resolver.js';
import { extractDbHost } from '@/misc/convert-host.js';
@ -21,7 +21,7 @@ import block from './block/index.js';
import flag from './flag/index.js';
import { move } from './move/index.js';
export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
if (isCollectionOrOrderedCollection(activity)) {
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
const act = await resolver.resolve(item);
@ -38,7 +38,7 @@ export async function performActivity(actor: CacheableRemoteUser, activity: IObj
}
}
async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
if (actor.isSuspended) return;
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 { ILike, getApId } from '../type.js';
import { fetchNote, extractEmojis } from '../models/note.js';
export default async (actor: CacheableRemoteUser, activity: ILike) => {
export default async (actor: IRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri);

View file

@ -1,12 +1,12 @@
import { IsNull } from 'typeorm';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { resolvePerson } from '@/remote/activitypub/models/person.js';
import { Followings, Users } from '@/models/index.js';
import { createNotification } from '@/services/create-notification.js';
import Resolver from '../../resolver.js';
import { IMove, isActor, getApId } from '../../type.js';
export async function move(actor: CacheableRemoteUser, activity: IMove, resolver: Resolver): Promise<void> {
export async function move(actor: IRemoteUser, activity: IMove, resolver: Resolver): Promise<void> {
// actor is not move origin
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 { MessagingMessages } from '@/models/index.js';
import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js';
import { IRead, getApId } from '../type.js';
export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise<string> => {
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
const id = await getApId(activity.object);
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 { relayRejected } from '@/services/relay.js';
import { Users } from '@/models/index.js';
import { IFollow } from '../../type.js';
import { DbResolver } from '../../db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// activity is a follow request started by this server, so activity.actor must be an existing local user.
const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.actor);

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 { apLogger } from '../../logger.js';
import { IReject, isFollow, getApType } from '../../type.js';
import rejectFollow from './follow.js';
export default async (actor: CacheableRemoteUser, activity: IReject, resolver: Resolver): Promise<string> => {
export default async (actor: IRemoteUser, activity: IReject, resolver: Resolver): Promise<string> => {
const uri = activity.id || activity;
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 { Resolver } from '@/remote/activitypub/resolver.js';
import { IRemove } from '../../type.js';
import { resolveNote } from '../../models/note.js';
export default async (actor: CacheableRemoteUser, activity: IRemove, resolver: Resolver): Promise<void> => {
export default async (actor: IRemoteUser, activity: IRemove, resolver: Resolver): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js';
import { cancelFollowRequest } from '@/services/following/requests/cancel.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { FollowRequests, Followings } from '@/models/index.js';
import { IFollow } from '@/remote/activitypub/type.js';
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver();
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 { Resolver } from '@/remote/activitypub/resolver.js';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '@/remote/activitypub/type.js';
@ -8,7 +8,7 @@ import undoLike from './like.js';
import undoAccept from './accept.js';
import { undoAnnounce } from './announce.js';
export default async (actor: CacheableRemoteUser, activity: IUndo, resolver: Resolver): Promise<string> => {
export default async (actor: IRemoteUser, activity: IUndo, resolver: Resolver): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.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 { ILike, getApId } from '@/remote/activitypub/type.js';
import { fetchNote } from '@/remote/activitypub/models/note.js';
@ -6,7 +6,7 @@ import { fetchNote } from '@/remote/activitypub/models/note.js';
/**
* Process Undo.Like activity
*/
export default async (actor: CacheableRemoteUser, activity: ILike) => {
export default async (actor: IRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri);

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import promiseLimit from 'promise-limit';
import config from '@/config/index.js';
import post from '@/services/note/create.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { unique, toArray, toSingle } from '@/prelude/array.js';
import { vote } from '@/services/note/polls/vote.js';
import { DriveFile } from '@/models/entities/drive-file.js';
@ -74,13 +74,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
const err = validateNote(object);
if (err) {
apLogger.error(`${err.message}`, {
resolver: {
history: resolver.getHistory(),
},
value,
object,
});
apLogger.error(`${err.message}`);
throw new Error('invalid note');
}
@ -91,7 +85,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
apLogger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser;
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {

View file

@ -6,7 +6,7 @@ import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instanc
import { Note } from '@/models/entities/note.js';
import { updateUsertags } from '@/services/update-hashtag.js';
import { Users, Instances, Followings, UserProfiles, UserPublickeys } from '@/models/index.js';
import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js';
import { User, IRemoteUser, User } from '@/models/entities/user.js';
import { Emoji } from '@/models/entities/emoji.js';
import { UserNotePining } from '@/models/entities/user-note-pining.js';
import { genId } from '@/misc/gen-id.js';
@ -121,7 +121,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
*
* 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');
const cached = uriPersonCache.get(uri);
@ -217,7 +217,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
// Fix an error when the input is an alias like /users/@a -> /users/:id
const u = await Users.findOneBy({
uri: person.id,
});
@ -394,7 +394,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
* If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it.
* Fetch the person from the remote server, register it in FoundKey, and return it.
*/
export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise<CacheableUser> {
export async function resolvePerson(uri: string, resolver: Resolver, hint?: IObject): Promise<User> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す

View file

@ -1,11 +1,11 @@
import { DAY } from '@/const.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { Resolver } from '@/remote/activitypub/resolver.js';
import { IObject } from './type.js';
import { performActivity } from './kernel/index.js';
import { updatePerson } from './models/person.js';
export async function perform(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
export async function perform(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
await performActivity(actor, activity, resolver);
// 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 renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { Users, Followings } from '@/models/index.js';
import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js';
@ -31,19 +31,12 @@ export default async (ctx: Router.RouterContext) => {
return;
}
//#region Check ff visibility
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
const ffVisible = await Users.areFollowersVisibleTo(user, null);
if (!ffVisible) {
ctx.status = 403;
ctx.set('Cache-Control', 'public, max-age=30');
return;
}
//#endregion
const limit = 10;
const partOf = `${config.url}/users/${userId}/followers`;

View file

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

View file

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

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

View file

@ -1,14 +1,14 @@
import { performance } from 'perf_hooks';
import Koa from 'koa';
import { CacheableLocalUser } from '@/models/entities/user.js';
import { ILocalUser } from '@/models/entities/user.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { limiter } from './limiter.js';
import endpoints, { IEndpointMeta } from './endpoints.js';
import { endpoints, IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { apiLogger } from './logger.js';
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
export default async (endpoint: string, user: ILocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin);
@ -82,15 +82,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
if (e instanceof ApiError) {
throw e;
} else {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, {
ep: ep.name,
ps: data,
e: {
message: e.message,
code: e.name,
stack: e.stack,
},
});
apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`);
throw new ApiError('INTERNAL_ERROR', {
e: {
message: e.message,

View file

@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import Ajv from 'ajv';
import { CacheableLocalUser } from '@/models/entities/user.js';
import { ILocalUser } from '@/models/entities/user.js';
import { Schema, SchemaType } from '@/misc/schema.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { IEndpointMeta } from './endpoints.js';
@ -10,7 +10,7 @@ export type Response = Record<string, any> | void;
// TODO: paramsの型をT['params']のスキーマ定義から推論する
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']>>>;
const ajv = new Ajv({
@ -20,10 +20,10 @@ const ajv = new Ajv({
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>)
: (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);
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => {
function cleanup() {
fs.unlink(file.path, () => {});
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { IsNull } from 'typeorm';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { Users, Followings } from '@/models/index.js';
import { toPunyNullable } from '@/misc/convert-host.js';
import define from '@/server/api/define.js';
import { ApiError } from '@/server/api/error.js';
@ -61,25 +61,8 @@ export default define(meta, paramDef, async (ps, me) => {
if (user == null) throw new ApiError('NO_SUCH_USER');
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError('ACCESS_DENIED');
}
} else if (profile.ffVisibility === 'followers') {
if (me == null) {
throw new ApiError('ACCESS_DENIED');
} else if (me.id !== user.id) {
const following = await Followings.countBy({
followeeId: user.id,
followerId: me.id,
});
if (!following) {
throw new ApiError('ACCESS_DENIED');
}
}
}
const ffVisible = await Users.areFollowersVisibleTo(user, me);
if (!ffVisible) throw new ApiError('ACCESS_DENIED');
const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
.andWhere('following.followeeId = :userId', { userId: user.id })

View file

@ -1,5 +1,5 @@
import { IsNull } from 'typeorm';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { Users, Followings } from '@/models/index.js';
import { toPunyNullable } from '@/misc/convert-host.js';
import define from '@/server/api/define.js';
import { ApiError } from '@/server/api/error.js';
@ -61,25 +61,8 @@ export default define(meta, paramDef, async (ps, me) => {
if (user == null) throw new ApiError('NO_SUCH_USER');
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError('ACCESS_DENIED');
}
} else if (profile.ffVisibility === 'followers') {
if (me == null) {
throw new ApiError('ACCESS_DENIED');
} else if (me.id !== user.id) {
const following = await Followings.countBy({
followeeId: user.id,
followerId: me.id,
});
if (!following) {
throw new ApiError('ACCESS_DENIED');
}
}
}
const ffVisible = await Users.areFollowersVisibleTo(user, me);
if (!ffVisible) throw new ApiError('ACCESS_DENIED');
const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
.andWhere('following.followerId = :userId', { userId: user.id })

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import { dirname } from 'node:path';
import Koa from 'koa';
import cors from '@koa/cors';
import Router from '@koa/router';
import sendDriveFile from './send-drive-file.js';
import { sendDriveFile } from './send-drive-file.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);

View file

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

View file

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

View file

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

View file

@ -2,13 +2,13 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import { renderBlock } from '@/remote/activitypub/renderer/block.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js';
import { deliver } from '@/queue/index.js';
import { CacheableUser } from '@/models/entities/user.js';
import { User } from '@/models/entities/user.js';
import { Blockings, Users } from '@/models/index.js';
import Logger from '../logger.js';
const logger = new Logger('blocking/delete');
export default async function(blocker: CacheableUser, blockee: CacheableUser) {
export default async function(blocker: User, blockee: User) {
const blocking = await Blockings.findOneBy({
blockerId: blocker.id,
blockeeId: blockee.id,

View file

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

View file

@ -2,7 +2,6 @@ import cluster from 'node:cluster';
import chalk from 'chalk';
import convertColor from 'color-convert';
import { format as dateFormat } from 'date-fns';
import * as SyslogPro from 'syslog-pro';
import config from '@/config/index.js';
import { envOption } from '@/env.js';
import type { KEYWORD } from 'color-convert/conversions.js';
@ -12,16 +11,26 @@ type Domain = {
color?: KEYWORD;
};
type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
export const LEVELS = {
error: 0,
warning: 1,
success: 2,
info: 3,
debug: 4,
};
export type Level = LEVELS[keyof LEVELS];
/**
* Class that facilitates recording log messages to the console and optionally a syslog server.
* Class that facilitates recording log messages to the console.
*/
export default class Logger {
private domain: Domain;
private parentLogger: Logger | null = null;
private store: boolean;
private syslogClient: SyslogPro.RFC5424 | null = null;
/**
* Messages below this level will be discarded.
*/
private minLevel: Level;
/**
* Create a logger instance.
@ -29,26 +38,13 @@ export default class Logger {
* @param color Log message color
* @param store Whether to store messages
*/
constructor(domain: string, color?: KEYWORD, store = true) {
constructor(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info) {
this.domain = {
name: domain,
color,
};
this.store = store;
if (config.syslog) {
this.syslogClient = new SyslogPro.RFC5424({
applicationName: 'FoundKey',
timestamp: true,
includeStructuredData: true,
color: true,
extendedColor: true,
server: {
target: config.syslog.host,
port: config.syslog.port,
},
});
}
this.minLevel = minLevel;
}
/**
@ -58,69 +54,90 @@ export default class Logger {
* @param store Whether to store messages
* @returns A Logger instance whose parent logger is this instance.
*/
public createSubLogger(domain: string, color?: KEYWORD, store = true): Logger {
const logger = new Logger(domain, color, store);
public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info): Logger {
const logger = new Logger(domain, color, store, minLevel);
logger.parentLogger = this;
return logger;
}
private log(level: Level, message: string, data?: Record<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;
const store = _store && this.store && (level !== 'debug');
const store = _store && this.store;
// Check against the configured log level.
if (level < this.minLevel) return;
// If this logger has a parent logger, delegate the actual logging to it,
// so the parent domain(s) will be logged properly.
if (this.parentLogger) {
this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store);
this.parentLogger.log(level, message, important, [this.domain].concat(subDomains), store);
return;
}
const time = dateFormat(new Date(), 'HH:mm:ss');
const worker = cluster.isPrimary ? '*' : cluster.worker?.id;
const l =
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
level === 'warning' ? chalk.yellow('WARN') :
level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') :
level === 'debug' ? chalk.gray('VERB') :
chalk.blue('INFO');
const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.rgb(...convertColor.keyword.rgb(d.color))(d.name) : chalk.white(d.name));
const m =
level === 'error' ? chalk.red(message) :
level === 'warning' ? chalk.yellow(message) :
level === 'success' ? chalk.green(message) :
level === 'debug' ? chalk.gray(message) :
message;
let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`;
let levelDisplay;
let messageDisplay;
switch (level) {
case LEVELS.error:
if (important) {
levelDisplay = chalk.bgRed.white('ERR ');
} else {
levelDisplay = chalk.red('ERR ');
}
messageDisplay = chalk.red(message);
break;
case LEVELS.warning:
levelDisplay = chalk.yellow('WARN');
messageDisplay = chalk.yellow(message);
break;
case LEVELS.success:
if (important) {
levelDisplay = chalk.bgGreen.white('DONE');
} else {
levelDisplay = chalk.green('DONE');
}
messageDisplay = chalk.green(message);
break;
case LEVELS.info:
levelDisplay = chalk.blue('INFO');
messageDisplay = message;
break;
case LEVELS.debug: default:
levelDisplay = chalk.gray('VERB');
messageDisplay = chalk.gray(message);
break;
}
let log = `${levelDisplay} ${worker}\t[${domains.join(' ')}]\t${messageDisplay}`;
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log);
if (store) {
if (this.syslogClient) {
const send =
level === 'error' ? this.syslogClient.error :
level === 'warning' ? this.syslogClient.warning :
this.syslogClient.info;
send.bind(this.syslogClient)(message).catch(() => {});
}
}
}
/**
* Log an error message.
* Use in situations where execution cannot be continued.
* @param err Error or string containing an error message
* @param data Data relating to the error
* @param important Whether this error is important
*/
public error(err: string | Error, data: Record<string, any> = {}, important = false): void {
public error(err: string | Error, important = false): void {
if (err instanceof Error) {
data.e = err;
this.log('error', err.toString(), data, important);
this.log(LEVELS.error, err.toString(), important);
} else if (typeof err === 'object') {
this.log('error', `${(err as any).message || (err as any).name || err}`, data, important);
this.log(LEVELS.error, `${(err as any).message || (err as any).name || err}`, important);
} else {
this.log('error', `${err}`, data, important);
this.log(LEVELS.error, `${err}`, important);
}
}
@ -128,45 +145,39 @@ export default class Logger {
* Log a warning message.
* Use in situations where execution can continue but needs to be improved.
* @param message Warning message
* @param data Data relating to the warning
* @param important Whether this warning is important
*/
public warn(message: string, data?: Record<string, any> | null, important = false): void {
this.log('warning', message, data, important);
public warn(message: string, important = false): void {
this.log(LEVELS.warning, message, important);
}
/**
* Log a success message.
* Use in situations where something has been successfully done.
* @param message Success message
* @param data Data relating to the success
* @param important Whether this success message is important
*/
public succ(message: string, data?: Record<string, any> | null, important = false): void {
this.log('success', message, data, important);
public succ(message: string, important = false): void {
this.log(LEVELS.success, message, important);
}
/**
* Log a debug message.
* Use for debugging (information needed by developers but not required by users).
* @param message Debug message
* @param data Data relating to the debug message
* @param important Whether this debug message is important
*/
public debug(message: string, data?: Record<string, any> | null, important = false): void {
if (process.env.NODE_ENV !== 'production' || envOption.verbose) {
this.log('debug', message, data, important);
}
public debug(message: string, important = false): void {
this.log(LEVELS.debug, message, important);
}
/**
* Log an informational message.
* Use when something needs to be logged but doesn't fit into other levels.
* @param message Info message
* @param data Data relating to the info message
* @param important Whether this info message is important
*/
public info(message: string, data?: Record<string, any> | null, important = false): void {
this.log('info', message, data, important);
public info(message: string, important = false): void {
this.log(LEVELS.info, message, important);
}
}

View file

@ -1,5 +1,5 @@
import { Not } from 'typeorm';
import { CacheableUser, User } from '@/models/entities/user.js';
import { User } from '@/models/entities/user.js';
import { UserGroup } from '@/models/entities/user-group.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js';
@ -13,7 +13,7 @@ import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import { deliver } from '@/queue/index.js';
export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
const message = {
id: genId(),
createdAt: new Date(),

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { IsNull } from 'typeorm';
import { CacheableLocalUser, ILocalUser, User } from '@/models/entities/user.js';
import { ILocalUser, User } from '@/models/entities/user.js';
import { Users } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { subscriber } from '@/db/redis.js';
@ -8,7 +8,7 @@ export const userByIdCache = new Cache<User>(
Infinity,
async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined,
);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser>(
export const localUserByNativeTokenCache = new Cache<ILocalUser>(
Infinity,
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;
before(async () => {
this.timeout(0);
p = await startServer();
});
@ -17,15 +18,15 @@ describe('API visibility', () => {
describe('Note visibility', async () => {
//#region vars
/** ヒロイン */
/** protagonist */
let alice: any;
/** フォロワー */
/** follower */
let follower: any;
/** 非フォロワー */
/** non-follower */
let other: any;
/** 非フォロワーでもリプライやメンションをされた人 */
/** non-follower who has been replied to or mentioned */
let target: any;
/** specified mentionでmentionを飛ばされる人 */
/** actor for which a specified visibility was set */
let target2: any;
/** public-post */
@ -100,90 +101,90 @@ describe('API visibility', () => {
//#region show post
// public
it('[show] public-postを自分が見れる', async(async () => {
it('[show] public post can be seen by author', async(async () => {
const res = await show(pub.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postをフォロワーが見れる', async(async () => {
it('[show] public post can be seen by follower', async(async () => {
const res = await show(pub.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postを非フォロワーが見れる', async(async () => {
it('[show] public post can be seen by non-follower', async(async () => {
const res = await show(pub.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-postを未認証が見れる', async(async () => {
it('[show] public post can be seen unauthenticated', async(async () => {
const res = await show(pub.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home-postを自分が見れる', async(async () => {
it('[show] home post can be seen by author', async(async () => {
const res = await show(home.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postをフォロワーが見れる', async(async () => {
it('[show] home post can be seen by follower', async(async () => {
const res = await show(home.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postを非フォロワーが見れる', async(async () => {
it('[show] home post can be seen by non-follower', async(async () => {
const res = await show(home.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-postを未認証が見れる', async(async () => {
it('[show] home post can be seen unauthenticated', async(async () => {
const res = await show(home.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers-postを自分が見れる', async(async () => {
it('[show] followers post can be seen by author', async(async () => {
const res = await show(fol.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-postをフォロワーが見れる', async(async () => {
it('[show] followers post can be seen by follower', async(async () => {
const res = await show(fol.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-postを非フォロワーが見れない', async(async () => {
it('[show] followers post is hidden from non-follower', async(async () => {
const res = await show(fol.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] followers-postを未認証が見れない', async(async () => {
it('[show] followers post is hidden when unathenticated', async(async () => {
const res = await show(fol.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it('[show] specified-postを自分が見れる', async(async () => {
it('[show] specified post can be seen by author', async(async () => {
const res = await show(spe.id, alice);
assert.strictEqual(res.status, 404);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-postを指定ユーザーが見れる', async(async () => {
it('[show] specified post can be seen by designated user', async(async () => {
const res = await show(spe.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-postをフォロワーが見れない', async(async () => {
it('[show] specified post is hidden from non-specified follower', async(async () => {
const res = await show(spe.id, follower);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-postを非フォロワーが見れない', async(async () => {
it('[show] specified post is hidden from non-follower', async(async () => {
const res = await show(spe.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-postを未認証が見れない', async(async () => {
it('[show] specified post is hidden when unauthenticated', async(async () => {
const res = await show(spe.id, null);
assert.strictEqual(res.status, 404);
}));
@ -191,110 +192,105 @@ describe('API visibility', () => {
//#region show reply
// public
it('[show] public-replyを自分が見れる', async(async () => {
it('[show] public reply can be seen by author', async(async () => {
const res = await show(pubR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyをされた人が見れる', async(async () => {
it('[show] public reply can be seen by replied to author', async(async () => {
const res = await show(pubR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyをフォロワーが見れる', async(async () => {
it('[show] public reply can be seen by follower', async(async () => {
const res = await show(pubR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyを非フォロワーが見れる', async(async () => {
it('[show] public reply can be seen by non-follower', async(async () => {
const res = await show(pubR.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] public-replyを未認証が見れる', async(async () => {
it('[show] public reply can be seen unauthenticated', async(async () => {
const res = await show(pubR.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// home
it('[show] home-replyを自分が見れる', async(async () => {
it('[show] home reply can be seen by author', async(async () => {
const res = await show(homeR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyをされた人が見れる', async(async () => {
it('[show] home reply can be seen by replied to author', async(async () => {
const res = await show(homeR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyをフォロワーが見れる', async(async () => {
it('[show] home reply can be seen by follower', async(async () => {
const res = await show(homeR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyを非フォロワーが見れる', async(async () => {
it('[show] home reply can be seen by non-follower', async(async () => {
const res = await show(homeR.id, other);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] home-replyを未認証が見れる', async(async () => {
it('[show] home reply can be seen unauthenticated', async(async () => {
const res = await show(homeR.id, null);
assert.strictEqual(res.body.text, 'x');
}));
// followers
it('[show] followers-replyを自分が見れる', async(async () => {
it('[show] followers reply can be seen by author', async(async () => {
const res = await show(folR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => {
it('[show] followers reply can be seen by replied to author', async(async () => {
const res = await show(folR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyをフォロワーが見れる', async(async () => {
it('[show] followers reply can be seen by follower', async(async () => {
const res = await show(folR.id, follower);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] followers-replyを非フォロワーが見れない', async(async () => {
it('[show] followers reply is hidden from non-follower', async(async () => {
const res = await show(folR.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] followers-replyを未認証が見れない', async(async () => {
it('[show] followers reply is hidden when unauthenticated', async(async () => {
const res = await show(folR.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it('[show] specified-replyを自分が見れる', async(async () => {
it('[show] specified reply can be seen by author', async(async () => {
const res = await show(speR.id, alice);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyを指定ユーザーが見れる', async(async () => {
it('[show] specified reply can be seen by replied to user', async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => {
const res = await show(speR.id, target);
assert.strictEqual(res.body.text, 'x');
}));
it('[show] specified-replyをフォロワーが見れない', async(async () => {
it('[show] specified reply is hidden from follower', async(async () => {
const res = await show(speR.id, follower);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-replyを非フォロワーが見れない', async(async () => {
it('[show] specified reply is hidden from non-follower', async(async () => {
const res = await show(speR.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-replyを未認証が見れない', async(async () => {
it('[show] specified reply is hidden when unauthenticated', async(async () => {
const res = await show(speR.id, null);
assert.strictEqual(res.status, 404);
}));
@ -302,131 +298,131 @@ describe('API visibility', () => {
//#region show mention
// public
it('[show] public-mentionを自分が見れる', async(async () => {
it('[show] public-mention can be seen by author', async(async () => {
const res = await show(pubM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionをされた人が見れる', async(async () => {
it('[show] public mention can be seen by mentioned', async(async () => {
const res = await show(pubM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionをフォロワーが見れる', async(async () => {
it('[show] public mention can be seen by follower', async(async () => {
const res = await show(pubM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionを非フォロワーが見れる', async(async () => {
it('[show] public mention can be seen by non-follower', async(async () => {
const res = await show(pubM.id, other);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] public-mentionを未認証が見れる', async(async () => {
it('[show] public mention can be seen unauthenticated', async(async () => {
const res = await show(pubM.id, null);
assert.strictEqual(res.body.text, '@target x');
}));
// home
it('[show] home-mentionを自分が見れる', async(async () => {
it('[show] home mention can be seen by author', async(async () => {
const res = await show(homeM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionをされた人が見れる', async(async () => {
it('[show] home mention can be seen by mentioned', async(async () => {
const res = await show(homeM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionをフォロワーが見れる', async(async () => {
it('[show] home mention can be seen by follower', async(async () => {
const res = await show(homeM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionを非フォロワーが見れる', async(async () => {
it('[show] home mention can be seen by non-follower', async(async () => {
const res = await show(homeM.id, other);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] home-mentionを未認証が見れる', async(async () => {
it('[show] home mention can be seen unauthenticated', async(async () => {
const res = await show(homeM.id, null);
assert.strictEqual(res.body.text, '@target x');
}));
// followers
it('[show] followers-mentionを自分が見れる', async(async () => {
it('[show] followers mention can be seen by author', async(async () => {
const res = await show(folM.id, alice);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async(async () => {
it('[show] followers mention can be seen by non-follower mentioned', async(async () => {
const res = await show(folM.id, target);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionをフォロワーが見れる', async(async () => {
it('[show] followers mention can be seen by follower', async(async () => {
const res = await show(folM.id, follower);
assert.strictEqual(res.body.text, '@target x');
}));
it('[show] followers-mentionを非フォロワーが見れない', async(async () => {
it('[show] followers mention is hidden from non-follower', async(async () => {
const res = await show(folM.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] followers-mentionを未認証が見れない', async(async () => {
it('[show] followers mention is hidden when unauthenticated', async(async () => {
const res = await show(folM.id, null);
assert.strictEqual(res.status, 404);
}));
// specified
it('[show] specified-mentionを自分が見れる', async(async () => {
it('[show] specified mention can be seen by author', async(async () => {
const res = await show(speM.id, alice);
assert.strictEqual(res.body.text, '@target2 x');
}));
it('[show] specified-mentionを指定ユーザーが見れる', async(async () => {
it('[show] specified mention can be seen by specified actor', async(async () => {
const res = await show(speM.id, target);
assert.strictEqual(res.body.text, '@target2 x');
}));
it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => {
it('[show] specified mention is hidden from mentioned but not specified actor', async(async () => {
const res = await show(speM.id, target2);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-mentionをフォロワーが見れない', async(async () => {
it('[show] specified mention is hidden from follower', async(async () => {
const res = await show(speM.id, follower);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-mentionを非フォロワーが見れない', async(async () => {
it('[show] specified mention is hidden from non-follower', async(async () => {
const res = await show(speM.id, other);
assert.strictEqual(res.status, 404);
}));
it('[show] specified-mentionを未認証が見れない', async(async () => {
it('[show] specified mention is hidden when unauthenticated', async(async () => {
const res = await show(speM.id, null);
assert.strictEqual(res.status, 404);
}));
//#endregion
//#region HTL
it('[HTL] public-post が 自分が見れる', async(async () => {
//#region Home Timeline
it('[TL] public post on author home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[HTL] public-post が 非フォロワーから見れない', async(async () => {
it('[TL] public post absent from non-follower home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == pub.id);
assert.strictEqual(notes.length, 0);
}));
it('[HTL] followers-post が フォロワーから見れる', async(async () => {
it('[TL] followers post on follower home TL', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == fol.id);
@ -434,22 +430,22 @@ describe('API visibility', () => {
}));
//#endregion
//#region RTL
it('[replies] followers-reply が フォロワーから見れる', async(async () => {
//#region replies timeline
it('[TL] followers reply on follower reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => {
it('[TL] followers reply absent from not replied to non-follower reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes.length, 0);
}));
it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
it('[TL] followers reply on replied to actor reply TL', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
@ -458,14 +454,14 @@ describe('API visibility', () => {
//#endregion
//#region MTL
it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
it('[TL] followers reply on replied to non-follower mention TL', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folR.id);
assert.strictEqual(notes[0].text, 'x');
}));
it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => {
it('[TL] followers mention on mentioned non-follower mention TL', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
assert.strictEqual(res.status, 200);
const notes = res.body.filter((n: any) => n.id == folM.id);

View file

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

View file

@ -13,6 +13,8 @@ describe('Block', () => {
let carol: any;
before(async () => {
this.timeout(0);
p = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
@ -23,7 +25,7 @@ describe('Block', () => {
await shutdownServer(p);
});
it('Block作成', async(async () => {
it('can block someone', async(async () => {
const res = await request('/blocking/create', {
userId: bob.id,
}, alice);
@ -31,45 +33,45 @@ describe('Block', () => {
assert.strictEqual(res.status, 200);
}));
it('ブロックされているユーザーをフォローできない', async(async () => {
it('cannot follow if blocked', async(async () => {
const res = await request('/following/create', { userId: alice.id }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('ブロックされているユーザーにリアクションできない', async(async () => {
it('cannot react to blocking users note', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('ブロックされているユーザーに返信できない', async(async () => {
it('cannot reply to blocking users note', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
it('ブロックされているユーザーのートをRenoteできない', async(async () => {
it('canot renote blocking users note', async(async () => {
const note = await post(alice, { text: 'hello' });
const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
assert.strictEqual(res.body.error.code, 'BLOCKED');
}));
// TODO: ユーザーリストに入れられないテスト
it('cannot include blocked users in user lists');
// TODO: ユーザーリストから除外されるテスト
it('removes users from user lists');
it('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async(async () => {
it('local timeline does not contain blocked users', async(async () => {
const aliceNote = await post(alice);
const bobNote = await post(bob);
const carolNote = await post(carol);

View file

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

View file

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

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';
type MockResponse = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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