Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina 2022-03-27 22:41:38 +09:00
commit e98e84d562
386 changed files with 2552 additions and 2341 deletions

View file

@ -17,6 +17,7 @@ You should also include the user name that made the change.
### Improvements ### Improvements
- Bull Dashboardを組み込み、ジョブキューの確認や操作を行えるように @syuilo - Bull Dashboardを組み込み、ジョブキューの確認や操作を行えるように @syuilo
- Check that installed Node.js version fulfills version requirement @ThatOneCalculator - Check that installed Node.js version fulfills version requirement @ThatOneCalculator
- Server: performance improvements @syuilo
### Bugfixes ### Bugfixes
- API: fix endpoint endpoint @Johann150 - API: fix endpoint endpoint @Johann150

View file

@ -6,6 +6,9 @@ Also, you might receive comments on your Issue/PR in Japanese, but you do not ne
The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
It will also allow the reader to use the translation tool of their preference if necessary. It will also allow the reader to use the translation tool of their preference if necessary.
## Roadmap
See [ROADMAP.md](./ROADMAP.md)
## Issues ## Issues
Before creating an issue, please check the following: Before creating an issue, please check the following:
- To avoid duplication, please search for similar issues before creating a new issue. - To avoid duplication, please search for similar issues before creating a new issue.
@ -198,11 +201,13 @@ MongoDBの時とは違い、findOneでレコードを取得する時に対象レ
MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
### Migration作成方法 ### Migration作成方法
``` packages/backendで:
npx ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前 -o ```sh
npx typeorm migration:generate -d ormconfig.js -o <migration name>
``` ```
作成されたスクリプトは不必要な変更を含むため除去してください。 - 生成後、ファイルをmigration下に移してください
- 作成されたスクリプトは不必要な変更を含むため除去してください
### コネクションには`markRaw`せよ ### コネクションには`markRaw`せよ
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。 **Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。

28
ROADMAP.md Normal file
View file

@ -0,0 +1,28 @@
# Roadmap
The order of individual tasks is a guide only and is subject to change depending on the situation.
Also, the later tasks are more indefinite and are subject to change as development progresses.
## (1) Improve maintainability \<current phase\>
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
- Make the number of type errors zero (backend)
- Probably need to switch some libraries to others that make it difficult to reduce type errors
- e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537
- Improve CI
- Fix tests
- mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work.
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
- Add more tests
- May need to implement a mechanism that allows for DI
- Improve documentation
## (2) Improve functionality
Once Phase 1 is complete and an environment conducive to the development of a stable system is in place, the implementation of new functions can begin gradually.
- OAuth2 support https://github.com/misskey-dev/misskey/issues/8262
- GraphQL support?
## (3) Improve scalability
Once the development of the feature has settled down, this may be an opportunity to make larger modifications.
- Rewriting in Rust?

View file

@ -841,6 +841,7 @@ oneHour: "1時間"
oneDay: "1日" oneDay: "1日"
oneWeek: "1週間" oneWeek: "1週間"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
_emailUnavailable: _emailUnavailable:
used: "既に使用されています" used: "既に使用されています"

View file

@ -13,8 +13,7 @@
"start": "cd packages/backend && node --experimental-json-modules ./built/index.js", "start": "cd packages/backend && node --experimental-json-modules ./built/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js",
"init": "npm run migrate", "init": "npm run migrate",
"ormconfig": "node ./packages/backend/ormconfig.js", "migrate": "cd packages/backend && npx typeorm migration:run -d ormconfig.js",
"migrate": "cd packages/backend && npx typeorm migration:run",
"migrateandstart": "npm run migrate && npm run start", "migrateandstart": "npm run migrate && npm run start",
"gulp": "gulp build", "gulp": "gulp build",
"watch": "npm run dev", "watch": "npm run dev",
@ -42,10 +41,10 @@
"js-yaml": "4.1.0" "js-yaml": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "5.15.0", "@typescript-eslint/parser": "5.16.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "9.5.2", "cypress": "9.5.2",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",
"typescript": "4.6.2" "typescript": "4.6.3"
} }
} }

View file

@ -1,7 +1,8 @@
import { DataSource } from 'typeorm';
import config from './built/config/index.js'; import config from './built/config/index.js';
import { entities } from './built/db/postgre.js'; import { entities } from './built/db/postgre.js';
export default { export default new DataSource({
type: 'postgres', type: 'postgres',
host: config.db.host, host: config.db.host,
port: config.db.port, port: config.db.port,
@ -11,7 +12,4 @@ export default {
extra: config.db.extra, extra: config.db.extra,
entities: entities, entities: entities,
migrations: ['migration/*.js'], migrations: ['migration/*.js'],
cli: { });
migrationsDir: 'migration'
}
};

View file

@ -3,7 +3,6 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"init": "npm run migrate",
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
"lint": "eslint --quiet src/**/*.ts", "lint": "eslint --quiet src/**/*.ts",
@ -31,7 +30,7 @@
"@types/jsdom": "16.2.14", "@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6", "@types/jsonld": "1.5.6",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa-bodyparser": "4.3.6", "@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2", "@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21", "@types/koa-favicon": "2.0.21",
"@types/koa-logger": "3.1.2", "@types/koa-logger": "3.1.2",
@ -42,7 +41,7 @@
"@types/koa__multer": "2.0.4", "@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11", "@types/koa__router": "8.0.11",
"@types/mocha": "9.1.0", "@types/mocha": "9.1.0",
"@types/node": "17.0.21", "@types/node": "17.0.23",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4", "@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -57,7 +56,7 @@
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2", "@types/sanitize-html": "2.6.2",
"@types/sharp": "0.30.0", "@types/sharp": "0.30.0",
"@types/sinonjs__fake-timers": "8.1.1", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/throttle-debounce": "2.1.0", "@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
@ -66,19 +65,19 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.15.0", "@typescript-eslint/eslint-plugin": "5.16.0",
"@typescript-eslint/parser": "5.15.0", "@typescript-eslint/parser": "5.16.0",
"@bull-board/koa": "3.10.0", "@bull-board/koa": "3.10.1",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"ajv": "8.10.0", "ajv": "8.11.0",
"archiver": "5.3.0", "archiver": "5.3.0",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1096.0", "aws-sdk": "2.1100.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.10.0", "broadcast-channel": "4.10.0",
"bull": "4.7.0", "bull": "4.8.1",
"cacheable-lookup": "6.0.4", "cacheable-lookup": "6.0.4",
"cafy": "15.2.1", "cafy": "15.2.1",
"cbor": "8.1.0", "cbor": "8.1.0",
@ -90,19 +89,19 @@
"date-fns": "2.28.0", "date-fns": "2.28.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "8.11.0", "eslint": "8.12.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "17.1.1", "file-type": "17.1.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"got": "12.0.2", "got": "12.0.3",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"http-signature": "1.3.6", "http-signature": "1.3.6",
"ip-cidr": "3.0.4", "ip-cidr": "3.0.4",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "19.0.0", "jsdom": "19.0.0",
"json5": "2.2.0", "json5": "2.2.1",
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "5.2.0", "jsonld": "5.2.0",
"jsrsasign": "8.0.20", "jsrsasign": "8.0.20",
@ -123,7 +122,7 @@
"multer": "1.4.4", "multer": "1.4.4",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.2.3", "node-fetch": "3.2.3",
"nodemailer": "6.7.2", "nodemailer": "6.7.3",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "6.0.1", "parse5": "6.0.1",
"pg": "8.7.3", "pg": "8.7.3",
@ -154,17 +153,17 @@
"style-loader": "3.3.1", "style-loader": "3.3.1",
"summaly": "2.5.0", "summaly": "2.5.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.11.8", "systeminformation": "5.11.9",
"throttle-debounce": "3.0.1", "throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"ts-loader": "9.2.8", "ts-loader": "9.2.8",
"ts-node": "10.7.0", "ts-node": "10.7.0",
"tsc-alias": "1.4.1", "tsc-alias": "1.4.1",
"tsconfig-paths": "3.14.0", "tsconfig-paths": "3.14.1",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.2.45", "typeorm": "0.3.4",
"typescript": "4.6.2", "typescript": "4.6.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "8.3.2", "uuid": "8.3.2",
@ -174,7 +173,7 @@
"xev": "2.0.1" "xev": "2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.88", "@redocly/openapi-core": "1.0.0-beta.90",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.20",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"execa": "6.1.0" "execa": "6.1.0"

View file

@ -7,7 +7,6 @@ import chalk from 'chalk';
import chalkTemplate from 'chalk-template'; import chalkTemplate from 'chalk-template';
import * as portscanner from 'portscanner'; import * as portscanner from 'portscanner';
import semver from 'semver'; import semver from 'semver';
import { getConnection } from 'typeorm';
import Logger from '@/services/logger.js'; import Logger from '@/services/logger.js';
import loadConfig from '@/config/load.js'; import loadConfig from '@/config/load.js';
@ -15,7 +14,7 @@ import { Config } from '@/config/types.js';
import { lessThan } from '@/prelude/array.js'; import { lessThan } from '@/prelude/array.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
import { showMachineInfo } from '@/misc/show-machine-info.js'; import { showMachineInfo } from '@/misc/show-machine-info.js';
import { initDb } from '../db/postgre.js'; import { db, initDb } from '../db/postgre.js';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -144,7 +143,7 @@ async function connectDb(): Promise<void> {
try { try {
dbLogger.info('Connecting...'); dbLogger.info('Connecting...');
await initDb(); await initDb();
const v = await getConnection().query('SHOW server_version').then(x => x[0].server_version); const v = await db.query('SHOW server_version').then(x => x[0].server_version);
dbLogger.succ(`Connected: v${v}`); dbLogger.succ(`Connected: v${v}`);
} catch (e) { } catch (e) {
dbLogger.error('Cannot connect', null, true); dbLogger.error('Cannot connect', null, true);

View file

@ -2,9 +2,10 @@
import pg from 'pg'; import pg from 'pg';
pg.types.setTypeParser(20, Number); pg.types.setTypeParser(20, Number);
import { createConnection, Logger, getConnection } from 'typeorm'; import { Logger, DataSource } from 'typeorm';
import * as highlight from 'cli-highlight'; import * as highlight from 'cli-highlight';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js'; import { dbLogger } from './logger.js';
@ -61,7 +62,6 @@ import { Antenna } from '@/models/entities/antenna.js';
import { AntennaNote } from '@/models/entities/antenna-note.js'; import { AntennaNote } from '@/models/entities/antenna-note.js';
import { PromoNote } from '@/models/entities/promo-note.js'; import { PromoNote } from '@/models/entities/promo-note.js';
import { PromoRead } from '@/models/entities/promo-read.js'; import { PromoRead } from '@/models/entities/promo-read.js';
import { envOption } from '../env.js';
import { Relay } from '@/models/entities/relay.js'; import { Relay } from '@/models/entities/relay.js';
import { MutedNote } from '@/models/entities/muted-note.js'; import { MutedNote } from '@/models/entities/muted-note.js';
import { Channel } from '@/models/entities/channel.js'; import { Channel } from '@/models/entities/channel.js';
@ -74,7 +74,7 @@ import { UserPending } from '@/models/entities/user-pending.js';
import { entities as charts } from '@/services/chart/entities.js'; import { entities as charts } from '@/services/chart/entities.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
class MyCustomLogger implements Logger { class MyCustomLogger implements Logger {
private highlight(sql: string) { private highlight(sql: string) {
@ -84,10 +84,8 @@ class MyCustomLogger implements Logger {
} }
public logQuery(query: string, parameters?: any[]) { public logQuery(query: string, parameters?: any[]) {
if (envOption.verbose) {
sqlLogger.info(this.highlight(query).substring(0, 100)); sqlLogger.info(this.highlight(query).substring(0, 100));
} }
}
public logQueryError(error: string, query: string, parameters?: any[]) { public logQueryError(error: string, query: string, parameters?: any[]) {
sqlLogger.error(this.highlight(query)); sqlLogger.error(this.highlight(query));
@ -176,17 +174,9 @@ export const entities = [
...charts, ...charts,
]; ];
export function initDb(justBorrow = false, sync = false, forceRecreate = false) { const log = process.env.NODE_ENV !== 'production';
if (!forceRecreate) {
try {
const conn = getConnection();
return Promise.resolve(conn);
} catch (e) {}
}
const log = process.env.NODE_ENV !== 'production'; export const db = new DataSource({
return createConnection({
type: 'postgres', type: 'postgres',
host: config.db.host, host: config.db.host,
port: config.db.port, port: config.db.port,
@ -197,8 +187,8 @@ export function initDb(justBorrow = false, sync = false, forceRecreate = false)
statement_timeout: 1000 * 10, statement_timeout: 1000 * 10,
...config.db.extra, ...config.db.extra,
}, },
synchronize: process.env.NODE_ENV === 'test' || sync, synchronize: process.env.NODE_ENV === 'test',
dropSchema: process.env.NODE_ENV === 'test' && !justBorrow, dropSchema: process.env.NODE_ENV === 'test',
cache: !config.db.disableCache ? { cache: !config.db.disableCache ? {
type: 'redis', type: 'redis',
options: { options: {
@ -211,20 +201,24 @@ export function initDb(justBorrow = false, sync = false, forceRecreate = false)
} : false, } : false,
logging: log, logging: log,
logger: log ? new MyCustomLogger() : undefined, logger: log ? new MyCustomLogger() : undefined,
maxQueryExecutionTime: 300,
entities: entities, entities: entities,
}); migrations: ['../../migration/*.js'],
});
export async function initDb() {
await db.connect();
} }
export async function resetDb() { export async function resetDb() {
const reset = async () => { const reset = async () => {
const conn = await getConnection(); const tables = await db.query(`SELECT relname AS "table"
const tables = await conn.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema') WHERE nspname NOT IN ('pg_catalog', 'information_schema')
AND C.relkind = 'r' AND C.relkind = 'r'
AND nspname !~ '^pg_toast';`); AND nspname !~ '^pg_toast';`);
for (const table of tables) { for (const table of tables) {
await conn.query(`DELETE FROM "${table.table}" CASCADE`); await db.query(`DELETE FROM "${table.table}" CASCADE`);
} }
}; };

View file

@ -1,5 +1,5 @@
export class Cache<T> { export class Cache<T> {
private cache: Map<string | null, { date: number; value: T; }>; public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) { constructor(lifetime: Cache<never>['lifetime']) {
@ -48,7 +48,32 @@ export class Cache<T> {
// Cache MISS // Cache MISS
const value = await fetcher(); const value = await fetcher();
return value;
}
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
// Cache MISS
const value = await fetcher();
if (value !== undefined) {
this.set(key, value); this.set(key, value);
}
return value; return value;
} }
} }

View file

@ -18,7 +18,7 @@ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'No
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
// アンテナ作成者がノート作成者にブロックされていたらスキップ // アンテナ作成者がノート作成者にブロックされていたらスキップ
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.find({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
if (blockings.some(blocking => blocking === antenna.userId)) return false; if (blockings.some(blocking => blocking === antenna.userId)) return false;
if (note.visibility === 'followers') { if (note.visibility === 'followers') {
@ -32,15 +32,15 @@ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'No
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await UserListJoinings.find({ const listUsers = (await UserListJoinings.findBy({
userListId: antenna.userListId!, userListId: antenna.userListId!,
})).map(x => x.userId); })).map(x => x.userId);
if (!listUsers.includes(note.userId)) return false; if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === 'group') { } else if (antenna.src === 'group') {
const joining = await UserGroupJoinings.findOneOrFail(antenna.userGroupJoiningId!); const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId! });
const groupUsers = (await UserGroupJoinings.find({ const groupUsers = (await UserGroupJoinings.findBy({
userGroupId: joining.userGroupId, userGroupId: joining.userGroupId,
})).map(x => x.userId); })).map(x => x.userId);

View file

@ -1,19 +1,21 @@
import { db } from '@/db/postgre.js';
import { Meta } from '@/models/entities/meta.js'; import { Meta } from '@/models/entities/meta.js';
import { getConnection } from 'typeorm';
let cache: Meta; let cache: Meta;
export async function fetchMeta(noCache = false): Promise<Meta> { export async function fetchMeta(noCache = false): Promise<Meta> {
if (!noCache && cache) return cache; if (!noCache && cache) return cache;
return await getConnection().transaction(async transactionalEntityManager => { return await db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const meta = await transactionalEntityManager.findOne(Meta, { const metas = await transactionalEntityManager.find(Meta, {
order: { order: {
id: 'DESC', id: 'DESC',
}, },
}); });
const meta = metas[0];
if (meta) { if (meta) {
cache = meta; cache = meta;
return meta; return meta;

View file

@ -5,5 +5,5 @@ import { Users } from '@/models/index.js';
export async function fetchProxyAccount(): Promise<ILocalUser | null> { export async function fetchProxyAccount(): Promise<ILocalUser | null> {
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.proxyAccountId == null) return null; if (meta.proxyAccountId == null) return null;
return await Users.findOneOrFail(meta.proxyAccountId) as ILocalUser; return await Users.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser;
} }

View file

@ -6,5 +6,5 @@ import { Cache } from './cache.js';
const cache = new Cache<UserKeypair>(Infinity); const cache = new Cache<UserKeypair>(Infinity);
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> { export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await cache.fetch(userId, () => UserKeypairs.findOneOrFail(userId)); return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId: userId }));
} }

View file

@ -1,4 +1,4 @@
import { In } from 'typeorm'; import { In, IsNull } from 'typeorm';
import { Emojis } from '@/models/index.js'; import { Emojis } from '@/models/index.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
@ -52,9 +52,9 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
const { name, host } = parseEmojiStr(emojiName, noteUserHost); const { name, host } = parseEmojiStr(emojiName, noteUserHost);
if (name == null) return null; if (name == null) return null;
const queryOrNull = async () => (await Emojis.findOne({ const queryOrNull = async () => (await Emojis.findOneBy({
name, name,
host, host: host ?? IsNull(),
})) || null; })) || null;
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
@ -112,7 +112,7 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
for (const host of hosts) { for (const host of hosts) {
emojisQuery.push({ emojisQuery.push({
name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)), name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
host: host, host: host ?? IsNull(),
}); });
} }
const _emojis = emojisQuery.length > 0 ? await Emojis.find({ const _emojis = emojisQuery.length > 0 ? await Emojis.find({

View file

@ -3,6 +3,7 @@ import { emojiRegex } from './emoji-regex.js';
import { fetchMeta } from './fetch-meta.js'; import { fetchMeta } from './fetch-meta.js';
import { Emojis } from '@/models/index.js'; import { Emojis } from '@/models/index.js';
import { toPunyNullable } from './convert-host.js'; import { toPunyNullable } from './convert-host.js';
import { IsNull } from 'typeorm';
const legacies: Record<string, string> = { const legacies: Record<string, string> = {
'like': '👍', 'like': '👍',
@ -74,8 +75,8 @@ export async function toDbReaction(reaction?: string | null, reacterHost?: strin
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) { if (custom) {
const name = custom[1]; const name = custom[1];
const emoji = await Emojis.findOne({ const emoji = await Emojis.findOneBy({
host: reacterHost || null, host: reacterHost ?? IsNull(),
name, name,
}); });

View file

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

View file

@ -1,4 +1,6 @@
import { getRepository, getCustomRepository } from 'typeorm'; import { } from 'typeorm';
import { db } from '@/db/postgre.js';
import { Announcement } from './entities/announcement.js'; import { Announcement } from './entities/announcement.js';
import { AnnouncementRead } from './entities/announcement-read.js'; import { AnnouncementRead } from './entities/announcement-read.js';
import { Instance } from './entities/instance.js'; import { Instance } from './entities/instance.js';
@ -63,65 +65,65 @@ import { PasswordResetRequest } from './entities/password-reset-request.js';
import { UserPending } from './entities/user-pending.js'; import { UserPending } from './entities/user-pending.js';
import { InstanceRepository } from './repositories/instance.js'; import { InstanceRepository } from './repositories/instance.js';
export const Announcements = getRepository(Announcement); export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead); export const AnnouncementReads = db.getRepository(AnnouncementRead);
export const Apps = getCustomRepository(AppRepository); export const Apps = (AppRepository);
export const Notes = getCustomRepository(NoteRepository); export const Notes = (NoteRepository);
export const NoteFavorites = getCustomRepository(NoteFavoriteRepository); export const NoteFavorites = (NoteFavoriteRepository);
export const NoteWatchings = getRepository(NoteWatching); export const NoteWatchings = db.getRepository(NoteWatching);
export const NoteThreadMutings = getRepository(NoteThreadMuting); export const NoteThreadMutings = db.getRepository(NoteThreadMuting);
export const NoteReactions = getCustomRepository(NoteReactionRepository); export const NoteReactions = (NoteReactionRepository);
export const NoteUnreads = getRepository(NoteUnread); export const NoteUnreads = db.getRepository(NoteUnread);
export const Polls = getRepository(Poll); export const Polls = db.getRepository(Poll);
export const PollVotes = getRepository(PollVote); export const PollVotes = db.getRepository(PollVote);
export const Users = getCustomRepository(UserRepository); export const Users = (UserRepository);
export const UserProfiles = getRepository(UserProfile); export const UserProfiles = db.getRepository(UserProfile);
export const UserKeypairs = getRepository(UserKeypair); export const UserKeypairs = db.getRepository(UserKeypair);
export const UserPendings = getRepository(UserPending); export const UserPendings = db.getRepository(UserPending);
export const AttestationChallenges = getRepository(AttestationChallenge); export const AttestationChallenges = db.getRepository(AttestationChallenge);
export const UserSecurityKeys = getRepository(UserSecurityKey); export const UserSecurityKeys = db.getRepository(UserSecurityKey);
export const UserPublickeys = getRepository(UserPublickey); export const UserPublickeys = db.getRepository(UserPublickey);
export const UserLists = getCustomRepository(UserListRepository); export const UserLists = (UserListRepository);
export const UserListJoinings = getRepository(UserListJoining); export const UserListJoinings = db.getRepository(UserListJoining);
export const UserGroups = getCustomRepository(UserGroupRepository); export const UserGroups = (UserGroupRepository);
export const UserGroupJoinings = getRepository(UserGroupJoining); export const UserGroupJoinings = db.getRepository(UserGroupJoining);
export const UserGroupInvitations = getCustomRepository(UserGroupInvitationRepository); export const UserGroupInvitations = (UserGroupInvitationRepository);
export const UserNotePinings = getRepository(UserNotePining); export const UserNotePinings = db.getRepository(UserNotePining);
export const UsedUsernames = getRepository(UsedUsername); export const UsedUsernames = db.getRepository(UsedUsername);
export const Followings = getCustomRepository(FollowingRepository); export const Followings = (FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository); export const FollowRequests = (FollowRequestRepository);
export const Instances = getCustomRepository(InstanceRepository); export const Instances = (InstanceRepository);
export const Emojis = getCustomRepository(EmojiRepository); export const Emojis = (EmojiRepository);
export const DriveFiles = getCustomRepository(DriveFileRepository); export const DriveFiles = (DriveFileRepository);
export const DriveFolders = getCustomRepository(DriveFolderRepository); export const DriveFolders = (DriveFolderRepository);
export const Notifications = getCustomRepository(NotificationRepository); export const Notifications = (NotificationRepository);
export const Metas = getRepository(Meta); export const Metas = db.getRepository(Meta);
export const Mutings = getCustomRepository(MutingRepository); export const Mutings = (MutingRepository);
export const Blockings = getCustomRepository(BlockingRepository); export const Blockings = (BlockingRepository);
export const SwSubscriptions = getRepository(SwSubscription); export const SwSubscriptions = db.getRepository(SwSubscription);
export const Hashtags = getCustomRepository(HashtagRepository); export const Hashtags = (HashtagRepository);
export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository); export const AbuseUserReports = (AbuseUserReportRepository);
export const RegistrationTickets = getRepository(RegistrationTicket); export const RegistrationTickets = db.getRepository(RegistrationTicket);
export const AuthSessions = getCustomRepository(AuthSessionRepository); export const AuthSessions = (AuthSessionRepository);
export const AccessTokens = getRepository(AccessToken); export const AccessTokens = db.getRepository(AccessToken);
export const Signins = getCustomRepository(SigninRepository); export const Signins = (SigninRepository);
export const MessagingMessages = getCustomRepository(MessagingMessageRepository); export const MessagingMessages = (MessagingMessageRepository);
export const Pages = getCustomRepository(PageRepository); export const Pages = (PageRepository);
export const PageLikes = getCustomRepository(PageLikeRepository); export const PageLikes = (PageLikeRepository);
export const GalleryPosts = getCustomRepository(GalleryPostRepository); export const GalleryPosts = (GalleryPostRepository);
export const GalleryLikes = getCustomRepository(GalleryLikeRepository); export const GalleryLikes = (GalleryLikeRepository);
export const ModerationLogs = getCustomRepository(ModerationLogRepository); export const ModerationLogs = (ModerationLogRepository);
export const Clips = getCustomRepository(ClipRepository); export const Clips = (ClipRepository);
export const ClipNotes = getRepository(ClipNote); export const ClipNotes = db.getRepository(ClipNote);
export const Antennas = getCustomRepository(AntennaRepository); export const Antennas = (AntennaRepository);
export const AntennaNotes = getRepository(AntennaNote); export const AntennaNotes = db.getRepository(AntennaNote);
export const PromoNotes = getRepository(PromoNote); export const PromoNotes = db.getRepository(PromoNote);
export const PromoReads = getRepository(PromoRead); export const PromoReads = db.getRepository(PromoRead);
export const Relays = getCustomRepository(RelayRepository); export const Relays = (RelayRepository);
export const MutedNotes = getRepository(MutedNote); export const MutedNotes = db.getRepository(MutedNote);
export const Channels = getCustomRepository(ChannelRepository); export const Channels = (ChannelRepository);
export const ChannelFollowings = getRepository(ChannelFollowing); export const ChannelFollowings = db.getRepository(ChannelFollowing);
export const ChannelNotePinings = getRepository(ChannelNotePining); export const ChannelNotePinings = db.getRepository(ChannelNotePining);
export const RegistryItems = getRepository(RegistryItem); export const RegistryItems = db.getRepository(RegistryItem);
export const Ads = getRepository(Ad); export const Ads = db.getRepository(Ad);
export const PasswordResetRequests = getRepository(PasswordResetRequest); export const PasswordResetRequests = db.getRepository(PasswordResetRequest);

View file

@ -1,14 +1,13 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; import { AbuseUserReport } from '@/models/entities/abuse-user-report.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
@EntityRepository(AbuseUserReport) export const AbuseUserReportRepository = db.getRepository(AbuseUserReport).extend({
export class AbuseUserReportRepository extends Repository<AbuseUserReport> { async pack(
public async pack(
src: AbuseUserReport['id'] | AbuseUserReport, src: AbuseUserReport['id'] | AbuseUserReport,
) { ) {
const report = typeof src === 'object' ? src : await this.findOneOrFail(src); const report = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: report.id, id: report.id,
@ -29,11 +28,11 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
}) : null, }) : null,
forwarded: report.forwarded, forwarded: report.forwarded,
}); });
} },
public packMany( packMany(
reports: any[], reports: any[],
) { ) {
return Promise.all(reports.map(x => this.pack(x))); return Promise.all(reports.map(x => this.pack(x)));
} },
} });

View file

@ -1,17 +1,16 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Antenna } from '@/models/entities/antenna.js'; import { Antenna } from '@/models/entities/antenna.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { AntennaNotes, UserGroupJoinings } from '../index.js'; import { AntennaNotes, UserGroupJoinings } from '../index.js';
@EntityRepository(Antenna) export const AntennaRepository = db.getRepository(Antenna).extend({
export class AntennaRepository extends Repository<Antenna> { async pack(
public async pack(
src: Antenna['id'] | Antenna, src: Antenna['id'] | Antenna,
): Promise<Packed<'Antenna'>> { ): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src); const antenna = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null; const hasUnreadNote = (await AntennaNotes.findOneBy({ antennaId: antenna.id, read: false })) != null;
const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null; const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOneBy({ id: antenna.userGroupJoiningId }) : null;
return { return {
id: antenna.id, id: antenna.id,
@ -29,5 +28,5 @@ export class AntennaRepository extends Repository<Antenna> {
withFile: antenna.withFile, withFile: antenna.withFile,
hasUnreadNote, hasUnreadNote,
}; };
} },
} });

View file

@ -1,12 +1,11 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { App } from '@/models/entities/app.js'; import { App } from '@/models/entities/app.js';
import { AccessTokens } from '../index.js'; import { AccessTokens } from '../index.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { User } from '../entities/user.js'; import { User } from '../entities/user.js';
@EntityRepository(App) export const AppRepository = db.getRepository(App).extend({
export class AppRepository extends Repository<App> { async pack(
public async pack(
src: App['id'] | App, src: App['id'] | App,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -21,7 +20,7 @@ export class AppRepository extends Repository<App> {
includeProfileImageIds: false, includeProfileImageIds: false,
}, options); }, options);
const app = typeof src === 'object' ? src : await this.findOneOrFail(src); const app = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: app.id, id: app.id,
@ -30,11 +29,11 @@ export class AppRepository extends Repository<App> {
permission: app.permission, permission: app.permission,
...(opts.includeSecret ? { secret: app.secret } : {}), ...(opts.includeSecret ? { secret: app.secret } : {}),
...(me ? { ...(me ? {
isAuthorized: await AccessTokens.count({ isAuthorized: await AccessTokens.countBy({
appId: app.id, appId: app.id,
userId: me.id, userId: me.id,
}).then(count => count > 0), }).then(count => count > 0),
} : {}), } : {}),
}; };
} },
} });

View file

@ -1,21 +1,20 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Apps } from '../index.js'; import { Apps } from '../index.js';
import { AuthSession } from '@/models/entities/auth-session.js'; import { AuthSession } from '@/models/entities/auth-session.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(AuthSession) export const AuthSessionRepository = db.getRepository(AuthSession).extend({
export class AuthSessionRepository extends Repository<AuthSession> { async pack(
public async pack(
src: AuthSession['id'] | AuthSession, src: AuthSession['id'] | AuthSession,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined
) { ) {
const session = typeof src === 'object' ? src : await this.findOneOrFail(src); const session = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: session.id, id: session.id,
app: Apps.pack(session.appId, me), app: Apps.pack(session.appId, me),
token: session.token, token: session.token,
}); });
} },
} });

View file

@ -1,17 +1,16 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { Blocking } from '@/models/entities/blocking.js'; import { Blocking } from '@/models/entities/blocking.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(Blocking) export const BlockingRepository = db.getRepository(Blocking).extend({
export class BlockingRepository extends Repository<Blocking> { async pack(
public async pack(
src: Blocking['id'] | Blocking, src: Blocking['id'] | Blocking,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined
): Promise<Packed<'Blocking'>> { ): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src); const blocking = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: blocking.id, id: blocking.id,
@ -21,12 +20,12 @@ export class BlockingRepository extends Repository<Blocking> {
detail: true, detail: true,
}), }),
}); });
} },
public packMany( packMany(
blockings: any[], blockings: any[],
me: { id: User['id'] } me: { id: User['id'] }
) { ) {
return Promise.all(blockings.map(x => this.pack(x, me))); return Promise.all(blockings.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,23 +1,22 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Channel } from '@/models/entities/channel.js'; import { Channel } from '@/models/entities/channel.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index.js'; import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(Channel) export const ChannelRepository = db.getRepository(Channel).extend({
export class ChannelRepository extends Repository<Channel> { async pack(
public async pack(
src: Channel['id'] | Channel, src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Channel'>> { ): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.findOneOrFail(src); const channel = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const meId = me ? me.id : null; const meId = me ? me.id : null;
const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null; const banner = channel.bannerId ? await DriveFiles.findOneBy({ id: channel.bannerId }) : null;
const hasUnreadNote = meId ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined; const hasUnreadNote = meId ? (await NoteUnreads.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
const following = meId ? await ChannelFollowings.findOne({ const following = meId ? await ChannelFollowings.findOneBy({
followerId: meId, followerId: meId,
followeeId: channel.id, followeeId: channel.id,
}) : null; }) : null;
@ -38,5 +37,5 @@ export class ChannelRepository extends Repository<Channel> {
hasUnreadNote, hasUnreadNote,
} : {}), } : {}),
}; };
} },
} });

View file

@ -1,15 +1,14 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Clip } from '@/models/entities/clip.js'; import { Clip } from '@/models/entities/clip.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
@EntityRepository(Clip) export const ClipRepository = db.getRepository(Clip).extend({
export class ClipRepository extends Repository<Clip> { async pack(
public async pack(
src: Clip['id'] | Clip, src: Clip['id'] | Clip,
): Promise<Packed<'Clip'>> { ): Promise<Packed<'Clip'>> {
const clip = typeof src === 'object' ? src : await this.findOneOrFail(src); const clip = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: clip.id, id: clip.id,
@ -20,12 +19,12 @@ export class ClipRepository extends Repository<Clip> {
description: clip.description, description: clip.description,
isPublic: clip.isPublic, isPublic: clip.isPublic,
}); });
} },
public packMany( packMany(
clips: Clip[], clips: Clip[],
) { ) {
return Promise.all(clips.map(x => this.pack(x))); return Promise.all(clips.map(x => this.pack(x)));
} },
} });

View file

@ -1,4 +1,4 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { Users, DriveFolders } from '../index.js'; import { Users, DriveFolders } from '../index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@ -16,9 +16,8 @@ type PackOptions = {
withUser?: boolean, withUser?: boolean,
}; };
@EntityRepository(DriveFile) export const DriveFileRepository = db.getRepository(DriveFile).extend({
export class DriveFileRepository extends Repository<DriveFile> { validateFileName(name: string): boolean {
public validateFileName(name: string): boolean {
return ( return (
(name.trim().length > 0) && (name.trim().length > 0) &&
(name.length <= 200) && (name.length <= 200) &&
@ -26,9 +25,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
(name.indexOf('/') === -1) && (name.indexOf('/') === -1) &&
(name.indexOf('..') === -1) (name.indexOf('..') === -1)
); );
} },
public getPublicProperties(file: DriveFile): DriveFile['properties'] { getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) { if (file.properties.orientation != null) {
const properties = JSON.parse(JSON.stringify(file.properties)); const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) { if (file.properties.orientation >= 5) {
@ -39,9 +38,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
} }
return file.properties; return file.properties;
} },
public getPublicUrl(file: DriveFile, thumbnail = false): string | null { getPublicUrl(file: DriveFile, thumbnail = false): string | null {
// リモートかつメディアプロキシ // リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) { if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
return appendQuery(config.mediaProxy, query({ return appendQuery(config.mediaProxy, query({
@ -62,9 +61,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type); const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url); return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
} },
public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> { async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
const id = typeof user === 'object' ? user.id : user; const id = typeof user === 'object' ? user.id : user;
const { sum } = await this const { sum } = await this
@ -75,9 +74,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
.getRawOne(); .getRawOne();
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} },
public async calcDriveUsageOfHost(host: string): Promise<number> { async calcDriveUsageOfHost(host: string): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost = :host', { host: toPuny(host) }) .where('file.userHost = :host', { host: toPuny(host) })
@ -86,9 +85,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
.getRawOne(); .getRawOne();
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} },
public async calcDriveUsageOfLocal(): Promise<number> { async calcDriveUsageOfLocal(): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost IS NULL') .where('file.userHost IS NULL')
@ -97,9 +96,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
.getRawOne(); .getRawOne();
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} },
public async calcDriveUsageOfRemote(): Promise<number> { async calcDriveUsageOfRemote(): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost IS NOT NULL') .where('file.userHost IS NOT NULL')
@ -108,11 +107,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
.getRawOne(); .getRawOne();
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} },
public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>; async pack(
public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
public async pack(
src: DriveFile['id'] | DriveFile, src: DriveFile['id'] | DriveFile,
options?: PackOptions options?: PackOptions
): Promise<Packed<'DriveFile'> | null> { ): Promise<Packed<'DriveFile'> | null> {
@ -121,11 +118,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
self: false, self: false,
}, options); }, options);
const file = typeof src === 'object' ? src : await this.findOne(src); const file = typeof src === 'object' ? src : await this.findOneBy({ id: src });
if (file == null) return null; if (file == null) return null;
const meta = await fetchMeta();
return await awaitAll<Packed<'DriveFile'>>({ return await awaitAll<Packed<'DriveFile'>>({
id: file.id, id: file.id,
createdAt: file.createdAt.toISOString(), createdAt: file.createdAt.toISOString(),
@ -146,13 +141,13 @@ export class DriveFileRepository extends Repository<DriveFile> {
userId: opts.withUser ? file.userId : null, userId: opts.withUser ? file.userId : null,
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null, user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
}); });
} },
public async packMany( async packMany(
files: (DriveFile['id'] | DriveFile)[], files: (DriveFile['id'] | DriveFile)[],
options?: PackOptions options?: PackOptions
) { ) {
const items = await Promise.all(files.map(f => this.pack(f, options))); const items = await Promise.all(files.map(f => this.pack(f, options)));
return items.filter(x => x != null); return items.filter(x => x != null);
} },
} });

View file

@ -1,12 +1,11 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { DriveFolders, DriveFiles } from '../index.js'; import { DriveFolders, DriveFiles } from '../index.js';
import { DriveFolder } from '@/models/entities/drive-folder.js'; import { DriveFolder } from '@/models/entities/drive-folder.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
@EntityRepository(DriveFolder) export const DriveFolderRepository = db.getRepository(DriveFolder).extend({
export class DriveFolderRepository extends Repository<DriveFolder> { async pack(
public async pack(
src: DriveFolder['id'] | DriveFolder, src: DriveFolder['id'] | DriveFolder,
options?: { options?: {
detail: boolean detail: boolean
@ -16,7 +15,7 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
detail: false, detail: false,
}, options); }, options);
const folder = typeof src === 'object' ? src : await this.findOneOrFail(src); const folder = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: folder.id, id: folder.id,
@ -25,10 +24,10 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
parentId: folder.parentId, parentId: folder.parentId,
...(opts.detail ? { ...(opts.detail ? {
foldersCount: DriveFolders.count({ foldersCount: DriveFolders.countBy({
parentId: folder.id, parentId: folder.id,
}), }),
filesCount: DriveFiles.count({ filesCount: DriveFiles.countBy({
folderId: folder.id, folderId: folder.id,
}), }),
@ -39,5 +38,5 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
} : {}), } : {}),
} : {}), } : {}),
}); });
} },
} });

View file

@ -1,13 +1,12 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
@EntityRepository(Emoji) export const EmojiRepository = db.getRepository(Emoji).extend({
export class EmojiRepository extends Repository<Emoji> { async pack(
public async pack(
src: Emoji['id'] | Emoji, src: Emoji['id'] | Emoji,
): Promise<Packed<'Emoji'>> { ): Promise<Packed<'Emoji'>> {
const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src); const emoji = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: emoji.id, id: emoji.id,
@ -18,11 +17,11 @@ export class EmojiRepository extends Repository<Emoji> {
// || emoji.originalUrl してるのは後方互換性のため // || emoji.originalUrl してるのは後方互換性のため
url: emoji.publicUrl || emoji.originalUrl, url: emoji.publicUrl || emoji.originalUrl,
}; };
} },
public packMany( packMany(
emojis: any[], emojis: any[],
) { ) {
return Promise.all(emojis.map(x => this.pack(x))); return Promise.all(emojis.map(x => this.pack(x)));
} },
} });

View file

@ -1,20 +1,19 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { FollowRequest } from '@/models/entities/follow-request.js'; import { FollowRequest } from '@/models/entities/follow-request.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(FollowRequest) export const FollowRequestRepository = db.getRepository(FollowRequest).extend({
export class FollowRequestRepository extends Repository<FollowRequest> { async pack(
public async pack(
src: FollowRequest['id'] | FollowRequest, src: FollowRequest['id'] | FollowRequest,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined
) { ) {
const request = typeof src === 'object' ? src : await this.findOneOrFail(src); const request = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: request.id, id: request.id,
follower: await Users.pack(request.followerId, me), follower: await Users.pack(request.followerId, me),
followee: await Users.pack(request.followeeId, me), followee: await Users.pack(request.followeeId, me),
}; };
} },
} });

View file

@ -1,4 +1,4 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { Following } from '@/models/entities/following.js'; import { Following } from '@/models/entities/following.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
@ -29,25 +29,24 @@ type RemoteFolloweeFollowing = Following & {
followeeSharedInbox: string; followeeSharedInbox: string;
}; };
@EntityRepository(Following) export const FollowingRepository = db.getRepository(Following).extend({
export class FollowingRepository extends Repository<Following> { isLocalFollower(following: Following): following is LocalFollowerFollowing {
public isLocalFollower(following: Following): following is LocalFollowerFollowing {
return following.followerHost == null; return following.followerHost == null;
} },
public isRemoteFollower(following: Following): following is RemoteFollowerFollowing { isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
return following.followerHost != null; return following.followerHost != null;
} },
public isLocalFollowee(following: Following): following is LocalFolloweeFollowing { isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
return following.followeeHost == null; return following.followeeHost == null;
} },
public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
return following.followeeHost != null; return following.followeeHost != null;
} },
public async pack( async pack(
src: Following['id'] | Following, src: Following['id'] | Following,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
opts?: { opts?: {
@ -55,7 +54,7 @@ export class FollowingRepository extends Repository<Following> {
populateFollower?: boolean; populateFollower?: boolean;
} }
): Promise<Packed<'Following'>> { ): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.findOneOrFail(src); const following = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
if (opts == null) opts = {}; if (opts == null) opts = {};
@ -71,9 +70,9 @@ export class FollowingRepository extends Repository<Following> {
detail: true, detail: true,
}) : undefined, }) : undefined,
}); });
} },
public packMany( packMany(
followings: any[], followings: any[],
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
opts?: { opts?: {
@ -82,5 +81,5 @@ export class FollowingRepository extends Repository<Following> {
} }
) { ) {
return Promise.all(followings.map(x => this.pack(x, me, opts))); return Promise.all(followings.map(x => this.pack(x, me, opts)));
} },
} });

View file

@ -1,25 +1,24 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { GalleryLike } from '@/models/entities/gallery-like.js'; import { GalleryLike } from '@/models/entities/gallery-like.js';
import { GalleryPosts } from '../index.js'; import { GalleryPosts } from '../index.js';
@EntityRepository(GalleryLike) export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({
export class GalleryLikeRepository extends Repository<GalleryLike> { async pack(
public async pack(
src: GalleryLike['id'] | GalleryLike, src: GalleryLike['id'] | GalleryLike,
me?: any me?: any
) { ) {
const like = typeof src === 'object' ? src : await this.findOneOrFail(src); const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: like.id, id: like.id,
post: await GalleryPosts.pack(like.post || like.postId, me), post: await GalleryPosts.pack(like.post || like.postId, me),
}; };
} },
public packMany( packMany(
likes: any[], likes: any[],
me: any me: any
) { ) {
return Promise.all(likes.map(x => this.pack(x, me))); return Promise.all(likes.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,18 +1,17 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { GalleryPost } from '@/models/entities/gallery-post.js'; import { GalleryPost } from '@/models/entities/gallery-post.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { Users, DriveFiles, GalleryLikes } from '../index.js'; import { Users, DriveFiles, GalleryLikes } from '../index.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(GalleryPost) export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
export class GalleryPostRepository extends Repository<GalleryPost> { async pack(
public async pack(
src: GalleryPost['id'] | GalleryPost, src: GalleryPost['id'] | GalleryPost,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'GalleryPost'>> { ): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.findOneOrFail(src); const post = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: post.id, id: post.id,
@ -27,14 +26,14 @@ export class GalleryPostRepository extends Repository<GalleryPost> {
tags: post.tags.length > 0 ? post.tags : undefined, tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive, isSensitive: post.isSensitive,
likedCount: post.likedCount, likedCount: post.likedCount,
isLiked: meId ? await GalleryLikes.findOne({ postId: post.id, userId: meId }).then(x => x != null) : undefined, isLiked: meId ? await GalleryLikes.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
}); });
} },
public packMany( packMany(
posts: GalleryPost[], posts: GalleryPost[],
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
) { ) {
return Promise.all(posts.map(x => this.pack(x, me))); return Promise.all(posts.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,10 +1,9 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Hashtag } from '@/models/entities/hashtag.js'; import { Hashtag } from '@/models/entities/hashtag.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
@EntityRepository(Hashtag) export const HashtagRepository = db.getRepository(Hashtag).extend({
export class HashtagRepository extends Repository<Hashtag> { async pack(
public async pack(
src: Hashtag, src: Hashtag,
): Promise<Packed<'Hashtag'>> { ): Promise<Packed<'Hashtag'>> {
return { return {
@ -16,11 +15,11 @@ export class HashtagRepository extends Repository<Hashtag> {
attachedLocalUsersCount: src.attachedLocalUsersCount, attachedLocalUsersCount: src.attachedLocalUsersCount,
attachedRemoteUsersCount: src.attachedRemoteUsersCount, attachedRemoteUsersCount: src.attachedRemoteUsersCount,
}; };
} },
public packMany( packMany(
hashtags: Hashtag[], hashtags: Hashtag[],
) { ) {
return Promise.all(hashtags.map(x => this.pack(x))); return Promise.all(hashtags.map(x => this.pack(x)));
} },
} });

View file

@ -1,10 +1,9 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Instance } from '@/models/entities/instance.js'; import { Instance } from '@/models/entities/instance.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
@EntityRepository(Instance) export const InstanceRepository = db.getRepository(Instance).extend({
export class InstanceRepository extends Repository<Instance> { async pack(
public async pack(
instance: Instance, instance: Instance,
): Promise<Packed<'FederationInstance'>> { ): Promise<Packed<'FederationInstance'>> {
return { return {
@ -29,11 +28,11 @@ export class InstanceRepository extends Repository<Instance> {
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
}; };
} },
public packMany( packMany(
instances: Instance[], instances: Instance[],
) { ) {
return Promise.all(instances.map(x => this.pack(x))); return Promise.all(instances.map(x => this.pack(x)));
} },
} });

View file

@ -1,12 +1,11 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Users, DriveFiles, UserGroups } from '../index.js'; import { Users, DriveFiles, UserGroups } from '../index.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(MessagingMessage) export const MessagingMessageRepository = db.getRepository(MessagingMessage).extend({
export class MessagingMessageRepository extends Repository<MessagingMessage> { async pack(
public async pack(
src: MessagingMessage['id'] | MessagingMessage, src: MessagingMessage['id'] | MessagingMessage,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -19,7 +18,7 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
populateGroup: true, populateGroup: true,
}; };
const message = typeof src === 'object' ? src : await this.findOneOrFail(src); const message = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: message.id, id: message.id,
@ -36,5 +35,5 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
isRead: message.isRead, isRead: message.isRead,
reads: message.reads, reads: message.reads,
}; };
} },
} });

View file

@ -1,14 +1,13 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { ModerationLog } from '@/models/entities/moderation-log.js'; import { ModerationLog } from '@/models/entities/moderation-log.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
@EntityRepository(ModerationLog) export const ModerationLogRepository = db.getRepository(ModerationLog).extend({
export class ModerationLogRepository extends Repository<ModerationLog> { async pack(
public async pack(
src: ModerationLog['id'] | ModerationLog, src: ModerationLog['id'] | ModerationLog,
) { ) {
const log = typeof src === 'object' ? src : await this.findOneOrFail(src); const log = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: log.id, id: log.id,
@ -20,11 +19,11 @@ export class ModerationLogRepository extends Repository<ModerationLog> {
detail: true, detail: true,
}), }),
}); });
} },
public packMany( packMany(
reports: any[], reports: any[],
) { ) {
return Promise.all(reports.map(x => this.pack(x))); return Promise.all(reports.map(x => this.pack(x)));
} },
} });

View file

@ -1,17 +1,16 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Users } from '../index.js'; import { Users } from '../index.js';
import { Muting } from '@/models/entities/muting.js'; import { Muting } from '@/models/entities/muting.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(Muting) export const MutingRepository = db.getRepository(Muting).extend({
export class MutingRepository extends Repository<Muting> { async pack(
public async pack(
src: Muting['id'] | Muting, src: Muting['id'] | Muting,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined
): Promise<Packed<'Muting'>> { ): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.findOneOrFail(src); const muting = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll({ return await awaitAll({
id: muting.id, id: muting.id,
@ -22,12 +21,12 @@ export class MutingRepository extends Repository<Muting> {
detail: true, detail: true,
}), }),
}); });
} },
public packMany( packMany(
mutings: any[], mutings: any[],
me: { id: User['id'] } me: { id: User['id'] }
) { ) {
return Promise.all(mutings.map(x => this.pack(x, me))); return Promise.all(mutings.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,15 +1,14 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { NoteFavorite } from '@/models/entities/note-favorite.js'; import { NoteFavorite } from '@/models/entities/note-favorite.js';
import { Notes } from '../index.js'; import { Notes } from '../index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(NoteFavorite) export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({
export class NoteFavoriteRepository extends Repository<NoteFavorite> { async pack(
public async pack(
src: NoteFavorite['id'] | NoteFavorite, src: NoteFavorite['id'] | NoteFavorite,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined
) { ) {
const favorite = typeof src === 'object' ? src : await this.findOneOrFail(src); const favorite = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: favorite.id, id: favorite.id,
@ -17,12 +16,12 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
noteId: favorite.noteId, noteId: favorite.noteId,
note: await Notes.pack(favorite.note || favorite.noteId, me), note: await Notes.pack(favorite.note || favorite.noteId, me),
}; };
} },
public packMany( packMany(
favorites: any[], favorites: any[],
me: { id: User['id'] } me: { id: User['id'] }
) { ) {
return Promise.all(favorites.map(x => this.pack(x, me))); return Promise.all(favorites.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,13 +1,12 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { NoteReaction } from '@/models/entities/note-reaction.js'; import { NoteReaction } from '@/models/entities/note-reaction.js';
import { Notes, Users } from '../index.js'; import { Notes, Users } from '../index.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { convertLegacyReaction } from '@/misc/reaction-lib.js'; import { convertLegacyReaction } from '@/misc/reaction-lib.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(NoteReaction) export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
export class NoteReactionRepository extends Repository<NoteReaction> { async pack(
public async pack(
src: NoteReaction['id'] | NoteReaction, src: NoteReaction['id'] | NoteReaction,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -18,7 +17,7 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
withNote: false, withNote: false,
}, options); }, options);
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src); const reaction = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: reaction.id, id: reaction.id,
@ -29,5 +28,5 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
note: await Notes.pack(reaction.note ?? reaction.noteId, me), note: await Notes.pack(reaction.note ?? reaction.noteId, me),
} : {}), } : {}),
}; };
} },
} });

View file

@ -1,4 +1,4 @@
import { EntityRepository, Repository, In } from 'typeorm'; import { In } from 'typeorm';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@ -9,59 +9,9 @@ import { awaitAll } from '@/prelude/await-all.js';
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib.js'; import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib.js';
import { NoteReaction } from '@/models/entities/note-reaction.js'; import { NoteReaction } from '@/models/entities/note-reaction.js';
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
import { db } from '@/db/postgre.js';
@EntityRepository(Note) async function hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
export class NoteRepository extends Repository<Note> {
public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
const following = await Followings.findOne({
followeeId: note.userId,
followerId: meId,
});
if (following == null) {
return false;
} else {
return true;
}
}
}
return true;
}
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false; let hide = false;
@ -97,7 +47,7 @@ export class NoteRepository extends Repository<Note> {
hide = false; hide = false;
} else { } else {
// フォロワーかどうか // フォロワーかどうか
const following = await Followings.findOne({ const following = await Followings.findOneBy({
followeeId: packedNote.userId, followeeId: packedNote.userId,
followerId: meId, followerId: meId,
}); });
@ -119,9 +69,122 @@ export class NoteRepository extends Repository<Note> {
packedNote.cw = null; packedNote.cw = null;
packedNote.isHidden = true; packedNote.isHidden = true;
} }
}
async function populatePoll(note: Note, meId: User['id'] | null) {
const poll = await Polls.findOneByOrFail({ noteId: note.id });
const choices = poll.choices.map(c => ({
text: c,
votes: poll.votes[poll.choices.indexOf(c)],
isVoted: false,
}));
if (meId) {
if (poll.multiple) {
const votes = await PollVotes.findBy({
userId: meId,
noteId: note.id,
});
const myChoices = votes.map(v => v.choice);
for (const myChoice of myChoices) {
choices[myChoice].isVoted = true;
}
} else {
const vote = await PollVotes.findOneBy({
userId: meId,
noteId: note.id,
});
if (vote) {
choices[vote.choice].isVoted = true;
}
}
} }
public async pack( return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
choices,
};
}
async function populateMyReaction(note: Note, meId: User['id'], _hint_?: {
myReactions: Map<Note['id'], NoteReaction | null>;
}) {
if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(note.id);
if (reaction) {
return convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
const reaction = await NoteReactions.findOneBy({
userId: meId,
noteId: note.id,
});
if (reaction) {
return convertLegacyReaction(reaction.reaction);
}
return undefined;
}
export const NoteRepository = db.getRepository(Note).extend({
async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === 'followers') {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && (meId === note.reply.userId)) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions && note.mentions.some(id => meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
const following = await Followings.findOneBy({
followeeId: note.userId,
followerId: meId,
});
if (following == null) {
return false;
} else {
return true;
}
}
}
return true;
},
async pack(
src: Note['id'] | Note, src: Note['id'] | Note,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -138,68 +201,9 @@ export class NoteRepository extends Repository<Note> {
}, options); }, options);
const meId = me ? me.id : null; const meId = me ? me.id : null;
const note = typeof src === 'object' ? src : await this.findOneOrFail(src); const note = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const host = note.userHost; const host = note.userHost;
async function populatePoll() {
const poll = await Polls.findOneOrFail(note.id);
const choices = poll.choices.map(c => ({
text: c,
votes: poll.votes[poll.choices.indexOf(c)],
isVoted: false,
}));
if (poll.multiple) {
const votes = await PollVotes.find({
userId: meId!,
noteId: note.id,
});
const myChoices = votes.map(v => v.choice);
for (const myChoice of myChoices) {
choices[myChoice].isVoted = true;
}
} else {
const vote = await PollVotes.findOne({
userId: meId!,
noteId: note.id,
});
if (vote) {
choices[vote.choice].isVoted = true;
}
}
return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
choices,
};
}
async function populateMyReaction() {
if (options?._hint_?.myReactions) {
const reaction = options._hint_.myReactions.get(note.id);
if (reaction) {
return convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
const reaction = await NoteReactions.findOne({
userId: meId!,
noteId: note.id,
});
if (reaction) {
return convertLegacyReaction(reaction.reaction);
}
return undefined;
}
let text = note.text; let text = note.text;
if (note.name && (note.url ?? note.uri)) { if (note.name && (note.url ?? note.uri)) {
@ -209,7 +213,7 @@ export class NoteRepository extends Repository<Note> {
const channel = note.channelId const channel = note.channelId
? note.channel ? note.channel
? note.channel ? note.channel
: await Channels.findOne(note.channelId) : await Channels.findOneBy({ id: note.channelId })
: null; : null;
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, '')); const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
@ -255,10 +259,10 @@ export class NoteRepository extends Repository<Note> {
_hint_: options?._hint_, _hint_: options?._hint_,
}) : undefined, }) : undefined,
poll: note.hasPoll ? populatePoll() : undefined, poll: note.hasPoll ? populatePoll(note, meId) : undefined,
...(meId ? { ...(meId ? {
myReaction: populateMyReaction(), myReaction: populateMyReaction(note, meId, options?._hint_),
} : {}), } : {}),
} : {}), } : {}),
}); });
@ -275,13 +279,13 @@ export class NoteRepository extends Repository<Note> {
} }
if (!opts.skipHide) { if (!opts.skipHide) {
await this.hideNote(packed, meId); await hideNote(packed, meId);
} }
return packed; return packed;
} },
public async packMany( async packMany(
notes: Note[], notes: Note[],
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -296,7 +300,7 @@ export class NoteRepository extends Repository<Note> {
if (meId) { if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...notes.map(n => n.id), ...renoteIds]; const targets = [...notes.map(n => n.id), ...renoteIds];
const myReactions = await NoteReactions.find({ const myReactions = await NoteReactions.findBy({
userId: meId, userId: meId,
noteId: In(targets), noteId: In(targets),
}); });
@ -314,5 +318,5 @@ export class NoteRepository extends Repository<Note> {
myReactions: myReactionsMap, myReactions: myReactionsMap,
}, },
}))); })));
} },
} });

View file

@ -1,4 +1,4 @@
import { EntityRepository, In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js'; import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
import { Notification } from '@/models/entities/notification.js'; import { Notification } from '@/models/entities/notification.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
@ -8,10 +8,10 @@ import { NoteReaction } from '@/models/entities/note-reaction.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
import { notificationTypes } from '@/types.js'; import { notificationTypes } from '@/types.js';
import { db } from '@/db/postgre.js';
@EntityRepository(Notification) export const NotificationRepository = db.getRepository(Notification).extend({
export class NotificationRepository extends Repository<Notification> { async pack(
public async pack(
src: Notification['id'] | Notification, src: Notification['id'] | Notification,
options: { options: {
_hintForEachNotes_?: { _hintForEachNotes_?: {
@ -19,8 +19,8 @@ export class NotificationRepository extends Repository<Notification> {
}; };
} }
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.findOneOrFail(src); const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null; const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
@ -82,9 +82,9 @@ export class NotificationRepository extends Repository<Notification> {
icon: notification.customIcon || token?.iconUrl, icon: notification.customIcon || token?.iconUrl,
} : {}), } : {}),
}); });
} },
public async packMany( async packMany(
notifications: Notification[], notifications: Notification[],
meId: User['id'] meId: User['id']
) { ) {
@ -95,7 +95,7 @@ export class NotificationRepository extends Repository<Notification> {
const myReactionsMap = new Map<Note['id'], NoteReaction | null>(); const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!); const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const targets = [...noteIds, ...renoteIds]; const targets = [...noteIds, ...renoteIds];
const myReactions = await NoteReactions.find({ const myReactions = await NoteReactions.findBy({
userId: meId, userId: meId,
noteId: In(targets), noteId: In(targets),
}); });
@ -111,5 +111,5 @@ export class NotificationRepository extends Repository<Notification> {
myReactions: myReactionsMap, myReactions: myReactionsMap,
}, },
}))); })));
} },
} });

View file

@ -1,26 +1,25 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { PageLike } from '@/models/entities/page-like.js'; import { PageLike } from '@/models/entities/page-like.js';
import { Pages } from '../index.js'; import { Pages } from '../index.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(PageLike) export const PageLikeRepository = db.getRepository(PageLike).extend({
export class PageLikeRepository extends Repository<PageLike> { async pack(
public async pack(
src: PageLike['id'] | PageLike, src: PageLike['id'] | PageLike,
me?: { id: User['id'] } | null | undefined me?: { id: User['id'] } | null | undefined
) { ) {
const like = typeof src === 'object' ? src : await this.findOneOrFail(src); const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: like.id, id: like.id,
page: await Pages.pack(like.page || like.pageId, me), page: await Pages.pack(like.page || like.pageId, me),
}; };
} },
public packMany( packMany(
likes: any[], likes: any[],
me: { id: User['id'] } me: { id: User['id'] }
) { ) {
return Promise.all(likes.map(x => this.pack(x, me))); return Promise.all(likes.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,4 +1,4 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Page } from '@/models/entities/page.js'; import { Page } from '@/models/entities/page.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
import { Users, DriveFiles, PageLikes } from '../index.js'; import { Users, DriveFiles, PageLikes } from '../index.js';
@ -6,20 +6,19 @@ import { awaitAll } from '@/prelude/await-all.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
@EntityRepository(Page) export const PageRepository = db.getRepository(Page).extend({
export class PageRepository extends Repository<Page> { async pack(
public async pack(
src: Page['id'] | Page, src: Page['id'] | Page,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'Page'>> { ): Promise<Packed<'Page'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.findOneOrFail(src); const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const attachedFiles: Promise<DriveFile | undefined>[] = []; const attachedFiles: Promise<DriveFile | undefined>[] = [];
const collectFile = (xs: any[]) => { const collectFile = (xs: any[]) => {
for (const x of xs) { for (const x of xs) {
if (x.type === 'image') { if (x.type === 'image') {
attachedFiles.push(DriveFiles.findOne({ attachedFiles.push(DriveFiles.findOneBy({
id: x.fileId, id: x.fileId,
userId: page.userId, userId: page.userId,
})); }));
@ -76,14 +75,14 @@ export class PageRepository extends Repository<Page> {
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
}); });
} },
public packMany( packMany(
pages: Page[], pages: Page[],
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
) { ) {
return Promise.all(pages.map(x => this.pack(x, me))); return Promise.all(pages.map(x => this.pack(x, me)));
} },
} });

View file

@ -1,6 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Relay } from '@/models/entities/relay.js'; import { Relay } from '@/models/entities/relay.js';
@EntityRepository(Relay) export const RelayRepository = db.getRepository(Relay).extend({
export class RelayRepository extends Repository<Relay> { });
}

View file

@ -1,11 +1,10 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { Signin } from '@/models/entities/signin.js'; import { Signin } from '@/models/entities/signin.js';
@EntityRepository(Signin) export const SigninRepository = db.getRepository(Signin).extend({
export class SigninRepository extends Repository<Signin> { async pack(
public async pack(
src: Signin, src: Signin,
) { ) {
return src; return src;
} },
} });

View file

@ -1,23 +1,22 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js';
import { UserGroups } from '../index.js'; import { UserGroups } from '../index.js';
@EntityRepository(UserGroupInvitation) export const UserGroupInvitationRepository = db.getRepository(UserGroupInvitation).extend({
export class UserGroupInvitationRepository extends Repository<UserGroupInvitation> { async pack(
public async pack(
src: UserGroupInvitation['id'] | UserGroupInvitation, src: UserGroupInvitation['id'] | UserGroupInvitation,
) { ) {
const invitation = typeof src === 'object' ? src : await this.findOneOrFail(src); const invitation = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return { return {
id: invitation.id, id: invitation.id,
group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId), group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId),
}; };
} },
public packMany( packMany(
invitations: any[], invitations: any[],
) { ) {
return Promise.all(invitations.map(x => this.pack(x))); return Promise.all(invitations.map(x => this.pack(x)));
} },
} });

View file

@ -1,16 +1,15 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroup } from '@/models/entities/user-group.js';
import { UserGroupJoinings } from '../index.js'; import { UserGroupJoinings } from '../index.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
@EntityRepository(UserGroup) export const UserGroupRepository = db.getRepository(UserGroup).extend({
export class UserGroupRepository extends Repository<UserGroup> { async pack(
public async pack(
src: UserGroup['id'] | UserGroup, src: UserGroup['id'] | UserGroup,
): Promise<Packed<'UserGroup'>> { ): Promise<Packed<'UserGroup'>> {
const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src); const userGroup = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const users = await UserGroupJoinings.find({ const users = await UserGroupJoinings.findBy({
userGroupId: userGroup.id, userGroupId: userGroup.id,
}); });
@ -21,5 +20,5 @@ export class UserGroupRepository extends Repository<UserGroup> {
ownerId: userGroup.userId, ownerId: userGroup.userId,
userIds: users.map(x => x.userId), userIds: users.map(x => x.userId),
}; };
} },
} });

View file

@ -1,16 +1,15 @@
import { EntityRepository, Repository } from 'typeorm'; import { db } from '@/db/postgre.js';
import { UserList } from '@/models/entities/user-list.js'; import { UserList } from '@/models/entities/user-list.js';
import { UserListJoinings } from '../index.js'; import { UserListJoinings } from '../index.js';
import { Packed } from '@/misc/schema.js'; import { Packed } from '@/misc/schema.js';
@EntityRepository(UserList) export const UserListRepository = db.getRepository(UserList).extend({
export class UserListRepository extends Repository<UserList> { async pack(
public async pack(
src: UserList['id'] | UserList, src: UserList['id'] | UserList,
): Promise<Packed<'UserList'>> { ): Promise<Packed<'UserList'>> {
const userList = typeof src === 'object' ? src : await this.findOneOrFail(src); const userList = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const users = await UserListJoinings.find({ const users = await UserListJoinings.findBy({
userListId: userList.id, userListId: userList.id,
}); });
@ -20,5 +19,5 @@ export class UserListRepository extends Repository<UserList> {
name: userList.name, name: userList.name,
userIds: users.map(x => x.userId), userIds: users.map(x => x.userId),
}; };
} },
} });

View file

@ -10,6 +10,7 @@ import { getAntennas } from '@/misc/antenna-cache.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { Instance } from '../entities/instance.js'; import { Instance } from '../entities/instance.js';
import { db } from '@/db/postgre.js';
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3); const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
@ -23,51 +24,69 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
const ajv = new Ajv(); const ajv = new Ajv();
@EntityRepository(User) const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
export class UserRepository extends Repository<User> { const passwordSchema = { type: 'string', minLength: 1 } as const;
public localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
public passwordSchema = { type: 'string', minLength: 1 } as const; const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
public nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
public descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const; const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
public locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
public birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; function isLocalUser(user: User): user is ILocalUser;
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
function isLocalUser(user: User | { host: User['host'] }): boolean {
return user.host == null;
}
function isRemoteUser(user: User): user is IRemoteUser;
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
function isRemoteUser(user: User | { host: User['host'] }): boolean {
return !isLocalUser(user);
}
export const UserRepository = db.getRepository(User).extend({
localUsernameSchema,
passwordSchema,
nameSchema,
descriptionSchema,
locationSchema,
birthdaySchema,
//#region Validators //#region Validators
public validateLocalUsername = ajv.compile(this.localUsernameSchema); validateLocalUsername: ajv.compile(localUsernameSchema),
public validatePassword = ajv.compile(this.passwordSchema); validatePassword: ajv.compile(passwordSchema),
public validateName = ajv.compile(this.nameSchema); validateName: ajv.compile(nameSchema),
public validateDescription = ajv.compile(this.descriptionSchema); validateDescription: ajv.compile(descriptionSchema),
public validateLocation = ajv.compile(this.locationSchema); validateLocation: ajv.compile(locationSchema),
public validateBirthday = ajv.compile(this.birthdaySchema); validateBirthday: ajv.compile(birthdaySchema),
//#endregion //#endregion
public async getRelation(me: User['id'], target: User['id']) { async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOne({ Followings.findOneBy({
followerId: me, followerId: me,
followeeId: target, followeeId: target,
}), }),
Followings.findOne({ Followings.findOneBy({
followerId: target, followerId: target,
followeeId: me, followeeId: me,
}), }),
FollowRequests.findOne({ FollowRequests.findOneBy({
followerId: me, followerId: me,
followeeId: target, followeeId: target,
}), }),
FollowRequests.findOne({ FollowRequests.findOneBy({
followerId: target, followerId: target,
followeeId: me, followeeId: me,
}), }),
Blockings.findOne({ Blockings.findOneBy({
blockerId: me, blockerId: me,
blockeeId: target, blockeeId: target,
}), }),
Blockings.findOne({ Blockings.findOneBy({
blockerId: target, blockerId: target,
blockeeId: me, blockeeId: me,
}), }),
Mutings.findOne({ Mutings.findOneBy({
muterId: me, muterId: me,
muteeId: target, muteeId: target,
}), }),
@ -83,14 +102,14 @@ export class UserRepository extends Repository<User> {
isBlocked: fromBlocked != null, isBlocked: fromBlocked != null,
isMuted: mute != null, isMuted: mute != null,
}; };
} },
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> { async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
const mute = await Mutings.find({ const mute = await Mutings.findBy({
muterId: userId, muterId: userId,
}); });
const joinings = await UserGroupJoinings.find({ userId: userId }); const joinings = await UserGroupJoinings.findBy({ userId: userId });
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: j.userGroupId }) .where(`message.groupId = :groupId`, { groupId: j.userGroupId })
@ -112,44 +131,44 @@ export class UserRepository extends Repository<User> {
]); ]);
return withUser || withGroups.some(x => x); return withUser || withGroups.some(x => x);
} },
public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> { async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
const reads = await AnnouncementReads.find({ const reads = await AnnouncementReads.findBy({
userId: userId, userId: userId,
}); });
const count = await Announcements.count(reads.length > 0 ? { const count = await Announcements.countBy(reads.length > 0 ? {
id: Not(In(reads.map(read => read.announcementId))), id: Not(In(reads.map(read => read.announcementId))),
} : {}); } : {});
return count > 0; return count > 0;
} },
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> { async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
const myAntennas = (await getAntennas()).filter(a => a.userId === userId); const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
const unread = myAntennas.length > 0 ? await AntennaNotes.findOne({ const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({
antennaId: In(myAntennas.map(x => x.id)), antennaId: In(myAntennas.map(x => x.id)),
read: false, read: false,
}) : null; }) : null;
return unread != null; return unread != null;
} },
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> { async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
const channels = await ChannelFollowings.find({ followerId: userId }); const channels = await ChannelFollowings.findBy({ followerId: userId });
const unread = channels.length > 0 ? await NoteUnreads.findOne({ const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
userId: userId, userId: userId,
noteChannelId: In(channels.map(x => x.followeeId)), noteChannelId: In(channels.map(x => x.followeeId)),
}) : null; }) : null;
return unread != null; return unread != null;
} },
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> { async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
const mute = await Mutings.find({ const mute = await Mutings.findBy({
muterId: userId, muterId: userId,
}); });
const mutedUserIds = mute.map(m => m.muteeId); const mutedUserIds = mute.map(m => m.muteeId);
@ -164,17 +183,17 @@ export class UserRepository extends Repository<User> {
}); });
return count > 0; return count > 0;
} },
public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> { async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
const count = await FollowRequests.count({ const count = await FollowRequests.countBy({
followeeId: userId, followeeId: userId,
}); });
return count > 0; return count > 0;
} },
public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' { getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
if (user.hideOnlineStatus) return 'unknown'; if (user.hideOnlineStatus) return 'unknown';
if (user.lastActiveDate == null) return 'unknown'; if (user.lastActiveDate == null) return 'unknown';
const elapsed = Date.now() - user.lastActiveDate.getTime(); const elapsed = Date.now() - user.lastActiveDate.getTime();
@ -183,22 +202,22 @@ export class UserRepository extends Repository<User> {
elapsed < USER_ACTIVE_THRESHOLD ? 'active' : elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
'offline' 'offline'
); );
} },
public getAvatarUrl(user: User): string { getAvatarUrl(user: User): string {
// TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング // TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング
if (user.avatar) { if (user.avatar) {
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id); return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
} else { } else {
return this.getIdenticonUrl(user.id); return this.getIdenticonUrl(user.id);
} }
} },
public getIdenticonUrl(userId: User['id']): string { getIdenticonUrl(userId: User['id']): string {
return `${config.url}/identicon/${userId}`; return `${config.url}/identicon/${userId}`;
} },
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>( async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
src: User['id'] | User, src: User['id'] | User,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -215,11 +234,15 @@ export class UserRepository extends Repository<User> {
if (typeof src === 'object') { if (typeof src === 'object') {
user = src; user = src;
if (src.avatar === undefined && src.avatarId) src.avatar = await DriveFiles.findOne(src.avatarId) ?? null; if (src.avatar === undefined && src.avatarId) src.avatar = await DriveFiles.findOneBy({ id: src.avatarId }) ?? null;
if (src.banner === undefined && src.bannerId) src.banner = await DriveFiles.findOne(src.bannerId) ?? null; if (src.banner === undefined && src.bannerId) src.banner = await DriveFiles.findOneBy({ id: src.bannerId }) ?? null;
} else { } else {
user = await this.findOneOrFail(src, { user = await this.findOneOrFail({
relations: ['avatar', 'banner'], where: { id: src },
relations: {
avatar: true,
banner: true,
},
}); });
} }
@ -232,7 +255,7 @@ export class UserRepository extends Repository<User> {
.innerJoinAndSelect('pin.note', 'note') .innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC') .orderBy('pin.id', 'DESC')
.getMany() : []; .getMany() : [];
const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null; const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null;
const followingCount = profile == null ? null : const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount : (profile.ffVisibility === 'public') || isMe ? user.followingCount :
@ -258,9 +281,8 @@ export class UserRepository extends Repository<User> {
isModerator: user.isModerator || falsy, isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy, isBot: user.isBot || falsy,
isCat: user.isCat || falsy, isCat: user.isCat || falsy,
// TODO: typeorm 3.0にしたら .then(x => x || null) は消せる
instance: user.host ? userInstanceCache.fetch(user.host, instance: user.host ? userInstanceCache.fetch(user.host,
() => Instances.findOne({ host: user.host }).then(x => x || null), () => Instances.findOneBy({ host: user.host! }),
v => v != null v => v != null
).then(instance => instance ? { ).then(instance => instance ? {
name: instance.name, name: instance.name,
@ -304,7 +326,7 @@ export class UserRepository extends Repository<User> {
twoFactorEnabled: profile!.twoFactorEnabled, twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin, usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled securityKeys: profile!.twoFactorEnabled
? UserSecurityKeys.count({ ? UserSecurityKeys.countBy({
userId: user.id, userId: user.id,
}).then(result => result >= 1) }).then(result => result >= 1)
: false, : false,
@ -352,7 +374,11 @@ export class UserRepository extends Repository<User> {
where: { where: {
userId: user.id, userId: user.id,
}, },
select: ['id', 'name', 'lastUsed'], select: {
id: true,
name: true,
lastUsed: true,
},
}) })
: [], : [],
} : {}), } : {}),
@ -369,9 +395,9 @@ export class UserRepository extends Repository<User> {
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; } as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
return await awaitAll(packed); return await awaitAll(packed);
} },
public packMany<D extends boolean = false>( packMany<D extends boolean = false>(
users: (User['id'] | User)[], users: (User['id'] | User)[],
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
options?: { options?: {
@ -380,17 +406,8 @@ export class UserRepository extends Repository<User> {
} }
): Promise<IsUserDetailed<D>[]> { ): Promise<IsUserDetailed<D>[]> {
return Promise.all(users.map(u => this.pack(u, me, options))); return Promise.all(users.map(u => this.pack(u, me, options)));
} },
public isLocalUser(user: User): user is ILocalUser; isLocalUser,
public isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; }; isRemoteUser,
public isLocalUser(user: User | { host: User['host'] }): boolean { });
return user.host == null;
}
public isRemoteUser(user: User): user is IRemoteUser;
public isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
public isRemoteUser(user: User | { host: User['host'] }): boolean {
return !this.isLocalUser(user);
}
}

View file

@ -13,7 +13,7 @@ const logger = queueLogger.createSubLogger('delete-account');
export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> { export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
logger.info(`Deleting account of ${job.data.user.id} ...`); logger.info(`Deleting account of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
return; return;
} }
@ -75,7 +75,7 @@ export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise
} }
{ // Send email notification { // Send email notification
const profile = await UserProfiles.findOneOrFail(user.id); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.email && profile.emailVerified) { if (profile.email && profile.emailVerified) {
sendEmail(profile.email, 'Account deleted', sendEmail(profile.email, 'Account deleted',
`Your account has been deleted.`, `Your account has been deleted.`,

View file

@ -11,7 +11,7 @@ const logger = queueLogger.createSubLogger('delete-drive-files');
export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any): Promise<void> { export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
logger.info(`Deleting drive files of ${job.data.user.id} ...`); logger.info(`Deleting drive files of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
@ -44,7 +44,7 @@ export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any):
deletedCount++; deletedCount++;
} }
const total = await DriveFiles.count({ const total = await DriveFiles.countBy({
userId: user.id, userId: user.id,
}); });

View file

@ -15,7 +15,7 @@ const logger = queueLogger.createSubLogger('export-blocking');
export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): Promise<void> { export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
logger.info(`Exporting blocking of ${job.data.user.id} ...`); logger.info(`Exporting blocking of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
@ -56,7 +56,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
cursor = blockings[blockings.length - 1].id; cursor = blockings[blockings.length - 1].id;
for (const block of blockings) { for (const block of blockings) {
const u = await Users.findOne({ id: block.blockeeId }); const u = await Users.findOneBy({ id: block.blockeeId });
if (u == null) { if (u == null) {
exportedCount++; continue; exportedCount++; continue;
} }
@ -75,7 +75,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
exportedCount++; exportedCount++;
} }
const total = await Blockings.count({ const total = await Blockings.countBy({
blockerId: user.id, blockerId: user.id,
}); });

View file

@ -12,13 +12,14 @@ import { Users, Emojis } from '@/models/index.js';
import { } from '@/queue/types.js'; import { } from '@/queue/types.js';
import { downloadUrl } from '@/misc/download-url.js'; import { downloadUrl } from '@/misc/download-url.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { IsNull } from 'typeorm';
const logger = queueLogger.createSubLogger('export-custom-emojis'); const logger = queueLogger.createSubLogger('export-custom-emojis');
export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promise<void> { export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promise<void> {
logger.info(`Exporting custom emojis ...`); logger.info(`Exporting custom emojis ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
@ -57,7 +58,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
const customEmojis = await Emojis.find({ const customEmojis = await Emojis.find({
where: { where: {
host: null, host: IsNull(),
}, },
order: { order: {
id: 'ASC', id: 'ASC',

View file

@ -16,7 +16,7 @@ const logger = queueLogger.createSubLogger('export-following');
export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> { export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
logger.info(`Exporting following of ${job.data.user.id} ...`); logger.info(`Exporting following of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
@ -36,7 +36,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
let cursor: Following['id'] | null = null; let cursor: Following['id'] | null = null;
const mutings = job.data.excludeMuting ? await Mutings.find({ const mutings = job.data.excludeMuting ? await Mutings.findBy({
muterId: user.id, muterId: user.id,
}) : []; }) : [];
@ -60,7 +60,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
cursor = followings[followings.length - 1].id; cursor = followings[followings.length - 1].id;
for (const following of followings) { for (const following of followings) {
const u = await Users.findOne({ id: following.followeeId }); const u = await Users.findOneBy({ id: following.followeeId });
if (u == null) { if (u == null) {
continue; continue;
} }

View file

@ -15,7 +15,7 @@ const logger = queueLogger.createSubLogger('export-mute');
export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promise<void> { export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
logger.info(`Exporting mute of ${job.data.user.id} ...`); logger.info(`Exporting mute of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
@ -57,7 +57,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
cursor = mutes[mutes.length - 1].id; cursor = mutes[mutes.length - 1].id;
for (const mute of mutes) { for (const mute of mutes) {
const u = await Users.findOne({ id: mute.muteeId }); const u = await Users.findOneBy({ id: mute.muteeId });
if (u == null) { if (u == null) {
exportedCount++; continue; exportedCount++; continue;
} }
@ -76,7 +76,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
exportedCount++; exportedCount++;
} }
const total = await Mutings.count({ const total = await Mutings.countBy({
muterId: user.id, muterId: user.id,
}); });

View file

@ -16,7 +16,7 @@ const logger = queueLogger.createSubLogger('export-notes');
export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Promise<void> { export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
logger.info(`Exporting notes of ${job.data.user.id} ...`); logger.info(`Exporting notes of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
@ -74,7 +74,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
for (const note of notes) { for (const note of notes) {
let poll: Poll | undefined; let poll: Poll | undefined;
if (note.hasPoll) { if (note.hasPoll) {
poll = await Polls.findOneOrFail({ noteId: note.id }); poll = await Polls.findOneByOrFail({ noteId: note.id });
} }
const content = JSON.stringify(serialize(note, poll)); const content = JSON.stringify(serialize(note, poll));
const isFirst = exportedNotesCount === 0; const isFirst = exportedNotesCount === 0;
@ -82,7 +82,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
exportedNotesCount++; exportedNotesCount++;
} }
const total = await Notes.count({ const total = await Notes.countBy({
userId: user.id, userId: user.id,
}); });

View file

@ -15,13 +15,13 @@ const logger = queueLogger.createSubLogger('export-user-lists');
export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): Promise<void> { export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
logger.info(`Exporting user lists of ${job.data.user.id} ...`); logger.info(`Exporting user lists of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
} }
const lists = await UserLists.find({ const lists = await UserLists.findBy({
userId: user.id, userId: user.id,
}); });
@ -38,8 +38,8 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
const stream = fs.createWriteStream(path, { flags: 'a' }); const stream = fs.createWriteStream(path, { flags: 'a' });
for (const list of lists) { for (const list of lists) {
const joinings = await UserListJoinings.find({ userListId: list.id }); const joinings = await UserListJoinings.findBy({ userListId: list.id });
const users = await Users.find({ const users = await Users.findBy({
id: In(joinings.map(j => j.userId)), id: In(joinings.map(j => j.userId)),
}); });

View file

@ -8,19 +8,20 @@ import { isSelfHost, toPuny } from '@/misc/convert-host.js';
import { Users, DriveFiles, Blockings } from '@/models/index.js'; import { Users, DriveFiles, Blockings } from '@/models/index.js';
import { DbUserImportJobData } from '@/queue/types.js'; import { DbUserImportJobData } from '@/queue/types.js';
import block from '@/services/blocking/create.js'; import block from '@/services/blocking/create.js';
import { IsNull } from 'typeorm';
const logger = queueLogger.createSubLogger('import-blocking'); const logger = queueLogger.createSubLogger('import-blocking');
export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> { export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
logger.info(`Importing blocking of ${job.data.user.id} ...`); logger.info(`Importing blocking of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
} }
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOneBy({
id: job.data.fileId, id: job.data.fileId,
}); });
if (file == null) { if (file == null) {
@ -39,10 +40,10 @@ export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: a
const acct = line.split(',')[0].trim(); const acct = line.split(',')[0].trim();
const { username, host } = Acct.parse(acct); const { username, host } = Acct.parse(acct);
let target = isSelfHost(host!) ? await Users.findOne({ let target = isSelfHost(host!) ? await Users.findOneBy({
host: null, host: IsNull(),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}) : await Users.findOne({ }) : await Users.findOneBy({
host: toPuny(host!), host: toPuny(host!),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}); });

View file

@ -2,7 +2,6 @@ import Bull from 'bull';
import * as tmp from 'tmp'; import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import unzipper from 'unzipper'; import unzipper from 'unzipper';
import { getConnection } from 'typeorm';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
import { downloadUrl } from '@/misc/download-url.js'; import { downloadUrl } from '@/misc/download-url.js';
@ -10,6 +9,7 @@ import { DriveFiles, Emojis } from '@/models/index.js';
import { DbUserImportJobData } from '@/queue/types.js'; import { DbUserImportJobData } from '@/queue/types.js';
import { addFile } from '@/services/drive/add-file.js'; import { addFile } from '@/services/drive/add-file.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { db } from '@/db/postgre.js';
const logger = queueLogger.createSubLogger('import-custom-emojis'); const logger = queueLogger.createSubLogger('import-custom-emojis');
@ -17,7 +17,7 @@ const logger = queueLogger.createSubLogger('import-custom-emojis');
export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> { export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
logger.info(`Importing custom emojis ...`); logger.info(`Importing custom emojis ...`);
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOneBy({
id: job.data.fileId, id: job.data.fileId,
}); });
if (file == null) { if (file == null) {
@ -72,10 +72,10 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
originalUrl: driveFile.url, originalUrl: driveFile.url,
publicUrl: driveFile.webpublicUrl ?? driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url,
type: driveFile.webpublicType ?? driveFile.type, type: driveFile.webpublicType ?? driveFile.type,
}).then(x => Emojis.findOneOrFail(x.identifiers[0])); }).then(x => Emojis.findOneByOrFail(x.identifiers[0]));
} }
await getConnection().queryResultCache!.remove(['meta_emojis']); await db.queryResultCache!.remove(['meta_emojis']);
cleanup(); cleanup();

View file

@ -8,19 +8,20 @@ import { downloadTextFile } from '@/misc/download-text-file.js';
import { isSelfHost, toPuny } from '@/misc/convert-host.js'; import { isSelfHost, toPuny } from '@/misc/convert-host.js';
import { Users, DriveFiles } from '@/models/index.js'; import { Users, DriveFiles } from '@/models/index.js';
import { DbUserImportJobData } from '@/queue/types.js'; import { DbUserImportJobData } from '@/queue/types.js';
import { IsNull } from 'typeorm';
const logger = queueLogger.createSubLogger('import-following'); const logger = queueLogger.createSubLogger('import-following');
export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> { export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
logger.info(`Importing following of ${job.data.user.id} ...`); logger.info(`Importing following of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
} }
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOneBy({
id: job.data.fileId, id: job.data.fileId,
}); });
if (file == null) { if (file == null) {
@ -39,10 +40,10 @@ export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done:
const acct = line.split(',')[0].trim(); const acct = line.split(',')[0].trim();
const { username, host } = Acct.parse(acct); const { username, host } = Acct.parse(acct);
let target = isSelfHost(host!) ? await Users.findOne({ let target = isSelfHost(host!) ? await Users.findOneBy({
host: null, host: IsNull(),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}) : await Users.findOne({ }) : await Users.findOneBy({
host: toPuny(host!), host: toPuny(host!),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}); });

View file

@ -9,19 +9,20 @@ import { Users, DriveFiles, Mutings } from '@/models/index.js';
import { DbUserImportJobData } from '@/queue/types.js'; import { DbUserImportJobData } from '@/queue/types.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { IsNull } from 'typeorm';
const logger = queueLogger.createSubLogger('import-muting'); const logger = queueLogger.createSubLogger('import-muting');
export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> { export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
logger.info(`Importing muting of ${job.data.user.id} ...`); logger.info(`Importing muting of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
} }
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOneBy({
id: job.data.fileId, id: job.data.fileId,
}); });
if (file == null) { if (file == null) {
@ -40,10 +41,10 @@ export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any
const acct = line.split(',')[0].trim(); const acct = line.split(',')[0].trim();
const { username, host } = Acct.parse(acct); const { username, host } = Acct.parse(acct);
let target = isSelfHost(host!) ? await Users.findOne({ let target = isSelfHost(host!) ? await Users.findOneBy({
host: null, host: IsNull(),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}) : await Users.findOne({ }) : await Users.findOneBy({
host: toPuny(host!), host: toPuny(host!),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}); });

View file

@ -9,19 +9,20 @@ import { isSelfHost, toPuny } from '@/misc/convert-host.js';
import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index.js'; import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
import { DbUserImportJobData } from '@/queue/types.js'; import { DbUserImportJobData } from '@/queue/types.js';
import { IsNull } from 'typeorm';
const logger = queueLogger.createSubLogger('import-user-lists'); const logger = queueLogger.createSubLogger('import-user-lists');
export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> { export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
logger.info(`Importing user lists of ${job.data.user.id} ...`); logger.info(`Importing user lists of ${job.data.user.id} ...`);
const user = await Users.findOne(job.data.user.id); const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) { if (user == null) {
done(); done();
return; return;
} }
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOneBy({
id: job.data.fileId, id: job.data.fileId,
}); });
if (file == null) { if (file == null) {
@ -40,7 +41,7 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
const listName = line.split(',')[0].trim(); const listName = line.split(',')[0].trim();
const { username, host } = Acct.parse(line.split(',')[1].trim()); const { username, host } = Acct.parse(line.split(',')[1].trim());
let list = await UserLists.findOne({ let list = await UserLists.findOneBy({
userId: user.id, userId: user.id,
name: listName, name: listName,
}); });
@ -51,13 +52,13 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
name: listName, name: listName,
}).then(x => UserLists.findOneOrFail(x.identifiers[0])); }).then(x => UserLists.findOneByOrFail(x.identifiers[0]));
} }
let target = isSelfHost(host!) ? await Users.findOne({ let target = isSelfHost(host!) ? await Users.findOneBy({
host: null, host: IsNull(),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}) : await Users.findOne({ }) : await Users.findOneBy({
host: toPuny(host!), host: toPuny(host!),
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
}); });
@ -66,7 +67,7 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
target = await resolveUser(username, host); target = await resolveUser(username, host);
} }
if (await UserListJoinings.findOne({ userListId: list!.id, userId: target.id }) != null) continue; if (await UserListJoinings.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
pushUserToUserList(target, list!); pushUserToUserList(target, list!);
} catch (e) { } catch (e) {

View file

@ -8,7 +8,7 @@ import { createNotification } from '@/services/create-notification.js';
const logger = queueLogger.createSubLogger('ended-poll-notification'); const logger = queueLogger.createSubLogger('ended-poll-notification');
export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJobData>, done: any): Promise<void> { export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJobData>, done: any): Promise<void> {
const note = await Notes.findOne(job.data.noteId); const note = await Notes.findOneBy({ id: job.data.noteId });
if (note == null || !note.hasPoll) { if (note == null || !note.hasPoll) {
done(); done();
return; return;

View file

@ -15,6 +15,8 @@ import DbResolver from '@/remote/activitypub/db-resolver.js';
import { resolvePerson } from '@/remote/activitypub/models/person.js'; import { resolvePerson } from '@/remote/activitypub/models/person.js';
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js'; import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
import { StatusError } from '@/misc/fetch.js'; import { StatusError } from '@/misc/fetch.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
const logger = new Logger('inbox'); const logger = new Logger('inbox');
@ -42,11 +44,13 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `Old keyId is no longer supported. ${keyIdLower}`; return `Old keyId is no longer supported. ${keyIdLower}`;
} }
// TDOO: キャッシュ
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
// HTTP-Signature keyIdを元にDBから取得 // HTTP-Signature keyIdを元にDBから取得
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); let authUser: {
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) { if (authUser == null) {

View file

@ -37,7 +37,7 @@ export default async function cleanRemoteFiles(job: Bull.Job<Record<string, unkn
deletedCount += 8; deletedCount += 8;
const total = await DriveFiles.count({ const total = await DriveFiles.countBy({
userHost: Not(IsNull()), userHost: Not(IsNull()),
isLink: false, isLink: false,
}); });

View file

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

View file

@ -1,12 +1,17 @@
import escapeRegexp from 'escape-regexp';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { User, IRemoteUser } from '@/models/entities/user.js'; import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js'; import { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { IObject, getApId } from './type.js'; import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js'; import { resolvePerson } from './models/person.js';
import escapeRegexp from 'escape-regexp'; import { Cache } from '@/misc/cache.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export default class DbResolver { export default class DbResolver {
constructor() { constructor() {
@ -19,15 +24,15 @@ export default class DbResolver {
const parsed = this.parseUri(value); const parsed = this.parseUri(value);
if (parsed.id) { if (parsed.id) {
return (await Notes.findOne({ return await Notes.findOneBy({
id: parsed.id, id: parsed.id,
})) || null; });
} }
if (parsed.uri) { if (parsed.uri) {
return (await Notes.findOne({ return await Notes.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})) || null; });
} }
return null; return null;
@ -37,15 +42,15 @@ export default class DbResolver {
const parsed = this.parseUri(value); const parsed = this.parseUri(value);
if (parsed.id) { if (parsed.id) {
return (await MessagingMessages.findOne({ return await MessagingMessages.findOneBy({
id: parsed.id, id: parsed.id,
})) || null; });
} }
if (parsed.uri) { if (parsed.uri) {
return (await MessagingMessages.findOne({ return await MessagingMessages.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})) || null; });
} }
return null; return null;
@ -54,19 +59,19 @@ export default class DbResolver {
/** /**
* AP Person => Misskey User in DB * AP Person => Misskey User in DB
*/ */
public async getUserFromApId(value: string | IObject): Promise<User | null> { public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
const parsed = this.parseUri(value); const parsed = this.parseUri(value);
if (parsed.id) { if (parsed.id) {
return (await Users.findOne({ return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id, id: parsed.id,
})) || null; }).then(x => x ?? undefined)) ?? null;
} }
if (parsed.uri) { if (parsed.uri) {
return (await Users.findOne({ return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})) || null; }));
} }
return null; return null;
@ -75,17 +80,24 @@ export default class DbResolver {
/** /**
* AP KeyId => Misskey User and Key * AP KeyId => Misskey User and Key
*/ */
public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> { public async getAuthUserFromKeyId(keyId: string): Promise<{
const key = await UserPublickeys.findOne({ user: CacheableRemoteUser;
key: UserPublickey;
} | null> {
const key = await publicKeyCache.fetch(keyId, async () => {
const key = await UserPublickeys.findOneBy({
keyId, keyId,
}, {
relations: ['user'],
}); });
if (key == null) return null; if (key == null) return null;
return key;
}, key => key != null);
if (key == null) return null;
return { return {
user: key.user as IRemoteUser, user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser,
key, key,
}; };
} }
@ -93,12 +105,15 @@ export default class DbResolver {
/** /**
* AP Actor id => Misskey User and Key * AP Actor id => Misskey User and Key
*/ */
public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> { public async getAuthUserFromApId(uri: string): Promise<{
const user = await resolvePerson(uri) as IRemoteUser; user: CacheableRemoteUser;
key: UserPublickey | null;
} | null> {
const user = await resolvePerson(uri) as CacheableRemoteUser;
if (user == null) return null; if (user == null) return null;
const key = await UserPublickeys.findOne(user.id); const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null);
return { return {
user, user,
@ -125,11 +140,6 @@ export default class DbResolver {
} }
} }
export type AuthUser = {
user: IRemoteUser;
key?: UserPublickey;
};
type UriParseResult = { type UriParseResult = {
/** id in DB (local object only) */ /** id in DB (local object only) */
id?: string; id?: string;

View file

@ -1,6 +1,7 @@
import { Users, Followings } from '@/models/index.js'; import { Users, Followings } from '@/models/index.js';
import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js'; import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js';
import { deliver } from '@/queue/index.js'; import { deliver } from '@/queue/index.js';
import { IsNull, Not } from 'typeorm';
//#region types //#region types
interface IRecipe { interface IRecipe {
@ -82,16 +83,26 @@ export default class DeliverManager {
for (const recipe of this.recipes) { for (const recipe of this.recipes) {
if (isFollowers(recipe)) { if (isFollowers(recipe)) {
// followers deliver // followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await Followings.find({ const followers = await Followings.find({
where: {
followeeId: this.actor.id, followeeId: this.actor.id,
}); followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
for (const following of followers) { for (const following of followers) {
if (Followings.isRemoteFollower(following)) {
const inbox = following.followerSharedInbox || following.followerInbox; const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox); inboxes.add(inbox);
} }
}
} else if (isDirect(recipe)) { } else if (isDirect(recipe)) {
// direct deliver // direct deliver
const inbox = recipe.to.inbox; const inbox = recipe.to.inbox;
@ -112,7 +123,7 @@ export default class DeliverManager {
* @param activity Activity * @param activity Activity
* @param from Followee * @param from Followee
*/ */
export async function deliverToFollowers(actor: ILocalUser, activity: any) { export async function deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) {
const manager = new DeliverManager(actor, activity); const manager = new DeliverManager(actor, activity);
manager.addFollowersRecipe(); manager.addFollowersRecipe();
await manager.execute(); await manager.execute();
@ -123,7 +134,7 @@ export async function deliverToFollowers(actor: ILocalUser, activity: any) {
* @param activity Activity * @param activity Activity
* @param to Target user * @param to Target user
*/ */
export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) { export async function deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) {
const manager = new DeliverManager(actor, activity); const manager = new DeliverManager(actor, activity);
manager.addDirectRecipe(to); manager.addDirectRecipe(to);
await manager.execute(); await manager.execute();

View file

@ -1,10 +1,10 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import accept from '@/services/following/requests/accept.js'; import accept from '@/services/following/requests/accept.js';
import { IFollow } from '../../type.js'; import { IFollow } from '../../type.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
import { relayAccepted } from '@/services/relay.js'; import { relayAccepted } from '@/services/relay.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import acceptFollow from './follow.js'; import acceptFollow from './follow.js';
import { IAccept, isFollow, getApType } from '../../type.js'; import { IAccept, isFollow, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
const logger = apLogger; const logger = apLogger;
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => {
const uri = activity.id || activity; const uri = activity.id || activity;
logger.info(`Accept: ${uri}`); logger.info(`Accept: ${uri}`);

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IAdd } from '../../type.js'; import { IAdd } from '../../type.js';
import { resolveNote } from '../../models/note.js'; import { resolveNote } from '../../models/note.js';
import { addPinned } from '@/services/i/pin.js'; import { addPinned } from '@/services/i/pin.js';
export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: IAdd): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import announceNote from './note.js'; import announceNote from './note.js';
import { IAnnounce, getApId } from '../../type.js'; import { IAnnounce, getApId } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
const logger = apLogger; const logger = apLogger;
export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<void> => {
const uri = getApId(activity); const uri = getApId(activity);
logger.info(`Announce: ${uri}`); logger.info(`Announce: ${uri}`);

View file

@ -1,6 +1,6 @@
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import post from '@/services/note/create.js'; import post from '@/services/note/create.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IAnnounce, getApId } from '../../type.js'; import { IAnnounce, getApId } from '../../type.js';
import { fetchNote, resolveNote } from '../../models/note.js'; import { fetchNote, resolveNote } from '../../models/note.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
@ -15,10 +15,9 @@ const logger = apLogger;
/** /**
* *
*/ */
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> { export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
const uri = getApId(activity); const uri = getApId(activity);
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
return; return;
} }

View file

@ -1,9 +1,10 @@
import { IBlock } from '../../type.js'; import { IBlock } from '../../type.js';
import block from '@/services/blocking/create.js'; import block from '@/services/blocking/create.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
import { Users } from '@/models/index.js';
export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
@ -17,6 +18,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`; return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
} }
await block(actor, blockee); await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));
return `ok`; return `ok`;
}; };

View file

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

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { createNote, fetchNote } from '../../models/note.js'; import { createNote, fetchNote } from '../../models/note.js';
import { getApId, IObject, ICreate } from '../../type.js'; import { getApId, IObject, ICreate } from '../../type.js';
import { getApLock } from '@/misc/app-lock.js'; import { getApLock } from '@/misc/app-lock.js';
@ -9,7 +9,7 @@ import { StatusError } from '@/misc/fetch.js';
/** /**
* 稿 * 稿
*/ */
export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> { export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
const uri = getApId(note); const uri = getApId(note);
if (typeof note === 'object') { if (typeof note === 'object') {

View file

@ -1,18 +1,19 @@
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import { createDeleteAccountJob } from '@/queue/index.js'; import { createDeleteAccountJob } from '@/queue/index.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { Users } from '@/models/index.js'; import { Users } from '@/models/index.js';
const logger = apLogger; const logger = apLogger;
export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> { export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
logger.info(`Deleting the Actor: ${uri}`); logger.info(`Deleting the Actor: ${uri}`);
if (actor.uri !== uri) { if (actor.uri !== uri) {
return `skip: delete actor ${actor.uri} !== ${uri}`; return `skip: delete actor ${actor.uri} !== ${uri}`;
} }
if (actor.isDeleted) { const user = await Users.findOneByOrFail({ id: actor.id });
if (user.isDeleted) {
logger.info(`skip: already deleted`); logger.info(`skip: already deleted`);
} }

View file

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

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import deleteNode from '@/services/note/delete.js'; import deleteNode from '@/services/note/delete.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
@ -7,7 +7,7 @@ import { deleteMessage } from '@/services/messages/delete.js';
const logger = apLogger; const logger = apLogger;
export default async function(actor: IRemoteUser, uri: string): Promise<string> { export default async function(actor: CacheableRemoteUser, uri: string): Promise<string> {
logger.info(`Deleting the Note: ${uri}`); logger.info(`Deleting the Note: ${uri}`);
const unlock = await getApLock(uri); const unlock = await getApLock(uri);

View file

@ -1,17 +1,17 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { IFlag, getApIds } from '../../type.js'; import { IFlag, getApIds } from '../../type.js';
import { AbuseUserReports, Users } from '@/models/index.js'; import { AbuseUserReports, Users } from '@/models/index.js';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IFlag): Promise<string> => {
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object); const uris = getApIds(activity.object);
const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!); const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!);
const users = await Users.find({ const users = await Users.findBy({
id: In(userIds), id: In(userIds),
}); });
if (users.length < 1) return `skip`; if (users.length < 1) return `skip`;

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import follow from '@/services/following/create.js'; import follow from '@/services/following/create.js';
import { IFollow } from '../type.js'; import { IFollow } from '../type.js';
import DbResolver from '../db-resolver.js'; import DbResolver from '../db-resolver.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object); const followee = await dbResolver.getUserFromApId(activity.object);

View file

@ -1,5 +1,5 @@
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js'; import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import create from './create/index.js'; import create from './create/index.js';
import performDeleteActivity from './delete/index.js'; import performDeleteActivity from './delete/index.js';
import performUpdateActivity from './update/index.js'; import performUpdateActivity from './update/index.js';
@ -17,8 +17,9 @@ import flag from './flag/index.js';
import { apLogger } from '../logger.js'; import { apLogger } from '../logger.js';
import Resolver from '../resolver.js'; import Resolver from '../resolver.js';
import { toArray } from '@/prelude/array.js'; import { toArray } from '@/prelude/array.js';
import { Users } from '@/models/index.js';
export async function performActivity(actor: IRemoteUser, activity: IObject) { export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
if (isCollectionOrOrderedCollection(activity)) { if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver(); const resolver = new Resolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
@ -36,7 +37,7 @@ export async function performActivity(actor: IRemoteUser, activity: IObject) {
} }
} }
async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> { async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise<void> {
if (actor.isSuspended) return; if (actor.isSuspended) return;
if (isCreate(activity)) { if (isCreate(activity)) {

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { ILike, getApId } from '../type.js'; import { ILike, getApId } from '../type.js';
import create from '@/services/note/reaction/create.js'; import create from '@/services/note/reaction/create.js';
import { fetchNote, extractEmojis } from '../models/note.js'; import { fetchNote, extractEmojis } from '../models/note.js';
export default async (actor: IRemoteUser, activity: ILike) => { export default async (actor: CacheableRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object); const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri); const note = await fetchNote(targetUri);

View file

@ -1,10 +1,10 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRead, getApId } from '../type.js'; import { IRead, getApId } from '../type.js';
import { isSelfHost, extractDbHost } from '@/misc/convert-host.js'; import { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
import { MessagingMessages } from '@/models/index.js'; import { MessagingMessages } from '@/models/index.js';
import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message.js'; import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message.js';
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => { export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise<string> => {
const id = await getApId(activity.object); const id = await getApId(activity.object);
if (!isSelfHost(extractDbHost(id))) { if (!isSelfHost(extractDbHost(id))) {
@ -13,7 +13,7 @@ export const performReadActivity = async (actor: IRemoteUser, activity: IRead):
const messageId = id.split('/').pop(); const messageId = id.split('/').pop();
const message = await MessagingMessages.findOne(messageId); const message = await MessagingMessages.findOneBy({ id: messageId });
if (message == null) { if (message == null) {
return `skip: message not found`; return `skip: message not found`;
} }

View file

@ -1,11 +1,11 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { remoteReject } from '@/services/following/reject.js'; import { remoteReject } from '@/services/following/reject.js';
import { IFollow } from '../../type.js'; import { IFollow } from '../../type.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
import { relayRejected } from '@/services/relay.js'; import { relayRejected } from '@/services/relay.js';
import { Users } from '@/models/index.js'; import { Users } from '@/models/index.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import rejectFollow from './follow.js'; import rejectFollow from './follow.js';
import { IReject, isFollow, getApType } from '../../type.js'; import { IReject, isFollow, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
const logger = apLogger; const logger = apLogger;
export default async (actor: IRemoteUser, activity: IReject): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IReject): Promise<string> => {
const uri = activity.id || activity; const uri = activity.id || activity;
logger.info(`Reject: ${uri}`); logger.info(`Reject: ${uri}`);

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemove } from '../../type.js'; import { IRemove } from '../../type.js';
import { resolveNote } from '../../models/note.js'; import { resolveNote } from '../../models/note.js';
import { removePinned } from '@/services/i/pin.js'; import { removePinned } from '@/services/i/pin.js';
export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: IRemove): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js'; import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js'; import cancelRequest from '@/services/following/requests/cancel.js';
import {IAccept} from '../../type.js'; import {IAccept} from '../../type.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { Followings } from '@/models/index.js'; import { Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object); const follower = await dbResolver.getUserFromApId(activity.object);
@ -13,7 +13,7 @@ export default async (actor: IRemoteUser, activity: IAccept): Promise<string> =>
return `skip: follower not found`; return `skip: follower not found`;
} }
const following = await Followings.findOne({ const following = await Followings.findOneBy({
followerId: follower.id, followerId: follower.id,
followeeId: actor.id, followeeId: actor.id,
}); });

View file

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

View file

@ -1,9 +1,10 @@
import { IBlock } from '../../type.js'; import { IBlock } from '../../type.js';
import unblock from '@/services/blocking/delete.js'; import unblock from '@/services/blocking/delete.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
import { Users } from '@/models/index.js';
export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const blockee = await dbResolver.getUserFromApId(activity.object); const blockee = await dbResolver.getUserFromApId(activity.object);
@ -15,6 +16,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`; return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`;
} }
await unblock(actor, blockee); await unblock(await Users.findOneByOrFail({ id: actor.id }), blockee);
return `ok`; return `ok`;
}; };

View file

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js'; import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js'; import cancelRequest from '@/services/following/requests/cancel.js';
import { IFollow } from '../../type.js'; import { IFollow } from '../../type.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { FollowRequests, Followings } from '@/models/index.js'; import { FollowRequests, Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object); const followee = await dbResolver.getUserFromApId(activity.object);
@ -17,12 +17,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`; return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`;
} }
const req = await FollowRequests.findOne({ const req = await FollowRequests.findOneBy({
followerId: actor.id, followerId: actor.id,
followeeId: followee.id, followeeId: followee.id,
}); });
const following = await Followings.findOne({ const following = await Followings.findOneBy({
followerId: actor.id, followerId: actor.id,
followeeId: followee.id, followeeId: followee.id,
}); });

View file

@ -1,5 +1,5 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type.js'; import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '../../type.js';
import unfollow from './follow.js'; import unfollow from './follow.js';
import unblock from './block.js'; import unblock from './block.js';
import undoLike from './like.js'; import undoLike from './like.js';
@ -10,7 +10,7 @@ import { apLogger } from '../../logger.js';
const logger = apLogger; const logger = apLogger;
export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => { export default async (actor: CacheableRemoteUser, activity: IUndo): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { ILike, getApId } from '../../type.js'; import { ILike, getApId } from '../../type.js';
import deleteReaction from '@/services/note/reaction/delete.js'; import deleteReaction from '@/services/note/reaction/delete.js';
import { fetchNote } from '../../models/note.js'; import { fetchNote } from '../../models/note.js';
@ -6,7 +6,7 @@ import { fetchNote } from '../../models/note.js';
/** /**
* Process Undo.Like activity * Process Undo.Like activity
*/ */
export default async (actor: IRemoteUser, activity: ILike) => { export default async (actor: CacheableRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object); const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri); const note = await fetchNote(targetUri);

View file

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

View file

@ -1,10 +1,10 @@
import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import Resolver from '../resolver.js'; import Resolver from '../resolver.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { apLogger } from '../logger.js'; import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles } from '@/models/index.js'; import { DriveFiles, Users } from '@/models/index.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
@ -13,7 +13,7 @@ const logger = apLogger;
/** /**
* Imageを作成します * Imageを作成します
*/ */
export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> { export async function createImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
throw new Error('actor has been suspended'); throw new Error('actor has been suspended');
@ -47,7 +47,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
uri: image.url, uri: image.url,
}); });
file = await DriveFiles.findOneOrFail(file.id); file = await DriveFiles.findOneByOrFail({ id: file.id });
} }
} }
@ -60,7 +60,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
* Misskeyに対象のImageが登録されていればそれを返し * Misskeyに対象のImageが登録されていればそれを返し
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> { export async function resolveImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> {
// TODO // TODO
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録

View file

@ -3,17 +3,17 @@ import { IObject, isMention, IApMention } from '../type.js';
import { resolvePerson } from './person.js'; import { resolvePerson } from './person.js';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import Resolver from '../resolver.js'; import Resolver from '../resolver.js';
import { User } from '@/models/entities/user.js'; import { CacheableUser, User } from '@/models/entities/user.js';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
const resolver = new Resolver(); const resolver = new Resolver();
const limit = promiseLimit<User | null>(2); const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
)).filter((x): x is User => x != null); )).filter((x): x is CacheableUser => x != null);
return mentionedUsers; return mentionedUsers;
} }

View file

@ -5,7 +5,7 @@ import Resolver from '../resolver.js';
import post from '@/services/note/create.js'; import post from '@/services/note/create.js';
import { resolvePerson, updatePerson } from './person.js'; import { resolvePerson, updatePerson } from './person.js';
import { resolveImage } from './image.js'; import { resolveImage } from './image.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import { htmlToMfm } from '../misc/html-to-mfm.js'; import { htmlToMfm } from '../misc/html-to-mfm.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import { unique, toArray, toSingle } from '@/prelude/array.js'; import { unique, toArray, toSingle } from '@/prelude/array.js';
@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { extractDbHost, toPuny } from '@/misc/convert-host.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js';
import { Emojis, Polls, MessagingMessages } from '@/models/index.js'; import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
@ -90,7 +90,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
logger.info(`Creating the Note: ${note.id}`); logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ // 投稿者をフェッチ
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser; const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser;
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
@ -141,7 +141,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const uri = getApId(note.inReplyTo); const uri = getApId(note.inReplyTo);
if (uri.startsWith(config.url + '/')) { if (uri.startsWith(config.url + '/')) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
const talk = await MessagingMessages.findOne(id); const talk = await MessagingMessages.findOneBy({ id });
if (talk) { if (talk) {
isTalk = true; isTalk = true;
return null; return null;
@ -201,7 +201,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
// vote // vote
if (reply && reply.hasPoll) { if (reply && reply.hasPoll) {
const poll = await Polls.findOneOrFail(reply.id); const poll = await Polls.findOneByOrFail({ noteId: reply.id });
const tryCreateVote = async (name: string, index: number): Promise<null> => { const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
@ -230,11 +230,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined); const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
if (actor.uri) updatePerson(actor.uri);
}
if (isTalk) { if (isTalk) {
for (const recipient of visibleUsers) { for (const recipient of visibleUsers) {
await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id); await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);
@ -311,7 +306,7 @@ export async function extractEmojis(tags: IObject | IObject[], host: string): Pr
const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
tag.icon = toSingle(tag.icon); tag.icon = toSingle(tag.icon);
const exists = await Emojis.findOne({ const exists = await Emojis.findOneBy({
host, host,
name, name,
}); });
@ -332,7 +327,7 @@ export async function extractEmojis(tags: IObject | IObject[], host: string): Pr
updatedAt: new Date(), updatedAt: new Date(),
}); });
return await Emojis.findOne({ return await Emojis.findOneBy({
host, host,
name, name,
}) as Emoji; }) as Emoji;
@ -352,6 +347,6 @@ export async function extractEmojis(tags: IObject | IObject[], host: string): Pr
publicUrl: tag.icon!.url, publicUrl: tag.icon!.url,
updatedAt: new Date(), updatedAt: new Date(),
aliases: [], aliases: [],
} as Partial<Emoji>).then(x => Emojis.findOneOrFail(x.identifiers[0])); } as Partial<Emoji>).then(x => Emojis.findOneByOrFail(x.identifiers[0]));
})); }));
} }

View file

@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { updateUsertags } from '@/services/update-hashtag.js'; import { updateUsertags } from '@/services/update-hashtag.js';
import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js';
import { User, IRemoteUser } from '@/models/entities/user.js'; import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { UserNotePining } from '@/models/entities/user-note-pining.js';
import { genId } from '@/misc/gen-id.js'; import { genId } from '@/misc/gen-id.js';
@ -24,12 +24,14 @@ import { UserPublickey } from '@/models/entities/user-publickey.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import { toPuny } from '@/misc/convert-host.js'; import { toPuny } from '@/misc/convert-host.js';
import { UserProfile } from '@/models/entities/user-profile.js'; import { UserProfile } from '@/models/entities/user-profile.js';
import { getConnection } from 'typeorm';
import { toArray } from '@/prelude/array.js'; import { toArray } from '@/prelude/array.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import { StatusError } from '@/misc/fetch.js'; import { StatusError } from '@/misc/fetch.js';
import { uriPersonCache } from '@/services/user-cache.js';
import { publishInternalEvent } from '@/services/stream.js';
import { db } from '@/db/postgre.js';
const logger = apLogger; const logger = apLogger;
@ -91,19 +93,25 @@ function validateActor(x: IObject, uri: string): IActor {
* *
* Misskeyに対象のPersonが登録されていればそれを返します * Misskeyに対象のPersonが登録されていればそれを返します
*/ */
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> { export async function fetchPerson(uri: string, resolver?: Resolver): Promise<CacheableUser | null> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = uriPersonCache.get(uri);
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) { if (uri.startsWith(config.url + '/')) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
return await Users.findOne(id).then(x => x || null); const u = await Users.findOneBy({ id });
if (u) uriPersonCache.set(uri, u);
return u;
} }
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await Users.findOne({ uri }); const exist = await Users.findOneBy({ uri });
if (exist) { if (exist) {
uriPersonCache.set(uri, exist);
return exist; return exist;
} }
//#endregion //#endregion
@ -143,7 +151,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
let user: IRemoteUser; let user: IRemoteUser;
try { try {
// Start transaction // Start transaction
await getConnection().transaction(async transactionalEntityManager => { await db.transaction(async transactionalEntityManager => {
user = await transactionalEntityManager.save(new User({ user = await transactionalEntityManager.save(new User({
id: genId(), id: genId(),
avatarId: null, avatarId: null,
@ -189,7 +197,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await Users.findOne({ const u = await Users.findOneBy({
uri: person.id, uri: person.id,
}); });
@ -272,7 +280,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
} }
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const exist = await Users.findOne({ uri }) as IRemoteUser; const exist = await Users.findOneBy({ uri }) as IRemoteUser;
if (exist == null) { if (exist == null) {
return; return;
@ -352,6 +360,8 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
location: person['vcard:Address'] || null, location: person['vcard:Address'] || null,
}); });
publishInternalEvent('remoteUserUpdated', { id: exist.id });
// ハッシュタグ更新 // ハッシュタグ更新
updateUsertags(exist, tags); updateUsertags(exist, tags);
@ -371,7 +381,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
* Misskeyに対象のPersonが登録されていればそれを返し * Misskeyに対象のPersonが登録されていればそれを返し
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> { export async function resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -441,7 +451,7 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined)
} }
export async function updateFeatured(userId: User['id']) { export async function updateFeatured(userId: User['id']) {
const user = await Users.findOneOrFail(userId); const user = await Users.findOneByOrFail({ id: userId });
if (!Users.isRemoteUser(user)) return; if (!Users.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
@ -464,7 +474,7 @@ export async function updateFeatured(userId: User['id']) {
.slice(0, 5) .slice(0, 5)
.map(item => limit(() => resolveNote(item, resolver)))); .map(item => limit(() => resolveNote(item, resolver))));
await getConnection().transaction(async transactionalEntityManager => { await db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持

View file

@ -47,10 +47,10 @@ export async function updateQuestion(value: any) {
if (uri.startsWith(config.url + '/')) throw new Error('uri points local'); if (uri.startsWith(config.url + '/')) throw new Error('uri points local');
//#region このサーバーに既に登録されているか //#region このサーバーに既に登録されているか
const note = await Notes.findOne({ uri }); const note = await Notes.findOneBy({ uri });
if (note == null) throw new Error('Question is not registed'); if (note == null) throw new Error('Question is not registed');
const poll = await Polls.findOne({ noteId: note.id }); const poll = await Polls.findOneBy({ noteId: note.id });
if (poll == null) throw new Error('Question is not registed'); if (poll == null) throw new Error('Question is not registed');
//#endregion //#endregion

View file

@ -1,7 +1,17 @@
import { IObject } from './type.js'; import { IObject } from './type.js';
import { IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { performActivity } from './kernel/index.js'; import { performActivity } from './kernel/index.js';
import { updatePerson } from './models/person.js';
export default async (actor: IRemoteUser, activity: IObject): Promise<void> => { export default async (actor: CacheableRemoteUser, activity: IObject): Promise<void> => {
await performActivity(actor, activity); await performActivity(actor, activity);
// ついでにリモートユーザーの情報が古かったら更新しておく
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
updatePerson(actor.uri!);
});
}
}
}; };

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