forked from FoundKeyGang/FoundKey
Compare commits
116 commits
secure-mod
...
main
Author | SHA1 | Date | |
---|---|---|---|
9f6be8d557 | |||
9d9b2da6cc | |||
d1ec058d5c | |||
131c12a30b | |||
8d6476af2a | |||
57299f0df6 | |||
b0489abd7f | |||
26f1b66c6a | |||
1d877e97f0 | |||
0571a0843c | |||
56033c26f0 | |||
80af8a143e | |||
a3468491a7 | |||
486be564e8 | |||
c49f529ccb | |||
8979e779da | |||
|
b1bb5b28c5 | ||
f3c38ad5c8 | |||
899b01a031 | |||
a27a29b371 | |||
66a9d27ab1 | |||
ed14fe8e79 | |||
d411ea6281 | |||
5d23aa9e69 | |||
5b61941e4c | |||
ca90cedba0 | |||
2496b385ce | |||
54075789cd | |||
b2c800e654 | |||
5713f329ca | |||
609312bb82 | |||
7939d130aa | |||
489eea0c67 | |||
6f65326b32 | |||
408c5c3c65 | |||
e79d7879c6 | |||
e8ecd71f8a | |||
0db0db9a87 | |||
6df2f7c55c | |||
ac240eb58d | |||
e27494cf3e | |||
d725f93d40 | |||
6db9b76f46 | |||
f50b04b015 | |||
3fe1f7e70e | |||
eff9dbb5ee | |||
fb80fd1fbd | |||
2a33d0ac83 | |||
fb5f498641 | |||
23fbdfdf1f | |||
5b7a7794ab | |||
bd0c06e2d0 | |||
c282ed7683 | |||
47b2f619a6 | |||
240ad1cca6 | |||
eb1ecd90e6 | |||
14c7d2bf53 | |||
4bfbe0dd96 | |||
2aafe8fc9f | |||
7a64a3858d | |||
d0564759a5 | |||
253bffd974 | |||
735b9ab502 | |||
fb76843c19 | |||
1dd935dc0c | |||
934ee82b8f | |||
66d7b69377 | |||
c3c7164dfb | |||
a991740e00 | |||
4dc97d5b65 | |||
384e8c49b7 | |||
|
c5e1c42d0a | ||
|
f74395c386 | ||
|
b2c483faf5 | ||
|
5bf1e5ad71 | ||
cd55d7a56f | |||
a0ef32f4f6 | |||
ba911dab65 | |||
7ec8729d90 | |||
f97e990ed3 | |||
c36cca30cb | |||
43644494d3 | |||
923c93da12 | |||
aa1e4d0fbc | |||
bfba54524d | |||
d83c1c3851 | |||
3da7221eec | |||
9544cd69d2 | |||
b359b01700 | |||
cfb8723618 | |||
ee70ad52fc | |||
4c5aa9e538 | |||
4b6c3b2f37 | |||
fbf7ea07c9 | |||
507dede6da | |||
f0f673843e | |||
fed41d8d15 | |||
7257338077 | |||
3aa1d3bf97 | |||
f4ee8b321e | |||
04d4dd323f | |||
e814fdc7d1 | |||
f2f547172e | |||
f17485d8a2 | |||
70eec26b74 | |||
a74c1d9126 | |||
811d5cd0d7 | |||
d762143b89 | |||
21c1e5c06c | |||
91a4f38871 | |||
756ecbb1f7 | |||
b431471fd1 | |||
7cd11e7afd | |||
0b8fa2665c | |||
421b42d07d | |||
8920eeb86a |
265 changed files with 2354 additions and 3829 deletions
|
@ -124,6 +124,9 @@ redis:
|
|||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
||||
|
||||
# Max note text length (in characters)
|
||||
#maxNoteTextLength: 3000
|
||||
|
||||
#allowedPrivateNetworks: [
|
||||
# '127.0.0.1/32'
|
||||
#]
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
.autogen
|
||||
.github
|
||||
.travis
|
||||
.vscode
|
||||
.config
|
||||
Dockerfile
|
||||
|
@ -12,4 +10,3 @@ elasticsearch/
|
|||
node_modules/
|
||||
redis/
|
||||
files/
|
||||
misskey-assets/
|
||||
|
|
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -11,37 +11,84 @@ Unreleased changes should not be listed in this file.
|
|||
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
|
||||
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
||||
|
||||
## Unreleased
|
||||
## 13.0.0-preview2 - 2022-10-16
|
||||
### Security
|
||||
- server: Update `multer` dependency to resolve [CVE-2022-24434](https://nvd.nist.gov/vuln/detail/CVE-2022-24434)
|
||||
- server: Update `file-type`, `got`, and `sharp` dependencies to fix various security issues
|
||||
|
||||
### Added
|
||||
- Client: Show instance info in ticker
|
||||
- Client: Readded group pages
|
||||
- Client: add re-collapsing to quoted notes
|
||||
- allow to mute only renotes of a user
|
||||
- allow to export only selected custom emoji
|
||||
- client: improve emoji picker search
|
||||
- client: Extend Emoji list
|
||||
- client: show alt text in image viewer
|
||||
- client: Show instance info in ticker
|
||||
- client: Readded group pages
|
||||
- client: add re-collapsing to quoted notes
|
||||
- server: allow files storage path to be set explicitly
|
||||
- server: refactor expiring data and expire signins after 60 days
|
||||
- server: send delete activity to all known instances
|
||||
- server: add automatic dead instance detection
|
||||
|
||||
### Changed
|
||||
- Client: Use consistent date formatting based on language setting
|
||||
- Client: Add threshold to reduce occurances of "future" timestamps
|
||||
- Pages have been considerably simplified, several of the very complex features have been removed.
|
||||
- foundkey-js: Sync possible endpoints from backend
|
||||
- foundkey-js: update LiteInstanceMetadata fields
|
||||
- meta: use parallel and incremental builds
|
||||
- meta: update WORKDIR to foundkey
|
||||
- meta: update dependencies
|
||||
- client: consolidate about & notifications pages
|
||||
- client: include renote in visibility computation
|
||||
- client: make emoji amount slider more intuitive
|
||||
- client: sort emojis by query similarity in fuzzy picker
|
||||
- client: discard drafts that are just the default state
|
||||
- client: Use consistent date formatting based on language setting
|
||||
- client: Add threshold to reduce occurances of "future" timestamps
|
||||
- server: mute notifications in muted threads
|
||||
- server: allow for source lang to be overridden in note/translate
|
||||
- server: allow redis family to be specified as a string
|
||||
- server: increase image description limit to 2048 characters
|
||||
- server: Pages have been considerably simplified, several of the very complex features have been removed.
|
||||
Pages are now MFM only.
|
||||
**For admins:** There is a migration in place to convert page contents to text, but not everything can be migrated.
|
||||
You might want to check if you have any more complex pages on your instance and ask users to migrate them by hand.
|
||||
Or generally advise all users to simplify their pages to only text.
|
||||
|
||||
### Removed
|
||||
- Okteto config and Helm chart
|
||||
- Client: acrylic styling
|
||||
- Client: Twitter embeds, the standard URL preview is used instead.
|
||||
- Promotion entities and endpoints
|
||||
- Server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
|
||||
Foundkey will now work as if it was set to `true`.
|
||||
|
||||
### Fixed
|
||||
- Client: Notifications for ended polls can now be turned off
|
||||
- Client: Emoji picker should load faster now
|
||||
- Server: Blocking remote accounts
|
||||
- client: alt text dialog properly handles non-images
|
||||
- client: Fix style scoping in MkMention
|
||||
- client: default instance ticker name to instance's domain name
|
||||
- client: improve error message for empty gallery posts
|
||||
- client: fix default-selected reply scopes
|
||||
- client: Make MFM cheatsheet interactive again
|
||||
- client: Fix reports not showing in control panel
|
||||
- client: make hard coded strings in emoji admin panel internationalized
|
||||
- client: Notifications for ended polls can now be turned off
|
||||
- client: improve emoji picker performance
|
||||
- server: Blocking remote accounts
|
||||
- server: fix table name used in toHtml
|
||||
- server: Fix appendChildren TypeError
|
||||
- server: ensure only own notifications can be marked as read
|
||||
- server: render HTML mentions correctly
|
||||
- server: increase requestId max size for GNU Social
|
||||
- server: fix HTTP GET parameters in OpenAPI docs
|
||||
- server: proper error messages for creating accounts
|
||||
- server: Fix thread muting queries
|
||||
- docker: add built foundkey-js files to container
|
||||
- service worker: Remove fetch handler from service worker
|
||||
|
||||
### Security
|
||||
- Server: Update `multer` dependency to resolve [CVE-2022-24434](https://nvd.nist.gov/vuln/detail/CVE-2022-24434)
|
||||
- Server: Update `file-type`, `got`, and `sharp` dependencies to fix various security issues
|
||||
### Removed
|
||||
- remove misskey-assets submodule
|
||||
- server: remove room data from user
|
||||
- client: remove ai mode
|
||||
- client: remove "Disable AiScript on Pages" setting
|
||||
- client: acrylic styling
|
||||
- client: Twitter embeds, the standard URL preview is used instead.
|
||||
- foundkey-js: remove room api endpoints
|
||||
- server: remove unusable setting to send error reports
|
||||
- server: ignore detail parameter on meta endpoint
|
||||
- server: Promotion entities and endpoints
|
||||
- server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
|
||||
Foundkey will now work as if it was set to `true`.
|
||||
|
||||
## 13.0.0-preview1 - 2022-08-05
|
||||
### Added
|
||||
|
|
|
@ -139,6 +139,14 @@ 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/)
|
||||
|
@ -289,8 +297,11 @@ 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
|
||||
In `packages/backend`, run:
|
||||
First make changes to the entity files in `packages/backend/src/models/entities/`.
|
||||
|
||||
Then, in `packages/backend`, run:
|
||||
```sh
|
||||
yarn build
|
||||
npx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
```
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Reporting Security Issues
|
||||
|
||||
If you discover a security issue in Misskey, please report it by sending an
|
||||
email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
|
||||
If you discover a security issue in Foundkey, please report it by sending an
|
||||
email to [johann@qwertqwefsday.eu](mailto:johann@qwertqwefsday.eu).
|
||||
|
||||
This will allow us to assess the risk, and make a fix available before we add a
|
||||
bug report to the GitHub repository.
|
||||
bug report to the repository.
|
||||
|
||||
Thanks for helping make Misskey safe for everyone.
|
||||
Thanks for helping make Foundkey safe for everyone.
|
||||
|
|
|
@ -40,6 +40,9 @@ 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
|
||||
|
|
|
@ -190,7 +190,9 @@ 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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "foundkey",
|
||||
"version": "13.0.0-preview.1",
|
||||
"version": "13.0.0-preview2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export class removeAds1657570176749 {
|
||||
name = 'removeAds1657570176749'
|
||||
name = 'removeAds1657570176749';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE "ad"`);
|
44
packages/backend/migration/1667503570994-sync.js
Normal file
44
packages/backend/migration/1667503570994-sync.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
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") `),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "13.0.0-preview1",
|
||||
"version": "13.0.0-preview2",
|
||||
"main": "./index.js",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
@ -15,8 +15,8 @@
|
|||
"test": "npm run mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.2.2",
|
||||
"@bull-board/koa": "4.0.0",
|
||||
"@bull-board/api": "^4.3.1",
|
||||
"@bull-board/koa": "^4.3.1",
|
||||
"@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.30.7",
|
||||
"sharp": "0.31.2",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
|
|
|
@ -38,6 +38,8 @@ 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;
|
||||
|
|
|
@ -10,7 +10,7 @@ function getRedisFamily(family?: string | number): number {
|
|||
dual: 0,
|
||||
};
|
||||
if (typeof family === 'string' && family in familyMap) {
|
||||
return familyMap[family];
|
||||
return familyMap[family as keyof typeof familyMap];
|
||||
} else if (typeof family === 'number' && Object.values(familyMap).includes(family)) {
|
||||
return family;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export type Source = {
|
|||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
elasticsearch: {
|
||||
elasticsearch?: {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl?: boolean;
|
||||
|
@ -41,6 +41,8 @@ export type Source = {
|
|||
|
||||
maxFileSize?: number;
|
||||
|
||||
maxNoteTextLength?: number;
|
||||
|
||||
accesslog?: string;
|
||||
|
||||
clusterLimit?: number;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
||||
|
||||
// Time constants
|
||||
export const SECOND = 1000;
|
||||
export const MINUTE = 60 * SECOND;
|
||||
|
|
|
@ -62,22 +62,21 @@ 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) {
|
||||
//#region ホスト名部分が省略されているので復元する
|
||||
// restore the host name part
|
||||
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) {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
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: Cache<never>['lifetime']) {
|
||||
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
this.fetcher = fetcher;
|
||||
}
|
||||
|
||||
public set(key: string | null, value: T): void {
|
||||
|
@ -17,10 +19,13 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -29,52 +34,22 @@ export class Cache<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
public async fetch(key: string | null): Promise<T | undefined> {
|
||||
const cached = this.get(key);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
} else {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
}
|
||||
const value = await this.fetcher(key);
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
// don't cache undefined
|
||||
if (value !== undefined)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,22 +3,26 @@ 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'][]>(1000 * 60 * 5);
|
||||
const blockingCache = new Cache<User['id'][]>(
|
||||
5 * MINUTE,
|
||||
(blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)),
|
||||
);
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
// designation for users you follow, list users and groups is disabled for performance reasons
|
||||
|
||||
/**
|
||||
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
|
||||
* either noteUserFollowers or antennaUserFollowing must be specified
|
||||
*/
|
||||
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;
|
||||
|
||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
||||
// skip if the antenna creator is blocked by the note author
|
||||
const blockings = await blockingCache.fetch(noteUser.id);
|
||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
|
|
|
@ -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;
|
||||
|
||||
export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||
if (!noCache && cache) return cache;
|
||||
/**
|
||||
* Performs the primitive database operation to set the server configuration
|
||||
*/
|
||||
export async function setMeta(meta: Meta): Promise<void> {
|
||||
const unlock = await getFetchInstanceMetadataLock('localhost');
|
||||
|
||||
return await db.transaction(async transactionalEntityManager => {
|
||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
// 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',
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
unlock();
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
fetchMeta(true).then(meta => {
|
||||
cache = meta;
|
||||
});
|
||||
}, 1000 * 10);
|
||||
export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||
if (!noCache && cache) return cache;
|
||||
|
||||
await getMeta();
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
|
|
@ -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 = 512;
|
||||
export const DB_MAX_IMAGE_COMMENT_LENGTH = 2048;
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
export class I18n<T extends Record<string, any>> {
|
||||
public locale: T;
|
||||
const locales = await import('../../../../locales/index.js').then(mod => mod.default);
|
||||
|
||||
constructor(locale: T) {
|
||||
this.locale = locale;
|
||||
export class I18n {
|
||||
public ts: Record<string, any>;
|
||||
|
||||
//#region BIND
|
||||
constructor(locale: string) {
|
||||
this.ts = locales[locale];
|
||||
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.locale) as string;
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.ts) as string;
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
|
|
|
@ -3,8 +3,11 @@ 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);
|
||||
const cache = new Cache<UserKeypair>(
|
||||
Infinity,
|
||||
(userId) => UserKeypairs.findOneByOrFail({ userId }),
|
||||
);
|
||||
|
||||
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||
return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId }));
|
||||
return await cache.fetch(userId);
|
||||
}
|
||||
|
|
|
@ -4,14 +4,27 @@ 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';
|
||||
|
||||
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 添付用絵文字情報
|
||||
* Information needed to attach in ActivityPub
|
||||
*/
|
||||
type PopulatedEmoji = {
|
||||
name: string;
|
||||
|
@ -36,28 +49,22 @@ function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
|||
|
||||
const name = match[1];
|
||||
|
||||
// ホスト正規化
|
||||
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
|
||||
|
||||
return { name, host };
|
||||
}
|
||||
|
||||
/**
|
||||
* 添付用絵文字情報を解決する
|
||||
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
|
||||
* @param noteUserHost ノートやユーザープロフィールの所有者のホスト
|
||||
* @returns 絵文字情報, nullは未マッチを意味する
|
||||
* 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.
|
||||
*/
|
||||
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
|
||||
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
|
||||
if (name == null) return null;
|
||||
|
||||
const queryOrNull = async () => (await Emojis.findOneBy({
|
||||
name,
|
||||
host: host ?? IsNull(),
|
||||
})) || null;
|
||||
|
||||
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
const emoji = await cache.fetch(`${host ?? ''}:${name}`);
|
||||
|
||||
if (emoji == null) return null;
|
||||
|
||||
|
@ -72,7 +79,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)));
|
||||
|
@ -103,11 +110,20 @@ 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 => cache.get(`${emoji.name} ${emoji.host}`) == null);
|
||||
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 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({
|
||||
|
@ -115,11 +131,14 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
|
|||
host: host ?? IsNull(),
|
||||
});
|
||||
}
|
||||
const _emojis = emojisQuery.length > 0 ? await Emojis.find({
|
||||
|
||||
await Emojis.find({
|
||||
where: emojisQuery,
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
}) : [];
|
||||
for (const emoji of _emojis) {
|
||||
cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
}
|
||||
}).then(emojis => {
|
||||
// store all emojis into the cache
|
||||
emojis.forEach(emoji => {
|
||||
cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Note } from '@/models/entities/note.js';
|
||||
|
||||
export function isPureRenote(note: Note): boolean {
|
||||
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
|
||||
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
|
||||
}
|
||||
|
|
55
packages/backend/src/misc/skipped-instances.ts
Normal file
55
packages/backend/src/misc/skipped-instances.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
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;
|
||||
}
|
|
@ -62,7 +62,8 @@ export class DriveFile {
|
|||
public size: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
length: 2048,
|
||||
nullable: true,
|
||||
comment: 'The comment of the DriveFile.',
|
||||
})
|
||||
public comment: string | null;
|
||||
|
|
|
@ -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ステータスコード
|
||||
* HTTP status code that was received for the last outgoing HTTP request.
|
||||
*/
|
||||
@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', {
|
||||
|
|
|
@ -6,13 +6,16 @@ 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 } from '@/const.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } 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>(1000 * 60 * 60 * 3);
|
||||
const userInstanceCache = new Cache<Instance | null>(
|
||||
3 * HOUR,
|
||||
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
|
||||
);
|
||||
|
||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||
|
@ -27,7 +30,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: 500 } as const;
|
||||
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 2048 } 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;
|
||||
|
||||
|
@ -309,17 +312,15 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
isModerator: user.isModerator || falsy,
|
||||
isBot: user.isBot || falsy,
|
||||
isCat: user.isCat || falsy,
|
||||
instance: user.host ? userInstanceCache.fetch(user.host,
|
||||
() => Instances.findOneBy({ host: user.host! }),
|
||||
v => v != null,
|
||||
).then(instance => instance ? {
|
||||
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,
|
||||
} : undefined) : undefined,
|
||||
}),
|
||||
emojis: populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
|
||||
|
|
|
@ -6,41 +6,20 @@ 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';
|
||||
|
||||
const logger = new Logger('deliver');
|
||||
|
||||
let latest: string | null = null;
|
||||
|
||||
const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
|
||||
|
||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||
const { host } = new URL(job.data.to);
|
||||
const puny = toPuny(host);
|
||||
|
||||
// ブロックしてたら中断
|
||||
const meta = await fetchMeta();
|
||||
if (meta.blockedHosts.includes(toPuny(host))) {
|
||||
return 'skip (blocked)';
|
||||
}
|
||||
|
||||
// isSuspendedなら中断
|
||||
let suspendedHosts = suspendedHostsCache.get(null);
|
||||
if (suspendedHosts == null) {
|
||||
suspendedHosts = await Instances.find({
|
||||
where: {
|
||||
isSuspended: true,
|
||||
},
|
||||
});
|
||||
suspendedHostsCache.set(null, suspendedHosts);
|
||||
}
|
||||
if (suspendedHosts.map(x => x.host).includes(toPuny(host))) {
|
||||
return 'skip (suspended)';
|
||||
}
|
||||
if (await shouldSkipInstance(puny)) return 'skip';
|
||||
|
||||
try {
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||
|
@ -83,8 +62,8 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
|||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||
// A client error means that something is wrong with the request we are making,
|
||||
// which means that retrying it makes no sense.
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Bull from 'bull';
|
||||
import { In, LessThan } from 'typeorm';
|
||||
import { AttestationChallenges, Mutings, Signins } from '@/models/index.js';
|
||||
import { AttestationChallenges, Mutings, PasswordResetRequests, Signins } from '@/models/index.js';
|
||||
import { publishUserEvent } from '@/services/stream.js';
|
||||
import { MINUTE, DAY } from '@/const.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
|
@ -35,6 +35,11 @@ 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();
|
||||
|
|
|
@ -10,8 +10,14 @@ 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 | null>(Infinity);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
||||
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),
|
||||
);
|
||||
|
||||
export type UriParseResult = {
|
||||
/** wether the URI was generated by us */
|
||||
|
@ -99,13 +105,9 @@ export default class DbResolver {
|
|||
if (parsed.local) {
|
||||
if (parsed.type !== 'users') return null;
|
||||
|
||||
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
|
||||
id: parsed.id,
|
||||
}).then(x => x ?? undefined)) ?? null;
|
||||
return await userByIdCache.fetch(parsed.id) ?? null;
|
||||
} else {
|
||||
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
|
||||
uri: parsed.uri,
|
||||
}));
|
||||
return await uriPersonCache.fetch(parsed.uri) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,20 +118,12 @@ export default class DbResolver {
|
|||
user: CacheableRemoteUser;
|
||||
key: UserPublickey;
|
||||
} | null> {
|
||||
const key = await publicKeyCache.fetch(keyId, async () => {
|
||||
const key = await UserPublickeys.findOneBy({
|
||||
keyId,
|
||||
});
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
return key;
|
||||
}, key => key != null);
|
||||
const key = await publicKeyCache.fetch(keyId);
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
return {
|
||||
user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser,
|
||||
user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
@ -145,7 +139,7 @@ export default class DbResolver {
|
|||
|
||||
if (user == null) return null;
|
||||
|
||||
const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null);
|
||||
const key = await publicKeyByUserIdCache.fetch(user.id);
|
||||
|
||||
return {
|
||||
user,
|
||||
|
|
|
@ -2,12 +2,17 @@ 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 {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface IEveryoneRecipe extends IRecipe {
|
||||
type: 'Everyone';
|
||||
}
|
||||
|
||||
interface IFollowersRecipe extends IRecipe {
|
||||
type: 'Followers';
|
||||
}
|
||||
|
@ -17,6 +22,9 @@ interface IDirectRecipe extends IRecipe {
|
|||
to: IRemoteUser;
|
||||
}
|
||||
|
||||
const isEveryone = (recipe: any): recipe is IEveryoneRecipe =>
|
||||
recipe.type === 'Everyone';
|
||||
|
||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
||||
recipe.type === 'Followers';
|
||||
|
||||
|
@ -63,6 +71,13 @@ export default class DeliverManager {
|
|||
this.addRecipe(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe to send this activity to all known sharedInboxes
|
||||
*/
|
||||
public addEveryone() {
|
||||
this.addRecipe({ type: 'Everyone' } as IEveryoneRecipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe
|
||||
* @param recipe Recipe
|
||||
|
@ -82,31 +97,40 @@ export default class DeliverManager {
|
|||
/*
|
||||
build inbox list
|
||||
|
||||
Process follower recipes first to avoid duplication when processing
|
||||
direct recipes later.
|
||||
Processing order matters to avoid duplication.
|
||||
*/
|
||||
|
||||
if (this.recipes.some(r => isEveryone(r))) {
|
||||
// deliver to all of known network
|
||||
const sharedInboxes = await Users.createQueryBuilder('users')
|
||||
.select('users.sharedInbox', 'sharedInbox')
|
||||
// so we don't have to make our inboxes Set work as hard
|
||||
.distinct(true)
|
||||
// can't deliver to unknown shared inbox
|
||||
.where('users.sharedInbox IS NOT NULL')
|
||||
// don't deliver to ourselves
|
||||
.andWhere('users.host IS NOT NULL')
|
||||
.getRawMany();
|
||||
|
||||
for (const inbox of sharedInboxes) {
|
||||
inboxes.add(inbox.sharedInbox);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.recipes.some(r => isFollowers(r))) {
|
||||
// followers deliver
|
||||
// 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;
|
||||
}[];
|
||||
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();
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox || following.followerInbox;
|
||||
inboxes.add(inbox);
|
||||
}
|
||||
followers.forEach(({ inbox }) => inboxes.add(inbox));
|
||||
}
|
||||
|
||||
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
||||
|
@ -119,8 +143,19 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import create from '@/services/note/reaction/create.js';
|
||||
import { createReaction } 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 create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
|
||||
return await createReaction(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
|
||||
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
|
||||
return 'skip: already reacted';
|
||||
} else {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'] }) {
|
||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const req = createSignedGet({
|
||||
|
|
|
@ -23,8 +23,6 @@ import Featured from './activitypub/featured.js';
|
|||
// Init router
|
||||
const router = new Router();
|
||||
|
||||
//#region Routing
|
||||
|
||||
function inbox(ctx: Router.RouterContext) {
|
||||
let signature;
|
||||
|
||||
|
@ -45,6 +43,8 @@ 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,6 +94,15 @@ 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(),
|
||||
|
@ -185,7 +194,6 @@ router.get('/@:user', async (ctx, next) => {
|
|||
|
||||
await userInfo(ctx, user);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// emoji
|
||||
router.get('/emojis/:emoji', async ctx => {
|
||||
|
|
|
@ -5,59 +5,51 @@ import authenticate, { AuthenticationError } from './authenticate.js';
|
|||
import call from './call.js';
|
||||
import { ApiError } from './error.js';
|
||||
|
||||
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
|
||||
export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<void> {
|
||||
const body = ctx.is('multipart/form-data')
|
||||
? (ctx.request as any).body
|
||||
: ctx.method === 'GET'
|
||||
? ctx.query
|
||||
: ctx.request.body;
|
||||
|
||||
const reply = (x?: any, y?: ApiError) => {
|
||||
if (x == null) {
|
||||
ctx.status = 204;
|
||||
} else if (typeof x === 'number' && y) {
|
||||
ctx.status = x;
|
||||
const error = (e: ApiError): void => {
|
||||
ctx.status = e.httpStatusCode;
|
||||
if (e.httpStatusCode === 401) {
|
||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
||||
}
|
||||
ctx.body = {
|
||||
error: {
|
||||
message: y!.message,
|
||||
code: y!.code,
|
||||
id: y!.id,
|
||||
kind: y!.kind,
|
||||
...(y!.info ? { info: y!.info } : {}),
|
||||
message: e!.message,
|
||||
code: e!.code,
|
||||
...(e!.info ? { info: e!.info } : {}),
|
||||
endpoint: endpoint.name,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
||||
ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
|
||||
}
|
||||
res();
|
||||
};
|
||||
|
||||
// Authentication
|
||||
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
||||
authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(([user, app]) => {
|
||||
await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => {
|
||||
// API invoking
|
||||
call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
||||
await 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}`);
|
||||
}
|
||||
reply(res);
|
||||
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;
|
||||
}
|
||||
}).catch((e: ApiError) => {
|
||||
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
||||
error(e);
|
||||
});
|
||||
}).catch(e => {
|
||||
if (e instanceof AuthenticationError) {
|
||||
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();
|
||||
error(new ApiError('AUTHENTICATION_FAILED', e.message));
|
||||
} else {
|
||||
reply(500, new ApiError());
|
||||
error(new ApiError());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,10 +3,13 @@ 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 { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
|
||||
import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
|
||||
import isNativeToken from './common/is-native-token.js';
|
||||
|
||||
const appCache = new Cache<App>(Infinity);
|
||||
const appCache = new Cache<App>(
|
||||
Infinity,
|
||||
(id) => Apps.findOneByOrFail({ id }),
|
||||
);
|
||||
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(message: string) {
|
||||
|
@ -15,8 +18,8 @@ export class AuthenticationError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||
let token: string | null = null;
|
||||
export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||
let maybeToken: string | null = null;
|
||||
|
||||
// check if there is an authorization header set
|
||||
if (authorization != null) {
|
||||
|
@ -27,19 +30,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 ') {
|
||||
token = authorization.substring(7);
|
||||
maybeToken = authorization.substring(7);
|
||||
} else {
|
||||
throw new AuthenticationError('unsupported authentication scheme');
|
||||
}
|
||||
} else if (bodyToken != null) {
|
||||
token = bodyToken;
|
||||
maybeToken = bodyToken;
|
||||
} else {
|
||||
return [null, null];
|
||||
}
|
||||
const token: string = maybeToken;
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
const user = await localUserByNativeTokenCache.fetch(token,
|
||||
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>);
|
||||
const user = await localUserByNativeTokenCache.fetch(token);
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthenticationError('unknown token');
|
||||
|
@ -63,14 +66,13 @@ export default async (authorization: string | null | undefined, bodyToken: strin
|
|||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
const user = await localUserByIdCache.fetch(accessToken.userId,
|
||||
() => Users.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<ILocalUser>);
|
||||
const user = await userByIdCache.fetch(accessToken.userId);
|
||||
|
||||
// can't authorize remote users
|
||||
if (!Users.isLocalUser(user)) return [null, null];
|
||||
|
||||
if (accessToken.appId) {
|
||||
const app = await appCache.fetch(accessToken.appId,
|
||||
() => Apps.findOneByOrFail({ id: accessToken.appId! }));
|
||||
const app = await appCache.fetch(accessToken.appId);
|
||||
|
||||
return [user, {
|
||||
id: accessToken.id,
|
||||
|
|
|
@ -8,29 +8,16 @@ 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({
|
||||
message: 'No such endpoint.',
|
||||
code: 'NO_SUCH_ENDPOINT',
|
||||
id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709',
|
||||
httpStatusCode: 404,
|
||||
});
|
||||
}
|
||||
if (ep == null) throw new ApiError('NO_SUCH_ENDPOINT');
|
||||
|
||||
if (ep.meta.secure && !isSecure) {
|
||||
throw new ApiError(accessDenied);
|
||||
throw new ApiError('ACCESS_DENIED', 'This operation can only be performed with a native token.');
|
||||
}
|
||||
|
||||
if (ep.meta.limit && !isModerator) {
|
||||
|
@ -49,48 +36,29 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
}
|
||||
|
||||
// Rate limit
|
||||
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,
|
||||
});
|
||||
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(() => {
|
||||
throw new ApiError('RATE_LIMIT_EXCEEDED');
|
||||
});
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && user == null) {
|
||||
throw new ApiError({
|
||||
message: 'Credential required.',
|
||||
code: 'CREDENTIAL_REQUIRED',
|
||||
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||
httpStatusCode: 401,
|
||||
});
|
||||
throw new ApiError('AUTHENTICATION_REQUIRED');
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && user!.isSuspended) {
|
||||
throw new ApiError({
|
||||
message: 'Your account has been suspended.',
|
||||
code: 'YOUR_ACCOUNT_SUSPENDED',
|
||||
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
|
||||
httpStatusCode: 403,
|
||||
});
|
||||
throw new ApiError('SUSPENDED');
|
||||
}
|
||||
|
||||
if (ep.meta.requireAdmin && !user!.isAdmin) {
|
||||
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
|
||||
throw new ApiError('ACCESS_DENIED', 'This operation requires administrator privileges.');
|
||||
}
|
||||
|
||||
if (ep.meta.requireModerator && !isModerator) {
|
||||
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
|
||||
throw new ApiError('ACCESS_DENIED', 'This operation requires moderator privileges.');
|
||||
}
|
||||
|
||||
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
|
||||
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',
|
||||
});
|
||||
throw new ApiError('ACCESS_DENIED', 'This operation requires privileges which this token does not grant.');
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
|
@ -101,11 +69,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
try {
|
||||
data[k] = JSON.parse(data[k]);
|
||||
} catch (e) {
|
||||
throw new ApiError({
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
||||
}, {
|
||||
throw new ApiError('INVALID_PARAM', {
|
||||
param: k,
|
||||
reason: `cannot cast to ${param.type}`,
|
||||
});
|
||||
|
@ -129,7 +93,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
stack: e.stack,
|
||||
},
|
||||
});
|
||||
throw new ApiError(null, {
|
||||
throw new ApiError('INTERNAL_ERROR', {
|
||||
e: {
|
||||
message: e.message,
|
||||
code: e.name,
|
||||
|
|
|
@ -24,25 +24,13 @@ export async function signup(opts: {
|
|||
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername(username)) {
|
||||
throw new ApiError({
|
||||
message: 'This username is invalid.',
|
||||
code: 'INVALID_USERNAME',
|
||||
id: 'ece89f3c-d845-4d9a-850b-1735285e8cd4',
|
||||
kind: 'client',
|
||||
httpStatusCode: 400,
|
||||
});
|
||||
throw new ApiError('INVALID_USERNAME');
|
||||
}
|
||||
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!Users.validatePassword(password)) {
|
||||
throw new ApiError({
|
||||
message: 'This password is invalid.',
|
||||
code: 'INVALID_PASSWORD',
|
||||
id: 'a941905b-fe7b-43e2-8ecd-50ad3a2287ab',
|
||||
kind: 'client',
|
||||
httpStatusCode: 400,
|
||||
});
|
||||
throw new ApiError('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
// Generate hash of password
|
||||
|
@ -53,22 +41,14 @@ 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(duplicateUsernameError);
|
||||
throw new ApiError('USED_USERNAME');
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) {
|
||||
throw new ApiError(duplicateUsernameError);
|
||||
throw new ApiError('USED_USERNAME');
|
||||
}
|
||||
|
||||
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||
|
@ -97,7 +77,7 @@ export async function signup(opts: {
|
|||
host: IsNull(),
|
||||
});
|
||||
|
||||
if (exist) throw new ApiError(duplicateUsernameError);
|
||||
if (exist) throw new ApiError('USED_USERNAME');
|
||||
|
||||
account = await transactionalEntityManager.save(new User({
|
||||
id: genId(),
|
||||
|
|
|
@ -28,22 +28,16 @@ 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({
|
||||
message: 'File required.',
|
||||
code: 'FILE_REQUIRED',
|
||||
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
||||
}));
|
||||
if (meta.requireFile && file == null) {
|
||||
return Promise.reject(new ApiError('FILE_REQUIRED'));
|
||||
}
|
||||
|
||||
const valid = validate(params);
|
||||
if (!valid) {
|
||||
if (file) cleanup();
|
||||
|
||||
const errors = validate.errors!;
|
||||
const err = new ApiError({
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||
}, {
|
||||
const err = new ApiError('INVALID_PARAM', {
|
||||
param: errors[0].schemaPath,
|
||||
reason: errors[0].message,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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';
|
||||
|
@ -270,14 +271,12 @@ 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';
|
||||
|
@ -580,14 +579,12 @@ 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],
|
||||
|
@ -625,13 +622,7 @@ export interface IEndpointMeta {
|
|||
|
||||
readonly tags?: ReadonlyArray<string>;
|
||||
|
||||
readonly errors?: {
|
||||
readonly [key: string]: {
|
||||
readonly message: string;
|
||||
readonly code: string;
|
||||
readonly id: string;
|
||||
};
|
||||
};
|
||||
readonly errors?: ReadonlyArray<keyof typeof errors>;
|
||||
|
||||
readonly res?: Schema;
|
||||
|
||||
|
|
|
@ -8,13 +8,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchAnnouncement: {
|
||||
message: 'No such announcement.',
|
||||
code: 'NO_SUCH_ANNOUNCEMENT',
|
||||
id: 'ecad8040-a276-4e85-bda9-015a708d291e',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_ANNOUNCEMENT'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -29,7 +23,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(meta.errors.noSuchAnnouncement);
|
||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
||||
|
||||
await Announcements.delete(announcement.id);
|
||||
});
|
||||
|
|
|
@ -8,13 +8,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchAnnouncement: {
|
||||
message: 'No such announcement.',
|
||||
code: 'NO_SUCH_ANNOUNCEMENT',
|
||||
id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_ANNOUNCEMENT'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -32,7 +26,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(meta.errors.noSuchAnnouncement);
|
||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
||||
|
||||
await Announcements.update(announcement.id, {
|
||||
updatedAt: new Date(),
|
||||
|
|
|
@ -8,13 +8,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FILE'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -180,9 +174,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}],
|
||||
});
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
||||
return file;
|
||||
});
|
||||
|
|
|
@ -13,13 +13,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'MO_SUCH_FILE',
|
||||
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FILE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -34,7 +28,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(meta.errors.noSuchFile);
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
||||
const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||
|
||||
|
|
|
@ -13,13 +13,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: 'No such emoji.',
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: 'e2785b66-dca3-4087-9cac-b93c541cc425',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_EMOJI', 'INTERNAL_ERROR'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -46,9 +40,7 @@ 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(meta.errors.noSuchEmoji);
|
||||
}
|
||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
||||
|
||||
let driveFile: DriveFile;
|
||||
|
||||
|
@ -56,7 +48,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();
|
||||
throw new ApiError('INTERNAL_ERROR', e);
|
||||
}
|
||||
|
||||
const copied = await Emojis.insert({
|
||||
|
|
|
@ -10,13 +10,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: 'No such emoji.',
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_EMOJI'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -31,7 +25,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(meta.errors.noSuchEmoji);
|
||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
||||
|
||||
await Emojis.delete(emoji.id);
|
||||
|
||||
|
|
|
@ -9,13 +9,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: 'No such emoji.',
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_EMOJI'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -39,7 +33,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(meta.errors.noSuchEmoji);
|
||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
||||
|
||||
await Emojis.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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 = {
|
||||
|
@ -310,7 +309,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
iconUrl: instance.iconUrl,
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
|
||||
maxNoteTextLength: config.maxNoteTextLength,
|
||||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
|
|
@ -9,13 +9,7 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
invalidUrl: {
|
||||
message: 'Invalid URL',
|
||||
code: 'INVALID_URL',
|
||||
id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c',
|
||||
},
|
||||
},
|
||||
errors: ['INVALID_URL'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -58,8 +52,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 {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
} catch (e) {
|
||||
throw new ApiError('INVALID_URL', e);
|
||||
}
|
||||
|
||||
return await addRelay(ps.inbox);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Meta } from '@/models/entities/meta.js';
|
||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { fetchMeta, setMeta } from '@/misc/fetch-meta.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -375,20 +374,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.deeplIsPro = ps.deeplIsPro;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
const meta = await fetchMeta();
|
||||
await setMeta({
|
||||
...meta,
|
||||
...set,
|
||||
});
|
||||
|
||||
insertModerationLog(me, 'updateMeta');
|
||||
|
|
|
@ -11,19 +11,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -71,18 +59,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchUserList);
|
||||
}
|
||||
if (userList == null) throw new ApiError('NO_SUCH_USER_LIST');
|
||||
} else if (ps.src === 'group' && ps.userGroupId) {
|
||||
userGroupJoining = await UserGroupJoinings.findOneBy({
|
||||
userGroupId: ps.userGroupId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (userGroupJoining == null) {
|
||||
throw new ApiError(meta.errors.noSuchUserGroup);
|
||||
}
|
||||
if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP');
|
||||
}
|
||||
|
||||
const antenna = await Antennas.insert({
|
||||
|
|
|
@ -10,13 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchAntenna: {
|
||||
message: 'No such antenna.',
|
||||
code: 'NO_SUCH_ANTENNA',
|
||||
id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_ANTENNA'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -34,9 +28,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (antenna == null) {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
||||
|
||||
await Antennas.delete(antenna.id);
|
||||
|
||||
|
|
|
@ -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,13 +14,7 @@ export const meta = {
|
|||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchAntenna: {
|
||||
message: 'No such antenna.',
|
||||
code: 'NO_SUCH_ANTENNA',
|
||||
id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_ANTENNA'],
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
@ -53,9 +47,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (antenna == null) {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
|
|
|
@ -9,13 +9,7 @@ export const meta = {
|
|||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchAntenna: {
|
||||
message: 'No such antenna.',
|
||||
code: 'NO_SUCH_ANTENNA',
|
||||
id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_ANTENNA'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -40,9 +34,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
userId: me.id,
|
||||
});
|
||||
|
||||
if (antenna == null) {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
||||
|
||||
return await Antennas.pack(antenna);
|
||||
});
|
||||
|
|
|
@ -10,25 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_ANTENNA', 'NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -74,9 +56,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (antenna == null) {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
||||
|
||||
let userList;
|
||||
let userGroupJoining;
|
||||
|
@ -87,18 +67,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchUserList);
|
||||
}
|
||||
if (userList == null) throw new ApiError('NO_SUCH_USER_LIST');
|
||||
} else if (ps.src === 'group' && ps.userGroupId) {
|
||||
userGroupJoining = await UserGroupJoinings.findOneBy({
|
||||
userGroupId: ps.userGroupId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (userGroupJoining == null) {
|
||||
throw new ApiError(meta.errors.noSuchUserGroup);
|
||||
}
|
||||
if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP');
|
||||
}
|
||||
|
||||
await Antennas.update(antenna.id, {
|
||||
|
|
|
@ -12,9 +12,6 @@ export const meta = {
|
|||
max: 30,
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -24,13 +24,7 @@ export const meta = {
|
|||
max: 30,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchObject: {
|
||||
message: 'No such object.',
|
||||
code: 'NO_SUCH_OBJECT',
|
||||
id: 'dc94d745-1262-4e63-a17d-fecaa57efc82',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_OBJECT'],
|
||||
|
||||
res: {
|
||||
optional: false, nullable: false,
|
||||
|
@ -83,7 +77,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
if (object) {
|
||||
return object;
|
||||
} else {
|
||||
throw new ApiError(meta.errors.noSuchObject);
|
||||
throw new ApiError('NO_SUCH_OBJECT');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 = {
|
||||
|
@ -21,10 +22,14 @@ export const paramDef = {
|
|||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
permission: { type: 'array', uniqueItems: true, items: {
|
||||
permission: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
// FIXME: add enum of possible permissions
|
||||
} },
|
||||
enum: kinds,
|
||||
},
|
||||
},
|
||||
callbackUrl: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['name', 'description', 'permission'],
|
||||
|
|
|
@ -5,13 +5,7 @@ import { ApiError } from '../../error.js';
|
|||
export const meta = {
|
||||
tags: ['app'],
|
||||
|
||||
errors: {
|
||||
noSuchApp: {
|
||||
message: 'No such app.',
|
||||
code: 'NO_SUCH_APP',
|
||||
id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_APP'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -33,14 +27,12 @@ export default define(meta, paramDef, async (ps, user, token) => {
|
|||
const isSecure = user != null && token == null;
|
||||
|
||||
// Lookup app
|
||||
const ap = await Apps.findOneBy({ id: ps.appId });
|
||||
const app = await Apps.findOneBy({ id: ps.appId });
|
||||
|
||||
if (ap == null) {
|
||||
throw new ApiError(meta.errors.noSuchApp);
|
||||
}
|
||||
if (app == null) throw new ApiError('NO_SUCH_APP');
|
||||
|
||||
return await Apps.pack(ap, user, {
|
||||
return await Apps.pack(app, user, {
|
||||
detail: true,
|
||||
includeSecret: isSecure && (ap.userId === user!.id),
|
||||
includeSecret: isSecure && (app.userId === user!.id),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,13 +12,7 @@ export const meta = {
|
|||
|
||||
secure: true,
|
||||
|
||||
errors: {
|
||||
noSuchSession: {
|
||||
message: 'No such session.',
|
||||
code: 'NO_SUCH_SESSION',
|
||||
id: '9c72d8de-391a-43c1-9d06-08d29efde8df',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_SESSION'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -35,9 +29,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const session = await AuthSessions
|
||||
.findOneBy({ token: ps.token });
|
||||
|
||||
if (session == null) {
|
||||
throw new ApiError(meta.errors.noSuchSession);
|
||||
}
|
||||
if (session == null) throw new ApiError('NO_SUCH_SESSION');
|
||||
|
||||
// Generate access token
|
||||
const accessToken = secureRndstr(32, true);
|
||||
|
|
|
@ -26,13 +26,7 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchApp: {
|
||||
message: 'No such app.',
|
||||
code: 'NO_SUCH_APP',
|
||||
id: '92f93e63-428e-4f2f-a5a4-39e1407fe998',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_APP'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -51,7 +45,7 @@ export default define(meta, paramDef, async (ps) => {
|
|||
});
|
||||
|
||||
if (app == null) {
|
||||
throw new ApiError(meta.errors.noSuchApp);
|
||||
throw new ApiError('NO_SUCH_APP');
|
||||
}
|
||||
|
||||
// Generate token
|
||||
|
|
|
@ -7,13 +7,7 @@ export const meta = {
|
|||
|
||||
requireCredential: false,
|
||||
|
||||
errors: {
|
||||
noSuchSession: {
|
||||
message: 'No such session.',
|
||||
code: 'NO_SUCH_SESSION',
|
||||
id: 'bd72c97d-eba7-4adb-a467-f171b8847250',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_SESSION'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -52,9 +46,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
token: ps.token,
|
||||
});
|
||||
|
||||
if (session == null) {
|
||||
throw new ApiError(meta.errors.noSuchSession);
|
||||
}
|
||||
if (session == null) throw new ApiError('NO_SUCH_SESSION');
|
||||
|
||||
return await AuthSessions.pack(session, user);
|
||||
});
|
||||
|
|
|
@ -24,25 +24,7 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_APP', 'NO_SUCH_SESSION', 'PENDING_SESSION'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -61,9 +43,7 @@ export default define(meta, paramDef, async (ps) => {
|
|||
secret: ps.appSecret,
|
||||
});
|
||||
|
||||
if (app == null) {
|
||||
throw new ApiError(meta.errors.noSuchApp);
|
||||
}
|
||||
if (app == null) throw new ApiError('NO_SUCH_APP');
|
||||
|
||||
// Fetch token
|
||||
const session = await AuthSessions.findOneBy({
|
||||
|
@ -71,13 +51,9 @@ export default define(meta, paramDef, async (ps) => {
|
|||
appId: app.id,
|
||||
});
|
||||
|
||||
if (session == null) {
|
||||
throw new ApiError(meta.errors.noSuchSession);
|
||||
}
|
||||
if (session == null) throw new ApiError('NO_SUCH_SESSION');
|
||||
|
||||
if (session.userId == null) {
|
||||
throw new ApiError(meta.errors.pendingSession);
|
||||
}
|
||||
if (session.userId == null) throw new ApiError('PENDING_SESSION');
|
||||
|
||||
// Lookup access token
|
||||
const accessToken = await AccessTokens.findOneByOrFail({
|
||||
|
|
|
@ -17,25 +17,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:blocks',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'ALREADY_BLOCKING'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -57,13 +39,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const blocker = await Users.findOneByOrFail({ id: user.id });
|
||||
|
||||
// 自分自身
|
||||
if (user.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||
}
|
||||
if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF');
|
||||
|
||||
// Get blockee
|
||||
const blockee = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
blockeeId: blockee.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyBlocking);
|
||||
}
|
||||
if (exist != null) throw new ApiError('ALREADY_BLOCKING');
|
||||
|
||||
await create(blocker, blockee);
|
||||
|
||||
|
|
|
@ -17,25 +17,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:blocks',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'NOT_BLOCKING'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -54,16 +36,14 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
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);
|
||||
}
|
||||
if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF');
|
||||
|
||||
const blocker = await Users.findOneByOrFail({ id: user.id });
|
||||
|
||||
// Get blockee
|
||||
const blockee = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
blockeeId: blockee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notBlocking);
|
||||
}
|
||||
if (exist == null) throw new ApiError('NOT_BLOCKING');
|
||||
|
||||
// Delete blocking
|
||||
await deleteBlocking(blocker, blockee);
|
||||
|
|
|
@ -17,13 +17,7 @@ export const meta = {
|
|||
ref: 'Channel',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FILE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -45,9 +39,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (banner == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (banner == null) throw new ApiError('NO_SUCH_FILE');
|
||||
}
|
||||
|
||||
const channel = await Channels.insert({
|
||||
|
|
|
@ -11,13 +11,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: 'c0031718-d573-4e85-928e-10039f1fbb68',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CHANNEL'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -34,9 +28,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
||||
|
||||
await ChannelFollowings.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -13,13 +13,7 @@ export const meta = {
|
|||
ref: 'Channel',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '6f6c314b-7486-4897-8966-c04a66a02923',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CHANNEL'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -36,9 +30,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
||||
|
||||
return await Channels.pack(channel, me);
|
||||
});
|
||||
|
|
|
@ -19,13 +19,7 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CHANNEL'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -47,9 +41,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
||||
|
||||
//#region Construct query
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
|
|
|
@ -10,13 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
noSuchChannel: {
|
||||
message: 'No such channel.',
|
||||
code: 'NO_SUCH_CHANNEL',
|
||||
id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CHANNEL'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
||||
|
||||
await ChannelFollowings.delete({
|
||||
followerId: user.id,
|
||||
|
|
|
@ -15,25 +15,7 @@ export const meta = {
|
|||
ref: 'Channel',
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['ACCESS_DENIED', 'NO_SUCH_CHANNEL', 'NO_SUCH_FILE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -53,13 +35,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
id: ps.channelId,
|
||||
});
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
||||
|
||||
if (channel.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
if (channel.userId !== me.id) throw new ApiError('ACCESS_DENIED', 'You are not the owner of this channel.');
|
||||
|
||||
// eslint:disable-next-line:no-unnecessary-initializer
|
||||
let banner = undefined;
|
||||
|
@ -69,9 +47,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
userId: me.id,
|
||||
});
|
||||
|
||||
if (banner == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (banner == null) throw new ApiError('NO_SUCH_FILE');
|
||||
} else if (ps.bannerId === null) {
|
||||
banner = null;
|
||||
}
|
||||
|
|
|
@ -11,25 +11,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['ALREADY_CLIPPED', 'NO_SUCH_CLIP', 'NO_SUCH_NOTE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -48,12 +30,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
||||
|
||||
const note = await getNote(ps.noteId, user).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE');
|
||||
throw err;
|
||||
});
|
||||
|
||||
|
@ -62,9 +42,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
clipId: clip.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyClipped);
|
||||
}
|
||||
if (exist != null) throw new ApiError('ALREADY_CLIPPED');
|
||||
|
||||
await ClipNotes.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -9,13 +9,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CLIP'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -33,9 +27,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
||||
|
||||
await Clips.delete(clip.id);
|
||||
});
|
||||
|
|
|
@ -13,13 +13,7 @@ export const meta = {
|
|||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CLIP'],
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
@ -49,12 +43,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
id: ps.clipId,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
||||
|
||||
if (!clip.isPublic && (user == null || (clip.userId !== user.id))) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
throw new ApiError('NO_SUCH_CLIP');
|
||||
}
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
|
|
|
@ -10,19 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CLIP', 'NO_SUCH_NOTE', 'NOT_CLIPPED'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -41,17 +29,17 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
||||
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE');
|
||||
throw e;
|
||||
});
|
||||
|
||||
await ClipNotes.delete({
|
||||
const { affected } = await ClipNotes.delete({
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
});
|
||||
|
||||
if (affected === 0) throw new ApiError('NOT_CLIPPED');
|
||||
});
|
||||
|
|
|
@ -9,13 +9,7 @@ export const meta = {
|
|||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CLIP'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -39,12 +33,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
id: ps.clipId,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
||||
|
||||
if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
throw new ApiError('NO_SUCH_CLIP');
|
||||
}
|
||||
|
||||
return await Clips.pack(clip);
|
||||
|
|
|
@ -9,13 +9,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchClip: {
|
||||
message: 'No such clip.',
|
||||
code: 'NO_SUCH_CLIP',
|
||||
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_CLIP'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -43,9 +37,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (clip == null) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
||||
|
||||
await Clips.update(clip.id, {
|
||||
name: ps.name,
|
||||
|
|
|
@ -21,13 +21,7 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: 'c118ece3-2e4b-4296-99d1-51756e32d232',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FILE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -46,9 +40,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
||||
const notes = await Notes.createQueryBuilder('note')
|
||||
.where(':file = ANY(note.fileIds)', { file: file.id })
|
||||
|
|
|
@ -28,13 +28,7 @@ export const meta = {
|
|||
ref: 'DriveFile',
|
||||
},
|
||||
|
||||
errors: {
|
||||
invalidFileName: {
|
||||
message: 'Invalid file name.',
|
||||
code: 'INVALID_FILE_NAME',
|
||||
id: 'f449b209-0c60-4e51-84d5-29486263bfd4',
|
||||
},
|
||||
},
|
||||
errors: ['INTERNAL_ERROR', 'INVALID_FILE_NAME'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -60,7 +54,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(meta.errors.invalidFileName);
|
||||
throw new ApiError('INVALID_FILE_NAME');
|
||||
}
|
||||
} else {
|
||||
name = null;
|
||||
|
@ -74,7 +68,7 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
|
|||
if (e instanceof Error || typeof e === 'string') {
|
||||
apiLogger.error(e);
|
||||
}
|
||||
throw new ApiError();
|
||||
throw new ApiError('INTERNAL_ERROR');
|
||||
} finally {
|
||||
cleanup!();
|
||||
}
|
||||
|
|
|
@ -13,19 +13,7 @@ export const meta = {
|
|||
|
||||
description: 'Delete an existing drive 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',
|
||||
},
|
||||
},
|
||||
errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -40,12 +28,10 @@ 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(meta.errors.noSuchFile);
|
||||
}
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
||||
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
throw new ApiError('ACCESS_DENIED');
|
||||
}
|
||||
|
||||
// Delete
|
||||
|
|
|
@ -18,19 +18,7 @@ export const meta = {
|
|||
ref: 'DriveFile',
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -69,12 +57,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
||||
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
throw new ApiError('ACCESS_DENIED');
|
||||
}
|
||||
|
||||
return await DriveFiles.pack(file, {
|
||||
|
|
|
@ -12,31 +12,7 @@ export const meta = {
|
|||
|
||||
description: 'Update the properties of a drive file.',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['ACCESS_DENIED', 'INVALID_FILE_NAME', 'NO_SUCH_FILE', 'NO_SUCH_FOLDER'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -61,17 +37,15 @@ 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(meta.errors.noSuchFile);
|
||||
}
|
||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
||||
|
||||
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
throw new ApiError('ACCESS_DENIED');
|
||||
}
|
||||
|
||||
if (ps.name) file.name = ps.name;
|
||||
if (!DriveFiles.validateFileName(file.name)) {
|
||||
throw new ApiError(meta.errors.invalidFileName);
|
||||
throw new ApiError('INVALID_FILE_NAME');
|
||||
}
|
||||
|
||||
if (ps.comment !== undefined) file.comment = ps.comment;
|
||||
|
@ -87,9 +61,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (folder == null) {
|
||||
throw new ApiError(meta.errors.noSuchFolder);
|
||||
}
|
||||
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
|
||||
|
||||
file.folderId = folder.id;
|
||||
}
|
||||
|
|
|
@ -11,13 +11,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:drive',
|
||||
|
||||
errors: {
|
||||
noSuchFolder: {
|
||||
message: 'No such folder.',
|
||||
code: 'NO_SUCH_FOLDER',
|
||||
id: '53326628-a00d-40a6-a3cd-8975105c0f95',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FOLDER'],
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
|
@ -46,9 +40,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (parent == null) {
|
||||
throw new ApiError(meta.errors.noSuchFolder);
|
||||
}
|
||||
if (parent == null) throw new ApiError('NO_SUCH_FOLDER');
|
||||
}
|
||||
|
||||
// Create folder
|
||||
|
|
|
@ -10,19 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:drive',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['HAS_CHILD_FILES_OR_FOLDERS', 'NO_SUCH_FOLDER'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -41,9 +29,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (folder == null) {
|
||||
throw new ApiError(meta.errors.noSuchFolder);
|
||||
}
|
||||
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
|
||||
|
||||
const [childFoldersCount, childFilesCount] = await Promise.all([
|
||||
DriveFolders.countBy({ parentId: folder.id }),
|
||||
|
@ -51,7 +37,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
]);
|
||||
|
||||
if (childFoldersCount !== 0 || childFilesCount !== 0) {
|
||||
throw new ApiError(meta.errors.hasChildFilesOrFolders);
|
||||
throw new ApiError('HAS_CHILD_FILES_OR_FOLDERS');
|
||||
}
|
||||
|
||||
await DriveFolders.delete(folder.id);
|
||||
|
|
|
@ -15,13 +15,7 @@ export const meta = {
|
|||
ref: 'DriveFolder',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFolder: {
|
||||
message: 'No such folder.',
|
||||
code: 'NO_SUCH_FOLDER',
|
||||
id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FOLDER'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -40,9 +34,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (folder == null) {
|
||||
throw new ApiError(meta.errors.noSuchFolder);
|
||||
}
|
||||
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
|
||||
|
||||
return await DriveFolders.pack(folder, {
|
||||
detail: true,
|
||||
|
|
|
@ -10,25 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:drive',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_FOLDER', 'NO_SUCH_PARENT_FOLDER', 'RECURSIVE_FOLDER'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -55,15 +37,13 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (folder == null) {
|
||||
throw new ApiError(meta.errors.noSuchFolder);
|
||||
}
|
||||
if (folder == null) throw new ApiError('NO_SUCH_FOLDER');
|
||||
|
||||
if (ps.name) folder.name = ps.name;
|
||||
|
||||
if (ps.parentId !== undefined) {
|
||||
if (ps.parentId === folder.id) {
|
||||
throw new ApiError(meta.errors.recursiveNesting);
|
||||
throw new ApiError('RECURSIVE_FOLDER');
|
||||
} else if (ps.parentId === null) {
|
||||
folder.parentId = null;
|
||||
} else {
|
||||
|
@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
});
|
||||
|
||||
if (parent == null) {
|
||||
throw new ApiError(meta.errors.noSuchParentFolder);
|
||||
}
|
||||
if (parent == null) throw new ApiError('NO_SUCH_PARENT_FOLDER');
|
||||
|
||||
// Check if the circular reference will occur
|
||||
async function checkCircle(folderId: string): Promise<boolean> {
|
||||
|
@ -95,7 +73,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
if (parent.parentId !== null) {
|
||||
if (await checkCircle(parent.parentId)) {
|
||||
throw new ApiError(meta.errors.recursiveNesting);
|
||||
throw new ApiError('RECURSIVE_FOLDER');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,7 +5,8 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,7 +5,7 @@ import define from '../../define.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,7 +5,7 @@ import define from '../../define.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
|
||||
res: {
|
||||
oneOf: [{
|
||||
|
|
|
@ -6,7 +6,7 @@ import define from '../../define.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
|
||||
allowGet: true,
|
||||
cacheSec: 60 * 60,
|
||||
|
|
|
@ -5,7 +5,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -8,7 +8,7 @@ const rssParser = new Parser();
|
|||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
allowGet: true,
|
||||
cacheSec: 60 * 3,
|
||||
} as const;
|
||||
|
|
|
@ -18,37 +18,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:following',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['ALREADY_FOLLOWING', 'BLOCKING', 'BLOCKED', 'FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -70,13 +40,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const follower = user;
|
||||
|
||||
// 自分自身
|
||||
if (user.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.followeeIsYourself);
|
||||
}
|
||||
if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF');
|
||||
|
||||
// Get followee
|
||||
const followee = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
@ -86,16 +54,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyFollowing);
|
||||
}
|
||||
if (exist != null) throw new ApiError('ALREADY_FOLLOWING');
|
||||
|
||||
try {
|
||||
await create(follower, followee);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
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);
|
||||
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError('BLOCKING');
|
||||
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError('BLOCKED');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -17,25 +17,7 @@ export const meta = {
|
|||
|
||||
kind: 'write: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',
|
||||
},
|
||||
},
|
||||
errors: ['FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -57,13 +39,11 @@ 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(meta.errors.followeeIsYourself);
|
||||
}
|
||||
if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF');
|
||||
|
||||
// Get followee
|
||||
const followee = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notFollowing);
|
||||
}
|
||||
if (exist == null) throw new ApiError('NOT_FOLLOWING');
|
||||
|
||||
await deleteFollowing(follower, followee);
|
||||
|
||||
|
|
|
@ -17,25 +17,7 @@ export const meta = {
|
|||
|
||||
kind: 'write: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',
|
||||
},
|
||||
},
|
||||
errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
@ -57,13 +39,11 @@ 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(meta.errors.followerIsYourself);
|
||||
}
|
||||
if (user.id === ps.userId) throw new ApiError('FOLLOWER_IS_YOURSELF');
|
||||
|
||||
// Get follower
|
||||
const follower = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
@ -73,9 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notFollowing);
|
||||
}
|
||||
if (exist == null) throw new ApiError('NOT_FOLLOWING');
|
||||
|
||||
await deleteFollowing(follower, followee);
|
||||
|
||||
|
|
|
@ -10,18 +10,7 @@ export const meta = {
|
|||
|
||||
kind: 'write:following',
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
errors: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -36,12 +25,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(meta.errors.noSuchUser);
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
||||
throw e;
|
||||
});
|
||||
|
||||
await acceptFollowRequest(user, follower).catch(e => {
|
||||
if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest);
|
||||
if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError('NO_SUCH_FOLLOW_REQUEST');
|
||||
throw e;
|
||||
});
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue