Compare commits

..

2 commits

Author SHA1 Message Date
Chloe Kudryavtsev 473d2ffbb1 fixup: missed a few elements in the div.text to span move 2022-10-17 17:36:53 -04:00
Chloe Kudryavtsev 4d7531979f client: replace indicator circles
Instead of indicator circles, we use colors and bold text.
The icon turns a doubly-saturated indicator color,
and the accompanying text (if any) turns bold.

In my limited testing, this works out very nicely aesthetically.
And it's much more lightweight!

Superseeds #172
ChangeLog: Changed
2022-10-17 16:42:42 -04:00
277 changed files with 3876 additions and 2375 deletions

View file

@ -124,9 +124,6 @@ redis:
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
# Max note text length (in characters)
#maxNoteTextLength: 3000
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]

View file

@ -1,4 +1,6 @@
.autogen
.github
.travis
.vscode
.config
Dockerfile
@ -10,3 +12,4 @@ elasticsearch/
node_modules/
redis/
files/
misskey-assets/

View file

@ -139,14 +139,6 @@ To generate the changelog, we use a standard shortlog command: `git shortlog --f
The person performing the release process should build the next CHANGELOG section based on this output, not use it as-is.
Full releases should also remove any pre-release CHANGELOG sections.
Here is the step by step checklist:
1. If **stable** release, announce the comment period. Restart the comment period if a blocker bug is found and fixed.
2. Edit various `package.json`s to the new version.
3. Write a new entry into the changelog.
You should use the `git shortlog --format='%h %s' --group=trailer:changelog LAST_TAG..` command to get general data,
then rewrite it in a human way.
4. Tag the commit with the changes in 2 and 3 (if together, else the latter).
## Translation
[![Translation status](http://translate.akkoma.dev/widgets/foundkey/-/svg-badge.svg)](http://translate.akkoma.dev/engage/foundkey/)
@ -297,11 +289,8 @@ PostgreSQL array indices **start at 1**.
When `IN` is performed on a column that may contain `NULL` values, use `OR` or similar to handle `NULL` values.
### creating migrations
First make changes to the entity files in `packages/backend/src/models/entities/`.
Then, in `packages/backend`, run:
In `packages/backend`, run:
```sh
yarn build
npx typeorm migration:generate -d ormconfig.js -o <migration name>
```

View file

@ -40,9 +40,6 @@ git merge tags/v13.0.0-preview2 --squash
# you are now on the "next" release
```
## Making sure modern Yarn works
Foundkey uses Modern Yarn instead of Classic (1.x). To make sure the `yarn` command will work going forward, run `corepack enable`.
## Rebuilding and running database migrations
This will be pretty much the same as a regular update of Misskey. Note that `yarn install` may take a while since dependency versions have been updated or removed and we use a newer version of Yarn.
```sh

View file

@ -190,9 +190,7 @@ charts: "Charts"
perHour: "Per Hour"
perDay: "Per Day"
stopActivityDelivery: "Stop sending activities"
stopActivityDeliveryDescription: "Local activities will not be sent to this instance. Receiving activities works as before."
blockThisInstance: "Block this instance"
blockThisInstanceDescription: "Local activites will not be sent to this instance. Activites from this instance will be discarded."
operations: "Operations"
software: "Software"
version: "Version"

View file

@ -1,6 +1,6 @@
{
"name": "foundkey",
"version": "13.0.0-preview2",
"version": "13.0.0-preview.1",
"repository": {
"type": "git",
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"

View file

@ -1,5 +1,5 @@
export class removeAds1657570176749 {
name = 'removeAds1657570176749';
name = 'removeAds1657570176749'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "ad"`);

View file

@ -1,44 +0,0 @@
export class sync1667503570994 {
name = 'sync1667503570994'
async up(queryRunner) {
await Promise.all([
// the migration for renote mutes added the index to the wrong table
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`),
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`),
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`),
queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `),
queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `),
queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET NOT NULL`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET DEFAULT ''`),
queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `),
queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`),
queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`),
]);
}
async down(queryRunner) {
await Promise.all([
queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`),
queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`),
queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP DEFAULT`),
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP NOT NULL`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`),
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`),
queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`),
queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`),
queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`),
queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `),
queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `),
queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `),
]);
}
}

View file

@ -1,6 +1,6 @@
{
"name": "backend",
"version": "13.0.0-preview2",
"version": "13.0.0-preview1",
"main": "./index.js",
"private": true,
"type": "module",
@ -15,8 +15,8 @@
"test": "npm run mocha"
},
"dependencies": {
"@bull-board/api": "^4.3.1",
"@bull-board/koa": "^4.3.1",
"@bull-board/api": "^4.2.2",
"@bull-board/koa": "4.0.0",
"@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.11.0",
"@koa/cors": "3.1.0",
@ -96,7 +96,7 @@
"rss-parser": "3.12.0",
"sanitize-html": "2.7.0",
"semver": "7.3.7",
"sharp": "0.31.2",
"sharp": "0.30.7",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",

View file

@ -38,8 +38,6 @@ export default function load(): Config {
config.port = config.port || parseInt(process.env.PORT || '', 10);
if (!config.maxNoteTextLength) config.maxNoteTextLength = 3000;
mixin.version = meta.version;
mixin.host = url.host;
mixin.hostname = url.hostname;

View file

@ -10,7 +10,7 @@ function getRedisFamily(family?: string | number): number {
dual: 0,
};
if (typeof family === 'string' && family in familyMap) {
return familyMap[family as keyof typeof familyMap];
return familyMap[family];
} else if (typeof family === 'number' && Object.values(familyMap).includes(family)) {
return family;
}

View file

@ -24,7 +24,7 @@ export type Source = {
db?: number;
prefix?: string;
};
elasticsearch?: {
elasticsearch: {
host: string;
port: number;
ssl?: boolean;
@ -41,8 +41,6 @@ export type Source = {
maxFileSize?: number;
maxNoteTextLength?: number;
accesslog?: string;
clusterLimit?: number;

View file

@ -1,3 +1,5 @@
export const MAX_NOTE_TEXT_LENGTH = 3000;
// Time constants
export const SECOND = 1000;
export const MINUTE = 60 * SECOND;

View file

@ -62,21 +62,22 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
// hashtags
// ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt;
// mentions
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');
if (part.length === 2 && href) {
// restore the host name part
//#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`;
text += acct;
//#endregion
} else if (part.length === 3) {
text += txt;
}
// other
// その他
} else {
const generateLink = () => {
if (!href && !txt) {

View file

@ -1,12 +1,10 @@
export class Cache<T> {
public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number;
public fetcher: (key: string | null) => Promise<T | undefined>;
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
constructor(lifetime: Cache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
this.fetcher = fetcher;
}
public set(key: string | null, value: T): void {
@ -19,13 +17,10 @@ export class Cache<T> {
public get(key: string | null): T | undefined {
const cached = this.cache.get(key);
if (cached == null) return undefined;
// discard if past the cache lifetime
if ((Date.now() - cached.date) > this.lifetime) {
this.cache.delete(key);
return undefined;
}
return cached.value;
}
@ -34,22 +29,52 @@ export class Cache<T> {
}
/**
* If the value is cached, it is returned. Otherwise the fetcher is
* run to get the value. If the fetcher returns undefined, it is
* returned but not cached.
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetch(key: string | null): Promise<T | undefined> {
const cached = this.get(key);
if (cached !== undefined) {
return cached;
} else {
const value = await this.fetcher(key);
// don't cache undefined
if (value !== undefined)
this.set(key, value);
return value;
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
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();
this.set(key, value);
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);
}
return value;
}
}

View file

@ -3,26 +3,22 @@ import { Note } from '@/models/entities/note.js';
import { User } from '@/models/entities/user.js';
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js';
import * as Acct from '@/misc/acct.js';
import { MINUTE } from '@/const.js';
import { getFullApAccount } from './convert-host.js';
import { Packed } from './schema.js';
import { Cache } from './cache.js';
const blockingCache = new Cache<User['id'][]>(
5 * MINUTE,
(blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)),
);
const blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
// designation for users you follow, list users and groups is disabled for performance reasons
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
/**
* either noteUserFollowers or antennaUserFollowing must be specified
* noteUserFollowers / antennaUserFollowing
*/
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
if (note.visibility === 'specified') return false;
// skip if the antenna creator is blocked by the note author
const blockings = await blockingCache.fetch(noteUser.id);
// アンテナ作成者がノート作成者にブロックされていたらスキップ
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 (note.visibility === 'followers') {

View file

@ -1,44 +1,44 @@
import push from 'web-push';
import { db } from '@/db/postgre.js';
import { Meta } from '@/models/entities/meta.js';
import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js';
let cache: Meta;
/**
* Performs the primitive database operation to set the server configuration
*/
export async function setMeta(meta: Meta): Promise<void> {
const unlock = await getFetchInstanceMetadataLock('localhost');
// try to mitigate older bugs where multiple meta entries may have been created
db.manager.clear(Meta);
db.manager.insert(Meta, meta);
unlock();
}
/**
* Performs the primitive database operation to fetch server configuration.
* Writes to `cache` instead of returning.
*/
async function getMeta(): Promise<void> {
const unlock = await getFetchInstanceMetadataLock('localhost');
// new IDs are prioritised because multiple records may have been created due to past bugs
cache = db.manager.findOne(Meta, {
order: {
id: 'DESC',
},
});
unlock();
}
export async function fetchMeta(noCache = false): Promise<Meta> {
if (!noCache && cache) return cache;
await getMeta();
return await db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(Meta, {
order: {
id: 'DESC',
},
});
return cache;
const meta = metas[0];
if (meta) {
cache = meta;
return meta;
} else {
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
const saved = await transactionalEntityManager
.upsert(
Meta,
{
id: 'x',
},
['id'],
)
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
cache = saved;
return saved;
}
});
}
setInterval(() => {
fetchMeta(true).then(meta => {
cache = meta;
});
}, 1000 * 10);

View file

@ -11,4 +11,4 @@ export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
* Maximum image description length that can be stored in DB.
* Surrogate pairs count as one
*/
export const DB_MAX_IMAGE_COMMENT_LENGTH = 2048;
export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;

View file

@ -1,18 +1,19 @@
const locales = await import('../../../../locales/index.js').then(mod => mod.default);
export class I18n<T extends Record<string, any>> {
public locale: T;
export class I18n {
public ts: Record<string, any>;
constructor(locale: T) {
this.locale = locale;
constructor(locale: string) {
this.ts = locales[locale];
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
public t(key: string, args?: Record<string, any>): string {
try {
let str = key.split('.').reduce((o, i) => o[i], this.ts) as string;
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
if (args) {
for (const [k, v] of Object.entries(args)) {

View file

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

View file

@ -4,27 +4,14 @@ import { Emojis } from '@/models/index.js';
import { Emoji } from '@/models/entities/emoji.js';
import { Note } from '@/models/entities/note.js';
import { query } from '@/prelude/url.js';
import { HOUR } from '@/const.js';
import { Cache } from './cache.js';
import { isSelfHost, toPunyNullable } from './convert-host.js';
import { decodeReaction } from './reaction-lib.js';
/**
* composite cache key: `${host ?? ''}:${name}`
*/
const cache = new Cache<Emoji | null>(
12 * HOUR,
async (key) => {
const [host, name] = key.split(':');
return (await Emojis.findOneBy({
name,
host: host || IsNull(),
})) || null;
},
);
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
/**
* Information needed to attach in ActivityPub
*
*/
type PopulatedEmoji = {
name: string;
@ -49,22 +36,28 @@ function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const name = match[1];
// ホスト正規化
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
return { name, host };
}
/**
* Resolve emoji information from ActivityPub attachment.
* @param emojiName custom emoji names attached to notes, user profiles or in rections. Colons should not be included. Localhost is denote by @. (see also `decodeReaction`)
* @param noteUserHost host that the content is from, to default to
* @returns emoji information. `null` means not found.
*
* @param emojiName (:, @. (decodeReactionで可能))
* @param noteUserHost
* @returns , nullは未マッチを意味する
*/
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
if (name == null) return null;
const emoji = await cache.fetch(`${host ?? ''}:${name}`);
const queryOrNull = async () => (await Emojis.findOneBy({
name,
host: host ?? IsNull(),
})) || null;
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
@ -79,7 +72,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
}
/**
* Retrieve list of emojis from the cache. Uncached emoji are dropped.
* (, )
*/
export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
@ -110,20 +103,11 @@ export function aggregateNoteEmojis(notes: Note[]) {
}
/**
* Query list of emojis in bulk and add them to the cache.
*
*/
export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
const notCachedEmojis = emojis.filter(emoji => {
// check if the cache has this emoji
return cache.get(`${emoji.host ?? ''}:${emoji.name}`) == null;
});
// check if there even are any uncached emoji to handle
if (notCachedEmojis.length === 0) return;
// query all uncached emoji
const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null);
const emojisQuery: any[] = [];
// group by hosts to try to reduce query size
const hosts = new Set(notCachedEmojis.map(e => e.host));
for (const host of hosts) {
emojisQuery.push({
@ -131,14 +115,11 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
host: host ?? IsNull(),
});
}
await Emojis.find({
const _emojis = emojisQuery.length > 0 ? await Emojis.find({
where: emojisQuery,
select: ['name', 'host', 'originalUrl', 'publicUrl'],
}).then(emojis => {
// store all emojis into the cache
emojis.forEach(emoji => {
cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji);
});
});
}) : [];
for (const emoji of _emojis) {
cache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}

View file

@ -1,5 +1,5 @@
import { Note } from '@/models/entities/note.js';
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
export function isPureRenote(note: Note): boolean {
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
}

View file

@ -1,55 +0,0 @@
import { Brackets } from 'typeorm';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Instances } from '@/models/index.js';
import { Instance } from '@/models/entities/instance.js';
import { DAY } from '@/const.js';
// Threshold from last contact after which an instance will be considered
// "dead" and should no longer get activities delivered to it.
const deadThreshold = 7 * DAY;
/**
* Returns the subset of hosts which should be skipped.
*
* @param hosts array of punycoded instance hosts
* @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter)
*/
export async function skippedInstances(hosts: Array<Instace['host']>): Array<Instance['host']> {
// first check for blocked instances since that info may already be in memory
const { blockedHosts } = await fetchMeta();
const skipped = hosts.filter(host => blockedHosts.includes(host));
// if possible return early and skip accessing the database
if (skipped.length === hosts.length) return hosts;
const deadTime = new Date(Date.now() - deadThreshold);
return skipped.concat(
await Instances.createQueryBuilder('instance')
.where('instance.host in (:...hosts)', {
// don't check hosts again that we already know are suspended
// also avoids adding duplicates to the list
hosts: hosts.filter(host => !skipped.includes(host)),
})
.andWhere(new Brackets(qb => { qb
.where('instance.isSuspended')
.orWhere('instance.lastCommunicatedAt < :deadTime', { deadTime })
.orWhere('instance.latestStatus = 410');
}))
.select('host')
.getRawMany()
);
}
/**
* Returns whether a specific host (punycoded) should be skipped.
* Convenience wrapper around skippedInstances which should only be used if there is a single host to check.
* If you have multiple hosts, consider using skippedInstances instead to do a bulk check.
*
* @param host punycoded instance host
* @returns whether the given host should be skipped
*/
export async function shouldSkipInstance(host: Instance['host']): boolean {
const skipped = await skippedInstances([host]);
return skipped.length > 0;
}

View file

@ -62,8 +62,7 @@ export class DriveFile {
public size: number;
@Column('varchar', {
length: 2048,
nullable: true,
length: 512, nullable: true,
comment: 'The comment of the DriveFile.',
})
public comment: string | null;

View file

@ -7,7 +7,7 @@ export class Instance {
public id: string;
/**
* Date and time this instance was first seen.
*
*/
@Index()
@Column('timestamp with time zone', {
@ -16,7 +16,7 @@ export class Instance {
public caughtAt: Date;
/**
* Hostname
*
*/
@Index({ unique: true })
@Column('varchar', {
@ -26,7 +26,7 @@ export class Instance {
public host: string;
/**
* Number of users on this instance.
*
*/
@Column('integer', {
default: 0,
@ -35,7 +35,7 @@ export class Instance {
public usersCount: number;
/**
* Number of notes on this instance.
* 稿
*/
@Column('integer', {
default: 0,
@ -44,7 +44,7 @@ export class Instance {
public notesCount: number;
/**
* Number of local users who are followed by users from this instance.
*
*/
@Column('integer', {
default: 0,
@ -52,7 +52,7 @@ export class Instance {
public followingCount: number;
/**
* Number of users from this instance who are followed by local users.
*
*/
@Column('integer', {
default: 0,
@ -60,7 +60,7 @@ export class Instance {
public followersCount: number;
/**
* Timestamp of the latest outgoing HTTP request.
*
*/
@Column('timestamp with time zone', {
nullable: true,
@ -68,7 +68,7 @@ export class Instance {
public latestRequestSentAt: Date | null;
/**
* HTTP status code that was received for the last outgoing HTTP request.
* HTTPステータスコード
*/
@Column('integer', {
nullable: true,
@ -76,7 +76,7 @@ export class Instance {
public latestStatus: number | null;
/**
* Timestamp of the latest incoming HTTP request.
*
*/
@Column('timestamp with time zone', {
nullable: true,
@ -84,13 +84,13 @@ export class Instance {
public latestRequestReceivedAt: Date | null;
/**
* Timestamp of last communication with this instance (incoming or outgoing).
*
*/
@Column('timestamp with time zone')
public lastCommunicatedAt: Date;
/**
* Whether this instance seems unresponsive.
*
*/
@Column('boolean', {
default: false,
@ -98,7 +98,7 @@ export class Instance {
public isNotResponding: boolean;
/**
* Whether sending activities to this instance has been suspended.
*
*/
@Index()
@Column('boolean', {

View file

@ -6,16 +6,13 @@ import { Packed } from '@/misc/schema.js';
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
import { populateEmojis } from '@/misc/populate-emojis.js';
import { getAntennas } from '@/misc/antenna-cache.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { db } from '@/db/postgre.js';
import { Instance } from '../entities/instance.js';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
const userInstanceCache = new Cache<Instance | null>(
3 * HOUR,
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
);
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
@ -30,7 +27,7 @@ const ajv = new Ajv();
const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
const passwordSchema = { type: 'string', minLength: 1 } as const;
const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 2048 } as const;
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
@ -312,15 +309,17 @@ export const UserRepository = db.getRepository(User).extend({
isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy,
isCat: user.isCat || falsy,
instance: !user.host ? undefined : userInstanceCache.fetch(user.host)
.then(instance => !instance ? undefined : {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
}),
instance: user.host ? userInstanceCache.fetch(user.host,
() => Instances.findOneBy({ host: user.host! }),
v => v != null,
).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
} : undefined) : undefined,
emojis: populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),

View file

@ -6,20 +6,39 @@ import Logger from '@/services/logger.js';
import { Instances } from '@/models/index.js';
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { toPuny } from '@/misc/convert-host.js';
import { Cache } from '@/misc/cache.js';
import { Instance } from '@/models/entities/instance.js';
import { StatusError } from '@/misc/fetch.js';
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
import { DeliverJobData } from '@/queue/types.js';
import { LessThan } from 'typeorm';
import { DAY } from '@/const.js';
const logger = new Logger('deliver');
let latest: string | null = null;
const deadThreshold = 30 * DAY;
export default async (job: Bull.Job<DeliverJobData>) => {
const { host } = new URL(job.data.to);
const puny = toPuny(host);
if (await shouldSkipInstance(puny)) return 'skip';
// ブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(puny)) {
return 'skip (blocked)';
}
const deadTime = new Date(Date.now() - deadThreshold);
const isSuspendedOrDead = await Instances.countBy([
{ host: puny, isSuspended: true },
{ host: puny, lastCommunicatedAt: LessThan(deadTime) },
]);
if (isSuspendedOrDead) {
return 'skip (suspended or dead)';
}
try {
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
@ -62,8 +81,8 @@ export default async (job: Bull.Job<DeliverJobData>) => {
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
// A client error means that something is wrong with the request we are making,
// which means that retrying it makes no sense.
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく
return `${res.statusCode} ${res.statusMessage}`;
}

View file

@ -1,6 +1,6 @@
import Bull from 'bull';
import { In, LessThan } from 'typeorm';
import { AttestationChallenges, Mutings, PasswordResetRequests, Signins } from '@/models/index.js';
import { AttestationChallenges, Mutings, Signins } from '@/models/index.js';
import { publishUserEvent } from '@/services/stream.js';
import { MINUTE, DAY } from '@/const.js';
import { queueLogger } from '@/queue/logger.js';
@ -35,11 +35,6 @@ export async function checkExpired(job: Bull.Job<Record<string, unknown>>, done:
createdAt: LessThan(new Date(new Date().getTime() - 5 * MINUTE)),
});
await PasswordResetRequests.delete({
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
createdAt: LessThan(new Date(new Date().getTime() - 30 * MINUTE)),
});
logger.succ('Deleted expired mutes, signins and attestation challenges.');
done();

View file

@ -10,14 +10,8 @@ import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
const publicKeyCache = new Cache<UserPublickey>(
Infinity,
(keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined),
);
const publicKeyByUserIdCache = new Cache<UserPublickey>(
Infinity,
(userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined),
);
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export type UriParseResult = {
/** wether the URI was generated by us */
@ -105,9 +99,13 @@ export default class DbResolver {
if (parsed.local) {
if (parsed.type !== 'users') return null;
return await userByIdCache.fetch(parsed.id) ?? null;
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) ?? null;
} else {
return await uriPersonCache.fetch(parsed.uri) ?? null;
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri,
}));
}
}
@ -118,12 +116,20 @@ export default class DbResolver {
user: CacheableRemoteUser;
key: UserPublickey;
} | null> {
const key = await publicKeyCache.fetch(keyId);
const key = await publicKeyCache.fetch(keyId, async () => {
const key = await UserPublickeys.findOneBy({
keyId,
});
if (key == null) return null;
return key;
}, key => key != null);
if (key == null) return null;
return {
user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser,
user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser,
key,
};
}
@ -139,7 +145,7 @@ export default class DbResolver {
if (user == null) return null;
const key = await publicKeyByUserIdCache.fetch(user.id);
const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null);
return {
user,

View file

@ -2,7 +2,6 @@ import { IsNull, Not } from 'typeorm';
import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js';
import { Users, Followings } from '@/models/index.js';
import { deliver } from '@/queue/index.js';
import { skippedInstances } from '@/misc/skipped-instances.js';
//#region types
interface IRecipe {
@ -119,18 +118,26 @@ export default class DeliverManager {
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
const followers = await Followings.createQueryBuilder('followings')
// return either the shared inbox (if available) or the individual inbox
.select('COALESCE(followings.followerSharedInbox, followings.followerInbox)', 'inbox')
// so we don't have to make our inboxes Set work as hard
.distinct(true)
// ...for the specific actors followers
.where('followings.followeeId = :actorId', { actorId: this.actor.id })
// don't deliver to ourselves
.andWhere('followings.followerHost IS NOT NULL')
.getRawMany();
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await Followings.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
followers.forEach(({ inbox }) => inboxes.add(inbox));
for (const following of followers) {
const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox);
}
}
this.recipes.filter((recipe): recipe is IDirectRecipe =>
@ -143,19 +150,8 @@ export default class DeliverManager {
)
.forEach(recipe => inboxes.add(recipe.to.inbox!));
const instancesToSkip = await skippedInstances(
// get (unique) list of hosts
Array.from(new Set(
Array.from(inboxes)
.map(inbox => new URL(inbox).host)
))
);
// deliver
for (const inbox of inboxes) {
// skip instances as indicated
if (instancesToSkip.includes(new URL(inbox).host)) continue;
deliver(this.actor, this.activity, inbox);
}
}

View file

@ -1,5 +1,5 @@
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { createReaction } from '@/services/note/reaction/create.js';
import create from '@/services/note/reaction/create.js';
import { ILike, getApId } from '../type.js';
import { fetchNote, extractEmojis } from '../models/note.js';
@ -11,7 +11,7 @@ export default async (actor: CacheableRemoteUser, activity: ILike) => {
await extractEmojis(activity.tag || [], actor.host).catch(() => null);
return await createReaction(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
return 'skip: already reacted';
} else {

View file

@ -1,5 +1,5 @@
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { deleteReaction } from '@/services/note/reaction/delete.js';
import deleteReaction from '@/services/note/reaction/delete.js';
import { ILike, getApId } from '@/remote/activitypub/type.js';
import { fetchNote } from '@/remote/activitypub/models/note.js';

View file

@ -4,7 +4,7 @@ import config from '@/config/index.js';
import post from '@/services/note/create.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { unique, toArray, toSingle } from '@/prelude/array.js';
import { vote } from '@/services/note/polls/vote.js';
import vote from '@/services/note/polls/vote.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { extractDbHost, toPuny } from '@/misc/convert-host.js';

View file

@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
* @param user http-signature user
* @param url URL to fetch
*/
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
export async function signedGet(url: string, user: { id: User['id'] }) {
const keypair = await getUserKeypair(user.id);
const req = createSignedGet({

View file

@ -23,6 +23,8 @@ import Featured from './activitypub/featured.js';
// Init router
const router = new Router();
//#region Routing
function inbox(ctx: Router.RouterContext) {
let signature;
@ -43,8 +45,6 @@ const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystr
function isActivityPubReq(ctx: Router.RouterContext) {
ctx.response.vary('Accept');
// if no accept header is supplied, koa returns the 1st, so html is used as a dummy
// i.e. activitypub requests must be explicit
const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
return typeof accepted === 'string' && !accepted.match(/html/);
}
@ -77,7 +77,7 @@ router.get('/notes/:note', async (ctx, next) => {
return;
}
// redirect if remote
// リモートだったらリダイレクト
if (note.userHost != null) {
if (note.uri == null || isSelfHost(note.userHost)) {
ctx.status = 500;
@ -94,15 +94,6 @@ router.get('/notes/:note', async (ctx, next) => {
// note activity
router.get('/notes/:note/activity', async ctx => {
if (!isActivityPubReq(ctx)) {
/*
Redirect to the human readable page. in this case using next is not possible,
since there is no human readable page explicitly for the activity.
*/
ctx.redirect(`/notes/${ctx.params.note}`);
return;
}
const note = await Notes.findOneBy({
id: ctx.params.note,
userHost: IsNull(),
@ -194,6 +185,7 @@ router.get('/@:user', async (ctx, next) => {
await userInfo(ctx, user);
});
//#endregion
// emoji
router.get('/emojis/:emoji', async ctx => {

View file

@ -5,51 +5,59 @@ import authenticate, { AuthenticationError } from './authenticate.js';
import call from './call.js';
import { ApiError } from './error.js';
export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<void> {
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
const body = ctx.is('multipart/form-data')
? (ctx.request as any).body
: ctx.method === 'GET'
? ctx.query
: ctx.request.body;
const error = (e: ApiError): void => {
ctx.status = e.httpStatusCode;
if (e.httpStatusCode === 401) {
ctx.response.set('WWW-Authenticate', 'Bearer');
const reply = (x?: any, y?: ApiError) => {
if (x == null) {
ctx.status = 204;
} else if (typeof x === 'number' && y) {
ctx.status = x;
ctx.body = {
error: {
message: y!.message,
code: y!.code,
id: y!.id,
kind: y!.kind,
...(y!.info ? { info: y!.info } : {}),
},
};
} else {
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
}
ctx.body = {
error: {
message: e!.message,
code: e!.code,
...(e!.info ? { info: e!.info } : {}),
endpoint: endpoint.name,
},
};
res();
};
// Authentication
// for GET requests, do not even pass on the body parameter as it is considered unsafe
await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => {
authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(([user, app]) => {
// API invoking
await call(endpoint.name, user, app, body, ctx).then((res: any) => {
call(endpoint.name, user, app, body, ctx).then((res: any) => {
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
if (res == null) {
ctx.status = 204;
} else {
ctx.status = 200;
// If a string is returned, it must be passed through JSON.stringify to be recognized as JSON.
ctx.body = typeof res === 'string' ? JSON.stringify(res) : res;
}
reply(res);
}).catch((e: ApiError) => {
error(e);
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
});
}).catch(e => {
if (e instanceof AuthenticationError) {
error(new ApiError('AUTHENTICATION_FAILED', e.message));
ctx.response.status = 403;
ctx.response.set('WWW-Authenticate', 'Bearer');
ctx.response.body = {
message: 'Authentication failed: ' + e.message,
code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
kind: 'client',
};
res();
} else {
error(new ApiError());
reply(500, new ApiError());
}
});
}
});

View file

@ -3,13 +3,10 @@ import { Users, AccessTokens, Apps } from '@/models/index.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { Cache } from '@/misc/cache.js';
import { App } from '@/models/entities/app.js';
import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
import isNativeToken from './common/is-native-token.js';
const appCache = new Cache<App>(
Infinity,
(id) => Apps.findOneByOrFail({ id }),
);
const appCache = new Cache<App>(Infinity);
export class AuthenticationError extends Error {
constructor(message: string) {
@ -18,8 +15,8 @@ export class AuthenticationError extends Error {
}
}
export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
let maybeToken: string | null = null;
export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
let token: string | null = null;
// check if there is an authorization header set
if (authorization != null) {
@ -30,19 +27,19 @@ export default async (authorization: string | null | undefined, bodyToken: strin
// check if OAuth 2.0 Bearer tokens are being used
// Authorization schemes are case insensitive
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
maybeToken = authorization.substring(7);
token = authorization.substring(7);
} else {
throw new AuthenticationError('unsupported authentication scheme');
}
} else if (bodyToken != null) {
maybeToken = bodyToken;
token = bodyToken;
} else {
return [null, null];
}
const token: string = maybeToken;
if (isNativeToken(token)) {
const user = await localUserByNativeTokenCache.fetch(token);
const user = await localUserByNativeTokenCache.fetch(token,
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>);
if (user == null) {
throw new AuthenticationError('unknown token');
@ -66,13 +63,14 @@ export default async (authorization: string | null | undefined, bodyToken: strin
lastUsedAt: new Date(),
});
const user = await userByIdCache.fetch(accessToken.userId);
// can't authorize remote users
if (!Users.isLocalUser(user)) return [null, null];
const user = await localUserByIdCache.fetch(accessToken.userId,
() => Users.findOneBy({
id: accessToken.userId,
}) as Promise<ILocalUser>);
if (accessToken.appId) {
const app = await appCache.fetch(accessToken.appId);
const app = await appCache.fetch(accessToken.appId,
() => Apps.findOneByOrFail({ id: accessToken.appId! }));
return [user, {
id: accessToken.id,

View file

@ -8,16 +8,29 @@ import endpoints, { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { apiLogger } from './logger.js';
const accessDenied = {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
};
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin);
const ep = endpoints.find(e => e.name === endpoint);
if (ep == null) throw new ApiError('NO_SUCH_ENDPOINT');
if (ep == null) {
throw new ApiError({
message: 'No such endpoint.',
code: 'NO_SUCH_ENDPOINT',
id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709',
httpStatusCode: 404,
});
}
if (ep.meta.secure && !isSecure) {
throw new ApiError('ACCESS_DENIED', 'This operation can only be performed with a native token.');
throw new ApiError(accessDenied);
}
if (ep.meta.limit && !isModerator) {
@ -36,29 +49,48 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
}
// Rate limit
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(() => {
throw new ApiError('RATE_LIMIT_EXCEEDED');
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
if (ep.meta.requireCredential && user == null) {
throw new ApiError('AUTHENTICATION_REQUIRED');
throw new ApiError({
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
httpStatusCode: 401,
});
}
if (ep.meta.requireCredential && user!.isSuspended) {
throw new ApiError('SUSPENDED');
throw new ApiError({
message: 'Your account has been suspended.',
code: 'YOUR_ACCOUNT_SUSPENDED',
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
httpStatusCode: 403,
});
}
if (ep.meta.requireAdmin && !user!.isAdmin) {
throw new ApiError('ACCESS_DENIED', 'This operation requires administrator privileges.');
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
}
if (ep.meta.requireModerator && !isModerator) {
throw new ApiError('ACCESS_DENIED', 'This operation requires moderator privileges.');
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
}
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
throw new ApiError('ACCESS_DENIED', 'This operation requires privileges which this token does not grant.');
throw new ApiError({
message: 'Your app does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
});
}
// Cast non JSON input
@ -69,7 +101,11 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
try {
data[k] = JSON.parse(data[k]);
} catch (e) {
throw new ApiError('INVALID_PARAM', {
throw new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
}, {
param: k,
reason: `cannot cast to ${param.type}`,
});
@ -93,7 +129,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
stack: e.stack,
},
});
throw new ApiError('INTERNAL_ERROR', {
throw new ApiError(null, {
e: {
message: e.message,
code: e.name,

View file

@ -24,13 +24,25 @@ export async function signup(opts: {
// Validate username
if (!Users.validateLocalUsername(username)) {
throw new ApiError('INVALID_USERNAME');
throw new ApiError({
message: 'This username is invalid.',
code: 'INVALID_USERNAME',
id: 'ece89f3c-d845-4d9a-850b-1735285e8cd4',
kind: 'client',
httpStatusCode: 400,
});
}
if (password != null && passwordHash == null) {
// Validate password
if (!Users.validatePassword(password)) {
throw new ApiError('INVALID_PASSWORD');
throw new ApiError({
message: 'This password is invalid.',
code: 'INVALID_PASSWORD',
id: 'a941905b-fe7b-43e2-8ecd-50ad3a2287ab',
kind: 'client',
httpStatusCode: 400,
});
}
// Generate hash of password
@ -41,14 +53,22 @@ export async function signup(opts: {
// Generate secret
const secret = generateUserToken();
const duplicateUsernameError = {
message: 'This username is not available.',
code: 'USED_USERNAME',
id: '7ddd595e-6860-4593-93c5-9fdbcb80cd81',
kind: 'client',
httpStatusCode: 409,
};
// Check username duplication
if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
throw new ApiError('USED_USERNAME');
throw new ApiError(duplicateUsernameError);
}
// Check deleted username duplication
if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) {
throw new ApiError('USED_USERNAME');
throw new ApiError(duplicateUsernameError);
}
const keyPair = await new Promise<string[]>((res, rej) =>
@ -77,7 +97,7 @@ export async function signup(opts: {
host: IsNull(),
});
if (exist) throw new ApiError('USED_USERNAME');
if (exist) throw new ApiError(duplicateUsernameError);
account = await transactionalEntityManager.save(new User({
id: genId(),

View file

@ -28,16 +28,22 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa
fs.unlink(file.path, () => {});
}
if (meta.requireFile && file == null) {
return Promise.reject(new ApiError('FILE_REQUIRED'));
}
if (meta.requireFile && file == null) return Promise.reject(new ApiError({
message: 'File required.',
code: 'FILE_REQUIRED',
id: '4267801e-70d1-416a-b011-4ee502885d8b',
}));
const valid = validate(params);
if (!valid) {
if (file) cleanup();
const errors = validate.errors!;
const err = new ApiError('INVALID_PARAM', {
const err = new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
}, {
param: errors[0].schemaPath,
reason: errors[0].message,
});

View file

@ -1,5 +1,4 @@
import { Schema } from '@/misc/schema.js';
import { errors } from './error.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
@ -271,12 +270,14 @@ import * as ep___serverInfo from './endpoints/server-info.js';
import * as ep___stats from './endpoints/stats.js';
import * as ep___sw_register from './endpoints/sw/register.js';
import * as ep___sw_unregister from './endpoints/sw/unregister.js';
import * as ep___test from './endpoints/test.js';
import * as ep___username_available from './endpoints/username/available.js';
import * as ep___users from './endpoints/users.js';
import * as ep___users_clips from './endpoints/users/clips.js';
import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
import * as ep___users_groups_create from './endpoints/users/groups/create.js';
import * as ep___users_groups_delete from './endpoints/users/groups/delete.js';
import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js';
@ -579,12 +580,14 @@ const eps = [
['stats', ep___stats],
['sw/register', ep___sw_register],
['sw/unregister', ep___sw_unregister],
['test', ep___test],
['username/available', ep___username_available],
['users', ep___users],
['users/clips', ep___users_clips],
['users/followers', ep___users_followers],
['users/following', ep___users_following],
['users/gallery/posts', ep___users_gallery_posts],
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
['users/groups/create', ep___users_groups_create],
['users/groups/delete', ep___users_groups_delete],
['users/groups/invitations/accept', ep___users_groups_invitations_accept],
@ -622,7 +625,13 @@ export interface IEndpointMeta {
readonly tags?: ReadonlyArray<string>;
readonly errors?: ReadonlyArray<keyof typeof errors>;
readonly errors?: {
readonly [key: string]: {
readonly message: string;
readonly code: string;
readonly id: string;
};
};
readonly res?: Schema;

View file

@ -8,7 +8,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_ANNOUNCEMENT'],
errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'ecad8040-a276-4e85-bda9-015a708d291e',
},
},
} as const;
export const paramDef = {
@ -23,7 +29,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const announcement = await Announcements.findOneBy({ id: ps.id });
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await Announcements.delete(announcement.id);
});

View file

@ -8,7 +8,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_ANNOUNCEMENT'],
errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc',
},
},
} as const;
export const paramDef = {
@ -26,7 +32,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const announcement = await Announcements.findOneBy({ id: ps.id });
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await Announcements.update(announcement.id, {
updatedAt: new Date(),

View file

@ -8,7 +8,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_FILE'],
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240',
},
},
res: {
type: 'object',
@ -174,7 +180,9 @@ export default define(meta, paramDef, async (ps, me) => {
}],
});
if (file == null) throw new ApiError('NO_SUCH_FILE');
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
return file;
});

View file

@ -13,7 +13,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_FILE'],
errors: {
noSuchFile: {
message: 'No such file.',
code: 'MO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
},
} as const;
export const paramDef = {
@ -28,7 +34,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const file = await DriveFiles.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError('NO_SUCH_FILE');
if (file == null) throw new ApiError(meta.errors.noSuchFile);
const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;

View file

@ -13,7 +13,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_EMOJI', 'INTERNAL_ERROR'],
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: 'e2785b66-dca3-4087-9cac-b93c541cc425',
},
},
res: {
type: 'object',
@ -40,7 +46,9 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const emoji = await Emojis.findOneBy({ id: ps.emojiId });
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
if (emoji == null) {
throw new ApiError(meta.errors.noSuchEmoji);
}
let driveFile: DriveFile;
@ -48,7 +56,7 @@ export default define(meta, paramDef, async (ps, me) => {
// Create file
driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
} catch (e) {
throw new ApiError('INTERNAL_ERROR', e);
throw new ApiError();
}
const copied = await Emojis.insert({

View file

@ -10,7 +10,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_EMOJI'],
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2',
},
},
} as const;
export const paramDef = {
@ -25,7 +31,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const emoji = await Emojis.findOneBy({ id: ps.id });
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
await Emojis.delete(emoji.id);

View file

@ -9,7 +9,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['NO_SUCH_EMOJI'],
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
},
},
} as const;
export const paramDef = {
@ -33,7 +39,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps) => {
const emoji = await Emojis.findOneBy({ id: ps.id });
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
await Emojis.update(emoji.id, {
updatedAt: new Date(),

View file

@ -1,5 +1,6 @@
import config from '@/config/index.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import define from '../../define.js';
export const meta = {
@ -309,7 +310,7 @@ export default define(meta, paramDef, async (ps, me) => {
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: config.maxNoteTextLength,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail,

View file

@ -9,7 +9,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
errors: ['INVALID_URL'],
errors: {
invalidUrl: {
message: 'Invalid URL',
code: 'INVALID_URL',
id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c',
},
},
res: {
type: 'object',
@ -52,8 +58,8 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
try {
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
} catch (e) {
throw new ApiError('INVALID_URL', e);
} catch {
throw new ApiError(meta.errors.invalidUrl);
}
return await addRelay(ps.inbox);

View file

@ -1,5 +1,6 @@
import { Meta } from '@/models/entities/meta.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { fetchMeta, setMeta } from '@/misc/fetch-meta.js';
import { db } from '@/db/postgre.js';
import define from '../../define.js';
export const meta = {
@ -374,10 +375,20 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = ps.deeplIsPro;
}
const meta = await fetchMeta();
await setMeta({
...meta,
...set,
await db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(Meta, {
order: {
id: 'DESC',
},
});
const meta = metas[0];
if (meta) {
await transactionalEntityManager.update(Meta, meta.id, set);
} else {
await transactionalEntityManager.save(Meta, set);
}
});
insertModerationLog(me, 'updateMeta');

View file

@ -11,7 +11,19 @@ export const meta = {
kind: 'write:account',
errors: ['NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'],
errors: {
noSuchUserList: {
message: 'No such user list.',
code: 'NO_SUCH_USER_LIST',
id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
},
noSuchUserGroup: {
message: 'No such user group.',
code: 'NO_SUCH_USER_GROUP',
id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682',
},
},
res: {
type: 'object',
@ -59,14 +71,18 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (userList == null) throw new ApiError('NO_SUCH_USER_LIST');
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
}
} else if (ps.src === 'group' && ps.userGroupId) {
userGroupJoining = await UserGroupJoinings.findOneBy({
userGroupId: ps.userGroupId,
userId: user.id,
});
if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP');
if (userGroupJoining == null) {
throw new ApiError(meta.errors.noSuchUserGroup);
}
}
const antenna = await Antennas.insert({

View file

@ -10,7 +10,13 @@ export const meta = {
kind: 'write:account',
errors: ['NO_SUCH_ANTENNA'],
errors: {
noSuchAntenna: {
message: 'No such antenna.',
code: 'NO_SUCH_ANTENNA',
id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df',
},
},
} as const;
export const paramDef = {
@ -28,7 +34,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}
await Antennas.delete(antenna.id);

View file

@ -1,4 +1,4 @@
import { readNote } from '@/services/note/read.js';
import readNote from '@/services/note/read.js';
import { Antennas, Notes, AntennaNotes } from '@/models/index.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
@ -14,7 +14,13 @@ export const meta = {
kind: 'read:account',
errors: ['NO_SUCH_ANTENNA'],
errors: {
noSuchAntenna: {
message: 'No such antenna.',
code: 'NO_SUCH_ANTENNA',
id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe',
},
},
res: {
type: 'array',
@ -47,7 +53,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)

View file

@ -9,7 +9,13 @@ export const meta = {
kind: 'read:account',
errors: ['NO_SUCH_ANTENNA'],
errors: {
noSuchAntenna: {
message: 'No such antenna.',
code: 'NO_SUCH_ANTENNA',
id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b',
},
},
res: {
type: 'object',
@ -34,7 +40,9 @@ export default define(meta, paramDef, async (ps, me) => {
userId: me.id,
});
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}
return await Antennas.pack(antenna);
});

View file

@ -10,7 +10,25 @@ export const meta = {
kind: 'write:account',
errors: ['NO_SUCH_ANTENNA', 'NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'],
errors: {
noSuchAntenna: {
message: 'No such antenna.',
code: 'NO_SUCH_ANTENNA',
id: '10c673ac-8852-48eb-aa1f-f5b67f069290',
},
noSuchUserList: {
message: 'No such user list.',
code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
},
noSuchUserGroup: {
message: 'No such user group.',
code: 'NO_SUCH_USER_GROUP',
id: '109ed789-b6eb-456e-b8a9-6059d567d385',
},
},
res: {
type: 'object',
@ -56,7 +74,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}
let userList;
let userGroupJoining;
@ -67,14 +87,18 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (userList == null) throw new ApiError('NO_SUCH_USER_LIST');
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
}
} else if (ps.src === 'group' && ps.userGroupId) {
userGroupJoining = await UserGroupJoinings.findOneBy({
userGroupId: ps.userGroupId,
userId: user.id,
});
if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP');
if (userGroupJoining == null) {
throw new ApiError(meta.errors.noSuchUserGroup);
}
}
await Antennas.update(antenna.id, {

View file

@ -12,6 +12,9 @@ export const meta = {
max: 30,
},
errors: {
},
res: {
type: 'object',
optional: false, nullable: false,

View file

@ -24,7 +24,13 @@ export const meta = {
max: 30,
},
errors: ['NO_SUCH_OBJECT'],
errors: {
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
id: 'dc94d745-1262-4e63-a17d-fecaa57efc82',
},
},
res: {
optional: false, nullable: false,
@ -77,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
if (object) {
return object;
} else {
throw new ApiError('NO_SUCH_OBJECT');
throw new ApiError(meta.errors.noSuchObject);
}
});

View file

@ -2,7 +2,6 @@ import { Apps } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { unique } from '@/prelude/array.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { kinds } from '@/misc/api-permissions.js';
import define from '../../define.js';
export const meta = {
@ -22,14 +21,10 @@ export const paramDef = {
properties: {
name: { type: 'string' },
description: { type: 'string' },
permission: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
enum: kinds,
},
},
permission: { type: 'array', uniqueItems: true, items: {
type: 'string',
// FIXME: add enum of possible permissions
} },
callbackUrl: { type: 'string', nullable: true },
},
required: ['name', 'description', 'permission'],

View file

@ -5,7 +5,13 @@ import { ApiError } from '../../error.js';
export const meta = {
tags: ['app'],
errors: ['NO_SUCH_APP'],
errors: {
noSuchApp: {
message: 'No such app.',
code: 'NO_SUCH_APP',
id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3',
},
},
res: {
type: 'object',
@ -27,12 +33,14 @@ export default define(meta, paramDef, async (ps, user, token) => {
const isSecure = user != null && token == null;
// Lookup app
const app = await Apps.findOneBy({ id: ps.appId });
const ap = await Apps.findOneBy({ id: ps.appId });
if (app == null) throw new ApiError('NO_SUCH_APP');
if (ap == null) {
throw new ApiError(meta.errors.noSuchApp);
}
return await Apps.pack(app, user, {
return await Apps.pack(ap, user, {
detail: true,
includeSecret: isSecure && (app.userId === user!.id),
includeSecret: isSecure && (ap.userId === user!.id),
});
});

View file

@ -12,7 +12,13 @@ export const meta = {
secure: true,
errors: ['NO_SUCH_SESSION'],
errors: {
noSuchSession: {
message: 'No such session.',
code: 'NO_SUCH_SESSION',
id: '9c72d8de-391a-43c1-9d06-08d29efde8df',
},
},
} as const;
export const paramDef = {
@ -29,7 +35,9 @@ export default define(meta, paramDef, async (ps, user) => {
const session = await AuthSessions
.findOneBy({ token: ps.token });
if (session == null) throw new ApiError('NO_SUCH_SESSION');
if (session == null) {
throw new ApiError(meta.errors.noSuchSession);
}
// Generate access token
const accessToken = secureRndstr(32, true);

View file

@ -26,7 +26,13 @@ export const meta = {
},
},
errors: ['NO_SUCH_APP'],
errors: {
noSuchApp: {
message: 'No such app.',
code: 'NO_SUCH_APP',
id: '92f93e63-428e-4f2f-a5a4-39e1407fe998',
},
},
} as const;
export const paramDef = {
@ -45,7 +51,7 @@ export default define(meta, paramDef, async (ps) => {
});
if (app == null) {
throw new ApiError('NO_SUCH_APP');
throw new ApiError(meta.errors.noSuchApp);
}
// Generate token

View file

@ -7,7 +7,13 @@ export const meta = {
requireCredential: false,
errors: ['NO_SUCH_SESSION'],
errors: {
noSuchSession: {
message: 'No such session.',
code: 'NO_SUCH_SESSION',
id: 'bd72c97d-eba7-4adb-a467-f171b8847250',
},
},
res: {
type: 'object',
@ -46,7 +52,9 @@ export default define(meta, paramDef, async (ps, user) => {
token: ps.token,
});
if (session == null) throw new ApiError('NO_SUCH_SESSION');
if (session == null) {
throw new ApiError(meta.errors.noSuchSession);
}
return await AuthSessions.pack(session, user);
});

View file

@ -24,7 +24,25 @@ export const meta = {
},
},
errors: ['NO_SUCH_APP', 'NO_SUCH_SESSION', 'PENDING_SESSION'],
errors: {
noSuchApp: {
message: 'No such app.',
code: 'NO_SUCH_APP',
id: 'fcab192a-2c5a-43b7-8ad8-9b7054d8d40d',
},
noSuchSession: {
message: 'No such session.',
code: 'NO_SUCH_SESSION',
id: '5b5a1503-8bc8-4bd0-8054-dc189e8cdcb3',
},
pendingSession: {
message: 'This session is not completed yet.',
code: 'PENDING_SESSION',
id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e',
},
},
} as const;
export const paramDef = {
@ -43,7 +61,9 @@ export default define(meta, paramDef, async (ps) => {
secret: ps.appSecret,
});
if (app == null) throw new ApiError('NO_SUCH_APP');
if (app == null) {
throw new ApiError(meta.errors.noSuchApp);
}
// Fetch token
const session = await AuthSessions.findOneBy({
@ -51,9 +71,13 @@ export default define(meta, paramDef, async (ps) => {
appId: app.id,
});
if (session == null) throw new ApiError('NO_SUCH_SESSION');
if (session == null) {
throw new ApiError(meta.errors.noSuchSession);
}
if (session.userId == null) throw new ApiError('PENDING_SESSION');
if (session.userId == null) {
throw new ApiError(meta.errors.pendingSession);
}
// Lookup access token
const accessToken = await AccessTokens.findOneByOrFail({

View file

@ -17,7 +17,25 @@ export const meta = {
kind: 'write:blocks',
errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'ALREADY_BLOCKING'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e',
},
blockeeIsYourself: {
message: 'Blockee is yourself.',
code: 'BLOCKEE_IS_YOURSELF',
id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6',
},
alreadyBlocking: {
message: 'You are already blocking that user.',
code: 'ALREADY_BLOCKING',
id: '787fed64-acb9-464a-82eb-afbd745b9614',
},
},
res: {
type: 'object',
@ -39,11 +57,13 @@ export default define(meta, paramDef, async (ps, user) => {
const blocker = await Users.findOneByOrFail({ id: user.id });
// 自分自身
if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF');
if (user.id === ps.userId) {
throw new ApiError(meta.errors.blockeeIsYourself);
}
// Get blockee
const blockee = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
blockeeId: blockee.id,
});
if (exist != null) throw new ApiError('ALREADY_BLOCKING');
if (exist != null) {
throw new ApiError(meta.errors.alreadyBlocking);
}
await create(blocker, blockee);

View file

@ -17,7 +17,25 @@ export const meta = {
kind: 'write:blocks',
errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'NOT_BLOCKING'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '8621d8bf-c358-4303-a066-5ea78610eb3f',
},
blockeeIsYourself: {
message: 'Blockee is yourself.',
code: 'BLOCKEE_IS_YOURSELF',
id: '06f6fac6-524b-473c-a354-e97a40ae6eac',
},
notBlocking: {
message: 'You are not blocking that user.',
code: 'NOT_BLOCKING',
id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd',
},
},
res: {
type: 'object',
@ -36,14 +54,16 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
// Check if the blockee is yourself
if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF');
const blocker = await Users.findOneByOrFail({ id: user.id });
// Check if the blockee is yourself
if (user.id === ps.userId) {
throw new ApiError(meta.errors.blockeeIsYourself);
}
// Get blockee
const blockee = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
blockeeId: blockee.id,
});
if (exist == null) throw new ApiError('NOT_BLOCKING');
if (exist == null) {
throw new ApiError(meta.errors.notBlocking);
}
// Delete blocking
await deleteBlocking(blocker, blockee);

View file

@ -17,7 +17,13 @@ export const meta = {
ref: 'Channel',
},
errors: ['NO_SUCH_FILE'],
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050',
},
},
} as const;
export const paramDef = {
@ -39,7 +45,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (banner == null) throw new ApiError('NO_SUCH_FILE');
if (banner == null) {
throw new ApiError(meta.errors.noSuchFile);
}
}
const channel = await Channels.insert({

View file

@ -11,7 +11,13 @@ export const meta = {
kind: 'write:channels',
errors: ['NO_SUCH_CHANNEL'],
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: 'c0031718-d573-4e85-928e-10039f1fbb68',
},
},
} as const;
export const paramDef = {
@ -28,7 +34,9 @@ export default define(meta, paramDef, async (ps, user) => {
id: ps.channelId,
});
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await ChannelFollowings.insert({
id: genId(),

View file

@ -13,7 +13,13 @@ export const meta = {
ref: 'Channel',
},
errors: ['NO_SUCH_CHANNEL'],
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '6f6c314b-7486-4897-8966-c04a66a02923',
},
},
} as const;
export const paramDef = {
@ -30,7 +36,9 @@ export default define(meta, paramDef, async (ps, me) => {
id: ps.channelId,
});
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
return await Channels.pack(channel, me);
});

View file

@ -19,7 +19,13 @@ export const meta = {
},
},
errors: ['NO_SUCH_CHANNEL'],
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f',
},
},
} as const;
export const paramDef = {
@ -41,7 +47,9 @@ export default define(meta, paramDef, async (ps, user) => {
id: ps.channelId,
});
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)

View file

@ -10,7 +10,13 @@ export const meta = {
kind: 'write:channels',
errors: ['NO_SUCH_CHANNEL'],
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6',
},
},
} as const;
export const paramDef = {
@ -27,7 +33,9 @@ export default define(meta, paramDef, async (ps, user) => {
id: ps.channelId,
});
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await ChannelFollowings.delete({
followerId: user.id,

View file

@ -15,7 +15,25 @@ export const meta = {
ref: 'Channel',
},
errors: ['ACCESS_DENIED', 'NO_SUCH_CHANNEL', 'NO_SUCH_FILE'],
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512',
},
accessDenied: {
message: 'You do not have edit privilege of the channel.',
code: 'ACCESS_DENIED',
id: '1fb7cb09-d46a-4fdf-b8df-057788cce513',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b',
},
},
} as const;
export const paramDef = {
@ -35,9 +53,13 @@ export default define(meta, paramDef, async (ps, me) => {
id: ps.channelId,
});
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
if (channel.userId !== me.id) throw new ApiError('ACCESS_DENIED', 'You are not the owner of this channel.');
if (channel.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
// eslint:disable-next-line:no-unnecessary-initializer
let banner = undefined;
@ -47,7 +69,9 @@ export default define(meta, paramDef, async (ps, me) => {
userId: me.id,
});
if (banner == null) throw new ApiError('NO_SUCH_FILE');
if (banner == null) {
throw new ApiError(meta.errors.noSuchFile);
}
} else if (ps.bannerId === null) {
banner = null;
}

View file

@ -11,7 +11,25 @@ export const meta = {
kind: 'write:account',
errors: ['ALREADY_CLIPPED', 'NO_SUCH_CLIP', 'NO_SUCH_NOTE'],
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
},
alreadyClipped: {
message: 'The note has already been clipped.',
code: 'ALREADY_CLIPPED',
id: '734806c4-542c-463a-9311-15c512803965',
},
},
} as const;
export const paramDef = {
@ -30,10 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const note = await getNote(ps.noteId, user).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE');
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
@ -42,7 +62,9 @@ export default define(meta, paramDef, async (ps, user) => {
clipId: clip.id,
});
if (exist != null) throw new ApiError('ALREADY_CLIPPED');
if (exist != null) {
throw new ApiError(meta.errors.alreadyClipped);
}
await ClipNotes.insert({
id: genId(),

View file

@ -9,7 +9,13 @@ export const meta = {
kind: 'write:account',
errors: ['NO_SUCH_CLIP'],
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
},
},
} as const;
export const paramDef = {
@ -27,7 +33,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
await Clips.delete(clip.id);
});

View file

@ -13,7 +13,13 @@ export const meta = {
kind: 'read:account',
errors: ['NO_SUCH_CLIP'],
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
},
},
res: {
type: 'array',
@ -43,10 +49,12 @@ export default define(meta, paramDef, async (ps, user) => {
id: ps.clipId,
});
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
if (!clip.isPublic && (user == null || (clip.userId !== user.id))) {
throw new ApiError('NO_SUCH_CLIP');
throw new ApiError(meta.errors.noSuchClip);
}
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)

View file

@ -10,7 +10,19 @@ export const meta = {
kind: 'write:account',
errors: ['NO_SUCH_CLIP', 'NO_SUCH_NOTE', 'NOT_CLIPPED'],
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52',
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
},
},
} as const;
export const paramDef = {
@ -29,17 +41,17 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE');
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
const { affected } = await ClipNotes.delete({
await ClipNotes.delete({
noteId: note.id,
clipId: clip.id,
});
if (affected === 0) throw new ApiError('NOT_CLIPPED');
});

View file

@ -9,7 +9,13 @@ export const meta = {
kind: 'read:account',
errors: ['NO_SUCH_CLIP'],
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
},
},
res: {
type: 'object',
@ -33,10 +39,12 @@ export default define(meta, paramDef, async (ps, me) => {
id: ps.clipId,
});
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
throw new ApiError('NO_SUCH_CLIP');
throw new ApiError(meta.errors.noSuchClip);
}
return await Clips.pack(clip);

View file

@ -9,7 +9,13 @@ export const meta = {
kind: 'write:account',
errors: ['NO_SUCH_CLIP'],
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
},
},
res: {
type: 'object',
@ -37,7 +43,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
await Clips.update(clip.id, {
name: ps.name,

View file

@ -21,7 +21,13 @@ export const meta = {
},
},
errors: ['NO_SUCH_FILE'],
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'c118ece3-2e4b-4296-99d1-51756e32d232',
},
},
} as const;
export const paramDef = {
@ -40,7 +46,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (file == null) throw new ApiError('NO_SUCH_FILE');
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
const notes = await Notes.createQueryBuilder('note')
.where(':file = ANY(note.fileIds)', { file: file.id })

View file

@ -28,7 +28,13 @@ export const meta = {
ref: 'DriveFile',
},
errors: ['INTERNAL_ERROR', 'INVALID_FILE_NAME'],
errors: {
invalidFileName: {
message: 'Invalid file name.',
code: 'INVALID_FILE_NAME',
id: 'f449b209-0c60-4e51-84d5-29486263bfd4',
},
},
} as const;
export const paramDef = {
@ -54,7 +60,7 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
} else if (name === 'blob') {
name = null;
} else if (!DriveFiles.validateFileName(name)) {
throw new ApiError('INVALID_FILE_NAME');
throw new ApiError(meta.errors.invalidFileName);
}
} else {
name = null;
@ -68,7 +74,7 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
if (e instanceof Error || typeof e === 'string') {
apiLogger.error(e);
}
throw new ApiError('INTERNAL_ERROR');
throw new ApiError();
} finally {
cleanup!();
}

View file

@ -13,7 +13,19 @@ export const meta = {
description: 'Delete an existing drive file.',
errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'],
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: '908939ec-e52b-4458-b395-1025195cea58',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '5eb8d909-2540-4970-90b8-dd6f86088121',
},
},
} as const;
export const paramDef = {
@ -28,10 +40,12 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const file = await DriveFiles.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError('NO_SUCH_FILE');
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
throw new ApiError('ACCESS_DENIED');
throw new ApiError(meta.errors.accessDenied);
}
// Delete

View file

@ -18,7 +18,19 @@ export const meta = {
ref: 'DriveFile',
},
errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'],
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: '067bc436-2718-4795-b0fb-ecbe43949e31',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '25b73c73-68b1-41d0-bad1-381cfdf6579f',
},
},
} as const;
export const paramDef = {
@ -57,10 +69,12 @@ export default define(meta, paramDef, async (ps, user) => {
});
}
if (file == null) throw new ApiError('NO_SUCH_FILE');
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
throw new ApiError('ACCESS_DENIED');
throw new ApiError(meta.errors.accessDenied);
}
return await DriveFiles.pack(file, {

View file

@ -12,7 +12,31 @@ export const meta = {
description: 'Update the properties of a drive file.',
errors: ['ACCESS_DENIED', 'INVALID_FILE_NAME', 'NO_SUCH_FILE', 'NO_SUCH_FOLDER'],
errors: {
invalidFileName: {
message: 'Invalid file name.',
code: 'INVALID_FILE_NAME',
id: '395e7156-f9f0-475e-af89-53c3c23080c2',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'e7778c7e-3af9-49cd-9690-6dbc3e6c972d',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '01a53b27-82fc-445b-a0c1-b558465a8ed2',
},
noSuchFolder: {
message: 'No such folder.',
code: 'NO_SUCH_FOLDER',
id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73',
},
},
res: {
type: 'object',
@ -37,15 +61,17 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const file = await DriveFiles.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError('NO_SUCH_FILE');
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
throw new ApiError('ACCESS_DENIED');
throw new ApiError(meta.errors.accessDenied);
}
if (ps.name) file.name = ps.name;
if (!DriveFiles.validateFileName(file.name)) {
throw new ApiError('INVALID_FILE_NAME');
throw new ApiError(meta.errors.invalidFileName);
}
if (ps.comment !== undefined) file.comment = ps.comment;
@ -61,7 +87,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
file.folderId = folder.id;
}

View file

@ -11,7 +11,13 @@ export const meta = {
kind: 'write:drive',
errors: ['NO_SUCH_FOLDER'],
errors: {
noSuchFolder: {
message: 'No such folder.',
code: 'NO_SUCH_FOLDER',
id: '53326628-a00d-40a6-a3cd-8975105c0f95',
},
},
res: {
type: 'object' as const,
@ -40,7 +46,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (parent == null) throw new ApiError('NO_SUCH_FOLDER');
if (parent == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
}
// Create folder

View file

@ -10,7 +10,19 @@ export const meta = {
kind: 'write:drive',
errors: ['HAS_CHILD_FILES_OR_FOLDERS', 'NO_SUCH_FOLDER'],
errors: {
noSuchFolder: {
message: 'No such folder.',
code: 'NO_SUCH_FOLDER',
id: '1069098f-c281-440f-b085-f9932edbe091',
},
hasChildFilesOrFolders: {
message: 'This folder has child files or folders.',
code: 'HAS_CHILD_FILES_OR_FOLDERS',
id: 'b0fc8a17-963c-405d-bfbc-859a487295e1',
},
},
} as const;
export const paramDef = {
@ -29,7 +41,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
const [childFoldersCount, childFilesCount] = await Promise.all([
DriveFolders.countBy({ parentId: folder.id }),
@ -37,7 +51,7 @@ export default define(meta, paramDef, async (ps, user) => {
]);
if (childFoldersCount !== 0 || childFilesCount !== 0) {
throw new ApiError('HAS_CHILD_FILES_OR_FOLDERS');
throw new ApiError(meta.errors.hasChildFilesOrFolders);
}
await DriveFolders.delete(folder.id);

View file

@ -15,7 +15,13 @@ export const meta = {
ref: 'DriveFolder',
},
errors: ['NO_SUCH_FOLDER'],
errors: {
noSuchFolder: {
message: 'No such folder.',
code: 'NO_SUCH_FOLDER',
id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9',
},
},
} as const;
export const paramDef = {
@ -34,7 +40,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
return await DriveFolders.pack(folder, {
detail: true,

View file

@ -10,7 +10,25 @@ export const meta = {
kind: 'write:drive',
errors: ['NO_SUCH_FOLDER', 'NO_SUCH_PARENT_FOLDER', 'RECURSIVE_FOLDER'],
errors: {
noSuchFolder: {
message: 'No such folder.',
code: 'NO_SUCH_FOLDER',
id: 'f7974dac-2c0d-4a27-926e-23583b28e98e',
},
noSuchParentFolder: {
message: 'No such parent folder.',
code: 'NO_SUCH_PARENT_FOLDER',
id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1',
},
recursiveNesting: {
message: 'It can not be structured like nesting folders recursively.',
code: 'NO_SUCH_PARENT_FOLDER',
id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1',
},
},
res: {
type: 'object',
@ -37,13 +55,15 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
if (folder == null) {
throw new ApiError(meta.errors.noSuchFolder);
}
if (ps.name) folder.name = ps.name;
if (ps.parentId !== undefined) {
if (ps.parentId === folder.id) {
throw new ApiError('RECURSIVE_FOLDER');
throw new ApiError(meta.errors.recursiveNesting);
} else if (ps.parentId === null) {
folder.parentId = null;
} else {
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
});
if (parent == null) throw new ApiError('NO_SUCH_PARENT_FOLDER');
if (parent == null) {
throw new ApiError(meta.errors.noSuchParentFolder);
}
// Check if the circular reference will occur
async function checkCircle(folderId: string): Promise<boolean> {
@ -73,7 +95,7 @@ export default define(meta, paramDef, async (ps, user) => {
if (parent.parentId !== null) {
if (await checkCircle(parent.parentId)) {
throw new ApiError('RECURSIVE_FOLDER');
throw new ApiError(meta.errors.recursiveNesting);
}
}

View file

@ -5,8 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
export const meta = {
tags: ['federation'],
requireCredential: true,
requireAdmin: true,
requireCredential: false,
res: {
type: 'array',

View file

@ -5,8 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
export const meta = {
tags: ['federation'],
requireCredential: true,
requireAdmin: true,
requireCredential: false,
res: {
type: 'array',

View file

@ -5,7 +5,7 @@ import define from '../../define.js';
export const meta = {
tags: ['federation'],
requireCredential: true,
requireCredential: false,
res: {
type: 'array',

View file

@ -5,7 +5,7 @@ import define from '../../define.js';
export const meta = {
tags: ['federation'],
requireCredential: true,
requireCredential: false,
res: {
oneOf: [{

View file

@ -6,7 +6,7 @@ import define from '../../define.js';
export const meta = {
tags: ['federation'],
requireCredential: true,
requireCredential: false,
allowGet: true,
cacheSec: 60 * 60,

View file

@ -5,7 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
export const meta = {
tags: ['federation'],
requireCredential: true,
requireCredential: false,
res: {
type: 'array',

View file

@ -8,7 +8,7 @@ const rssParser = new Parser();
export const meta = {
tags: ['meta'],
requireCredential: true,
requireCredential: false,
allowGet: true,
cacheSec: 60 * 3,
} as const;

View file

@ -18,7 +18,37 @@ export const meta = {
kind: 'write:following',
errors: ['ALREADY_FOLLOWING', 'BLOCKING', 'BLOCKED', 'FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
},
followeeIsYourself: {
message: 'Followee is yourself.',
code: 'FOLLOWEE_IS_YOURSELF',
id: '26fbe7bb-a331-4857-af17-205b426669a9',
},
alreadyFollowing: {
message: 'You are already following that user.',
code: 'ALREADY_FOLLOWING',
id: '35387507-38c7-4cb9-9197-300b93783fa0',
},
blocking: {
message: 'You are blocking that user.',
code: 'BLOCKING',
id: '4e2206ec-aa4f-4960-b865-6c23ac38e2d9',
},
blocked: {
message: 'You are blocked by that user.',
code: 'BLOCKED',
id: 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0',
},
},
res: {
type: 'object',
@ -40,11 +70,13 @@ export default define(meta, paramDef, async (ps, user) => {
const follower = user;
// 自分自身
if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF');
if (user.id === ps.userId) {
throw new ApiError(meta.errors.followeeIsYourself);
}
// Get followee
const followee = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
@ -54,14 +86,16 @@ export default define(meta, paramDef, async (ps, user) => {
followeeId: followee.id,
});
if (exist != null) throw new ApiError('ALREADY_FOLLOWING');
if (exist != null) {
throw new ApiError(meta.errors.alreadyFollowing);
}
try {
await create(follower, followee);
} catch (e) {
if (e instanceof IdentifiableError) {
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError('BLOCKING');
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError('BLOCKED');
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
}
throw e;
}

View file

@ -17,7 +17,25 @@ export const meta = {
kind: 'write:following',
errors: ['FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8',
},
followeeIsYourself: {
message: 'Followee is yourself.',
code: 'FOLLOWEE_IS_YOURSELF',
id: 'd9e400b9-36b0-4808-b1d8-79e707f1296c',
},
notFollowing: {
message: 'You are not following that user.',
code: 'NOT_FOLLOWING',
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09',
},
},
res: {
type: 'object',
@ -39,11 +57,13 @@ export default define(meta, paramDef, async (ps, user) => {
const follower = user;
// Check if the followee is yourself
if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF');
if (user.id === ps.userId) {
throw new ApiError(meta.errors.followeeIsYourself);
}
// Get followee
const followee = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
followeeId: followee.id,
});
if (exist == null) throw new ApiError('NOT_FOLLOWING');
if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await deleteFollowing(follower, followee);

View file

@ -17,7 +17,25 @@ export const meta = {
kind: 'write:following',
errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8',
},
followerIsYourself: {
message: 'Follower is yourself.',
code: 'FOLLOWER_IS_YOURSELF',
id: '07dc03b9-03da-422d-885b-438313707662',
},
notFollowing: {
message: 'The other use is not following you.',
code: 'NOT_FOLLOWING',
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09',
},
},
res: {
type: 'object',
@ -39,11 +57,13 @@ export default define(meta, paramDef, async (ps, user) => {
const followee = user;
// Check if the follower is yourself
if (user.id === ps.userId) throw new ApiError('FOLLOWER_IS_YOURSELF');
if (user.id === ps.userId) {
throw new ApiError(meta.errors.followerIsYourself);
}
// Get follower
const follower = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
followeeId: followee.id,
});
if (exist == null) throw new ApiError('NOT_FOLLOWING');
if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await deleteFollowing(follower, followee);

View file

@ -10,7 +10,18 @@ export const meta = {
kind: 'write:following',
errors: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '66ce1645-d66c-46bb-8b79-96739af885bd',
},
noFollowRequest: {
message: 'No follow request.',
code: 'NO_FOLLOW_REQUEST',
id: 'bcde4f8b-0913-4614-8881-614e522fb041',
},
},
} as const;
export const paramDef = {
@ -25,12 +36,12 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
// Fetch follower
const follower = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
await acceptFollowRequest(user, follower).catch(e => {
if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError('NO_SUCH_FOLLOW_REQUEST');
if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest);
throw e;
});

View file

@ -12,7 +12,19 @@ export const meta = {
kind: 'write:following',
errors: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab',
},
followRequestNotFound: {
message: 'Follow request not found.',
code: 'FOLLOW_REQUEST_NOT_FOUND',
id: '089b125b-d338-482a-9a09-e2622ac9f8d4',
},
},
res: {
type: 'object',
@ -33,7 +45,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
// Fetch followee
const followee = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
@ -41,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
await cancelFollowRequest(followee, user);
} catch (e) {
if (e instanceof IdentifiableError) {
if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError('NO_SUCH_FOLLOW_REQUEST');
if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound);
}
throw e;
}

View file

@ -10,7 +10,13 @@ export const meta = {
kind: 'write:following',
errors: ['NO_SUCH_USER'],
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555',
},
},
} as const;
export const paramDef = {
@ -25,7 +31,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
// Fetch follower
const follower = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});

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