forked from FoundKeyGang/FoundKey
Compare commits
5 commits
main
...
secure-mod
Author | SHA1 | Date | |
---|---|---|---|
aa76c974f3 | |||
61b7c8ca53 | |||
840227a901 | |||
9acd4bc855 | |||
8bd41f5c9e |
314 changed files with 4360 additions and 2383 deletions
|
@ -124,9 +124,6 @@ redis:
|
||||||
# Upload or download file size limits (bytes)
|
# Upload or download file size limits (bytes)
|
||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
# Max note text length (in characters)
|
|
||||||
#maxNoteTextLength: 3000
|
|
||||||
|
|
||||||
#allowedPrivateNetworks: [
|
#allowedPrivateNetworks: [
|
||||||
# '127.0.0.1/32'
|
# '127.0.0.1/32'
|
||||||
#]
|
#]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
.autogen
|
.autogen
|
||||||
|
.github
|
||||||
|
.travis
|
||||||
.vscode
|
.vscode
|
||||||
.config
|
.config
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
@ -10,3 +12,4 @@ elasticsearch/
|
||||||
node_modules/
|
node_modules/
|
||||||
redis/
|
redis/
|
||||||
files/
|
files/
|
||||||
|
misskey-assets/
|
||||||
|
|
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -11,85 +11,38 @@ 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.
|
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.
|
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
||||||
|
|
||||||
## 13.0.0-preview2 - 2022-10-16
|
## Unreleased
|
||||||
### 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
|
### Added
|
||||||
- allow to mute only renotes of a user
|
- Client: Show instance info in ticker
|
||||||
- allow to export only selected custom emoji
|
- Client: Readded group pages
|
||||||
- client: improve emoji picker search
|
- Client: add re-collapsing to quoted notes
|
||||||
- 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
|
### Changed
|
||||||
- foundkey-js: Sync possible endpoints from backend
|
- Client: Use consistent date formatting based on language setting
|
||||||
- foundkey-js: update LiteInstanceMetadata fields
|
- Client: Add threshold to reduce occurances of "future" timestamps
|
||||||
- meta: use parallel and incremental builds
|
- Pages have been considerably simplified, several of the very complex features have been removed.
|
||||||
- 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.
|
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.
|
**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.
|
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.
|
Or generally advise all users to simplify their pages to only text.
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- 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
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- remove misskey-assets submodule
|
- Okteto config and Helm chart
|
||||||
- server: remove room data from user
|
- Client: acrylic styling
|
||||||
- client: remove ai mode
|
- Client: Twitter embeds, the standard URL preview is used instead.
|
||||||
- client: remove "Disable AiScript on Pages" setting
|
- Promotion entities and endpoints
|
||||||
- client: acrylic styling
|
- Server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
|
||||||
- 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`.
|
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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
## 13.0.0-preview1 - 2022-08-05
|
## 13.0.0-preview1 - 2022-08-05
|
||||||
### Added
|
### Added
|
||||||
- Server: Replies can now be fetched recursively.
|
- Server: Replies can now be fetched recursively.
|
||||||
|
|
|
@ -139,14 +139,6 @@ To generate the changelog, we use a standard shortlog command: `git shortlog --f
|
||||||
The person performing the release process should build the next CHANGELOG section based on this output, not use it as-is.
|
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.
|
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
|
||||||
|
|
||||||
[![Translation status](http://translate.akkoma.dev/widgets/foundkey/-/svg-badge.svg)](http://translate.akkoma.dev/engage/foundkey/)
|
[![Translation status](http://translate.akkoma.dev/widgets/foundkey/-/svg-badge.svg)](http://translate.akkoma.dev/engage/foundkey/)
|
||||||
|
@ -297,11 +289,8 @@ PostgreSQL array indices **start at 1**.
|
||||||
When `IN` is performed on a column that may contain `NULL` values, use `OR` or similar to handle `NULL` values.
|
When `IN` is performed on a column that may contain `NULL` values, use `OR` or similar to handle `NULL` values.
|
||||||
|
|
||||||
### creating migrations
|
### creating migrations
|
||||||
First make changes to the entity files in `packages/backend/src/models/entities/`.
|
In `packages/backend`, run:
|
||||||
|
|
||||||
Then, in `packages/backend`, run:
|
|
||||||
```sh
|
```sh
|
||||||
yarn build
|
|
||||||
npx typeorm migration:generate -d ormconfig.js -o <migration name>
|
npx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Reporting Security Issues
|
# Reporting Security Issues
|
||||||
|
|
||||||
If you discover a security issue in Foundkey, please report it by sending an
|
If you discover a security issue in Misskey, please report it by sending an
|
||||||
email to [johann@qwertqwefsday.eu](mailto:johann@qwertqwefsday.eu).
|
email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
|
||||||
|
|
||||||
This will allow us to assess the risk, and make a fix available before we add a
|
This will allow us to assess the risk, and make a fix available before we add a
|
||||||
bug report to the repository.
|
bug report to the GitHub repository.
|
||||||
|
|
||||||
Thanks for helping make Foundkey safe for everyone.
|
Thanks for helping make Misskey safe for everyone.
|
||||||
|
|
|
@ -40,9 +40,6 @@ git merge tags/v13.0.0-preview2 --squash
|
||||||
# you are now on the "next" release
|
# 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
|
## 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.
|
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
|
```sh
|
||||||
|
|
|
@ -190,9 +190,7 @@ charts: "Charts"
|
||||||
perHour: "Per Hour"
|
perHour: "Per Hour"
|
||||||
perDay: "Per Day"
|
perDay: "Per Day"
|
||||||
stopActivityDelivery: "Stop sending activities"
|
stopActivityDelivery: "Stop sending activities"
|
||||||
stopActivityDeliveryDescription: "Local activities will not be sent to this instance. Receiving activities works as before."
|
|
||||||
blockThisInstance: "Block this instance"
|
blockThisInstance: "Block this instance"
|
||||||
blockThisInstanceDescription: "Local activites will not be sent to this instance. Activites from this instance will be discarded."
|
|
||||||
operations: "Operations"
|
operations: "Operations"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
@ -831,6 +829,13 @@ middle: "Medium"
|
||||||
low: "Low"
|
low: "Low"
|
||||||
emailNotConfiguredWarning: "Email address not set."
|
emailNotConfiguredWarning: "Email address not set."
|
||||||
ratio: "Ratio"
|
ratio: "Ratio"
|
||||||
|
secureMode: "Secure Mode (Authorized Fetch)"
|
||||||
|
instanceSecurity: "Instance Security"
|
||||||
|
secureModeInfo: "Requests from other instances must be signed, otherwise notes won't be returned."
|
||||||
|
privateMode: "Private Mode"
|
||||||
|
privateModeInfo: "When enabled, only authorized instances may fetch notes. Hides all notes from public."
|
||||||
|
allowedInstances: "Allowed Instances"
|
||||||
|
allowedInstancesDescription: "Set the hosts of the instances you want to allow, separated by line. Valid in private mode only."
|
||||||
previewNoteText: "Show preview"
|
previewNoteText: "Show preview"
|
||||||
customCss: "Custom CSS"
|
customCss: "Custom CSS"
|
||||||
customCssWarn: "This setting should only be used if you know what it does. Entering\
|
customCssWarn: "This setting should only be used if you know what it does. Entering\
|
||||||
|
|
|
@ -771,6 +771,13 @@ middle: "中"
|
||||||
low: "低"
|
low: "低"
|
||||||
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
||||||
ratio: "比率"
|
ratio: "比率"
|
||||||
|
secureMode: "セキュアモード (Authorized Fetch)"
|
||||||
|
instanceSecurity: "インスタンスのセキュリティー"
|
||||||
|
secureModeInfo: "他のインスタンスからリクエストするときに、証明を付けなければ返送しません。"
|
||||||
|
privateMode: "非公開モード"
|
||||||
|
privateModeInfo: "有効にして、許可されているインスタンスのみがリクエストできます。すべてのノートが公開に非表示にします。"
|
||||||
|
allowedInstances: "許可されたインスタンス"
|
||||||
|
allowedInstancesDescription: "許可したいインスタンスのホストを改行で区切って設定します。非公開モードだけで有効です。"
|
||||||
previewNoteText: "本文をプレビュー"
|
previewNoteText: "本文をプレビュー"
|
||||||
customCss: "カスタムCSS"
|
customCss: "カスタムCSS"
|
||||||
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
|
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "foundkey",
|
"name": "foundkey",
|
||||||
"version": "13.0.0-preview2",
|
"version": "13.0.0-preview.1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
export class allowlistSecureMode1626733991004 {
|
||||||
|
name = 'allowlistSecureMode1626733991004';
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "allowedHosts" character varying(256) [] default '{}'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "secureMode" bool default false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "privateMode" bool default false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowedHosts"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "secureMode"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privateMode"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export class removeAds1657570176749 {
|
export class removeAds1657570176749 {
|
||||||
name = 'removeAds1657570176749';
|
name = 'removeAds1657570176749'
|
||||||
|
|
||||||
async up(queryRunner) {
|
async up(queryRunner) {
|
||||||
await queryRunner.query(`DROP TABLE "ad"`);
|
await queryRunner.query(`DROP TABLE "ad"`);
|
|
@ -1,44 +0,0 @@
|
||||||
export class sync1667503570994 {
|
|
||||||
name = 'sync1667503570994'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await Promise.all([
|
|
||||||
// the migration for renote mutes added the index to the wrong table
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`),
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`),
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`),
|
|
||||||
queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `),
|
|
||||||
queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `),
|
|
||||||
queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `),
|
|
||||||
|
|
||||||
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`),
|
|
||||||
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`),
|
|
||||||
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`),
|
|
||||||
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET NOT NULL`),
|
|
||||||
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET DEFAULT ''`),
|
|
||||||
queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `),
|
|
||||||
queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`),
|
|
||||||
queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await Promise.all([
|
|
||||||
queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`),
|
|
||||||
queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`),
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`),
|
|
||||||
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP DEFAULT`),
|
|
||||||
queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP NOT NULL`),
|
|
||||||
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`),
|
|
||||||
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`),
|
|
||||||
queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`),
|
|
||||||
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`),
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`),
|
|
||||||
queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`),
|
|
||||||
queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `),
|
|
||||||
queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `),
|
|
||||||
queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "13.0.0-preview2",
|
"version": "13.0.0-preview1",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -15,8 +15,8 @@
|
||||||
"test": "npm run mocha"
|
"test": "npm run mocha"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.3.1",
|
"@bull-board/api": "^4.2.2",
|
||||||
"@bull-board/koa": "^4.3.1",
|
"@bull-board/koa": "4.0.0",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@elastic/elasticsearch": "7.11.0",
|
"@elastic/elasticsearch": "7.11.0",
|
||||||
"@koa/cors": "3.1.0",
|
"@koa/cors": "3.1.0",
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
"rss-parser": "3.12.0",
|
"rss-parser": "3.12.0",
|
||||||
"sanitize-html": "2.7.0",
|
"sanitize-html": "2.7.0",
|
||||||
"semver": "7.3.7",
|
"semver": "7.3.7",
|
||||||
"sharp": "0.31.2",
|
"sharp": "0.30.7",
|
||||||
"speakeasy": "2.0.0",
|
"speakeasy": "2.0.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
|
|
|
@ -38,8 +38,6 @@ export default function load(): Config {
|
||||||
|
|
||||||
config.port = config.port || parseInt(process.env.PORT || '', 10);
|
config.port = config.port || parseInt(process.env.PORT || '', 10);
|
||||||
|
|
||||||
if (!config.maxNoteTextLength) config.maxNoteTextLength = 3000;
|
|
||||||
|
|
||||||
mixin.version = meta.version;
|
mixin.version = meta.version;
|
||||||
mixin.host = url.host;
|
mixin.host = url.host;
|
||||||
mixin.hostname = url.hostname;
|
mixin.hostname = url.hostname;
|
||||||
|
|
|
@ -10,7 +10,7 @@ function getRedisFamily(family?: string | number): number {
|
||||||
dual: 0,
|
dual: 0,
|
||||||
};
|
};
|
||||||
if (typeof family === 'string' && family in familyMap) {
|
if (typeof family === 'string' && family in familyMap) {
|
||||||
return familyMap[family as keyof typeof familyMap];
|
return familyMap[family];
|
||||||
} else if (typeof family === 'number' && Object.values(familyMap).includes(family)) {
|
} else if (typeof family === 'number' && Object.values(familyMap).includes(family)) {
|
||||||
return family;
|
return family;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ export type Source = {
|
||||||
db?: number;
|
db?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
};
|
};
|
||||||
elasticsearch?: {
|
elasticsearch: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
ssl?: boolean;
|
ssl?: boolean;
|
||||||
|
@ -41,8 +41,6 @@ export type Source = {
|
||||||
|
|
||||||
maxFileSize?: number;
|
maxFileSize?: number;
|
||||||
|
|
||||||
maxNoteTextLength?: number;
|
|
||||||
|
|
||||||
accesslog?: string;
|
accesslog?: string;
|
||||||
|
|
||||||
clusterLimit?: number;
|
clusterLimit?: number;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
||||||
|
|
||||||
// Time constants
|
// Time constants
|
||||||
export const SECOND = 1000;
|
export const SECOND = 1000;
|
||||||
export const MINUTE = 60 * SECOND;
|
export const MINUTE = 60 * SECOND;
|
||||||
|
|
|
@ -62,21 +62,22 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
const rel = node.attrs.find(x => x.name === 'rel');
|
const rel = node.attrs.find(x => x.name === 'rel');
|
||||||
const href = node.attrs.find(x => x.name === 'href');
|
const href = node.attrs.find(x => x.name === 'href');
|
||||||
|
|
||||||
// hashtags
|
// ハッシュタグ
|
||||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||||
text += txt;
|
text += txt;
|
||||||
// mentions
|
// メンション
|
||||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
||||||
const part = txt.split('@');
|
const part = txt.split('@');
|
||||||
|
|
||||||
if (part.length === 2 && href) {
|
if (part.length === 2 && href) {
|
||||||
// restore the host name part
|
//#region ホスト名部分が省略されているので復元する
|
||||||
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
||||||
text += acct;
|
text += acct;
|
||||||
|
//#endregion
|
||||||
} else if (part.length === 3) {
|
} else if (part.length === 3) {
|
||||||
text += txt;
|
text += txt;
|
||||||
}
|
}
|
||||||
// other
|
// その他
|
||||||
} else {
|
} else {
|
||||||
const generateLink = () => {
|
const generateLink = () => {
|
||||||
if (!href && !txt) {
|
if (!href && !txt) {
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
export class Cache<T> {
|
export class Cache<T> {
|
||||||
public cache: Map<string | null, { date: number; value: T; }>;
|
public cache: Map<string | null, { date: number; value: T; }>;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
public fetcher: (key: string | null) => Promise<T | undefined>;
|
|
||||||
|
|
||||||
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
|
constructor(lifetime: Cache<never>['lifetime']) {
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
this.fetcher = fetcher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(key: string | null, value: T): void {
|
public set(key: string | null, value: T): void {
|
||||||
|
@ -19,13 +17,10 @@ export class Cache<T> {
|
||||||
public get(key: string | null): T | undefined {
|
public get(key: string | null): T | undefined {
|
||||||
const cached = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
if (cached == null) return undefined;
|
if (cached == null) return undefined;
|
||||||
|
|
||||||
// discard if past the cache lifetime
|
|
||||||
if ((Date.now() - cached.date) > this.lifetime) {
|
if ((Date.now() - cached.date) > this.lifetime) {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cached.value;
|
return cached.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,22 +29,52 @@ export class Cache<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the value is cached, it is returned. Otherwise the fetcher is
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
* run to get the value. If the fetcher returns undefined, it is
|
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||||
* returned but not cached.
|
|
||||||
*/
|
*/
|
||||||
public async fetch(key: string | null): Promise<T | undefined> {
|
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||||
const cached = this.get(key);
|
const cachedValue = this.get(key);
|
||||||
if (cached !== undefined) {
|
if (cachedValue !== undefined) {
|
||||||
return cached;
|
if (validator) {
|
||||||
|
if (validator(cachedValue)) {
|
||||||
|
// Cache HIT
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const value = await this.fetcher(key);
|
// Cache HIT
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// don't cache undefined
|
// Cache MISS
|
||||||
if (value !== undefined)
|
const value = await fetcher();
|
||||||
this.set(key, value);
|
this.set(key, value);
|
||||||
|
|
||||||
return 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,26 +3,22 @@ import { Note } from '@/models/entities/note.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js';
|
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { MINUTE } from '@/const.js';
|
|
||||||
import { getFullApAccount } from './convert-host.js';
|
import { getFullApAccount } from './convert-host.js';
|
||||||
import { Packed } from './schema.js';
|
import { Packed } from './schema.js';
|
||||||
import { Cache } from './cache.js';
|
import { Cache } from './cache.js';
|
||||||
|
|
||||||
const blockingCache = new Cache<User['id'][]>(
|
const blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
|
||||||
5 * MINUTE,
|
|
||||||
(blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// designation for users you follow, list users and groups is disabled for performance reasons
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* either noteUserFollowers or antennaUserFollowing must be specified
|
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
|
||||||
*/
|
*/
|
||||||
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
|
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;
|
if (note.visibility === 'specified') return false;
|
||||||
|
|
||||||
// skip if the antenna creator is blocked by the note author
|
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||||
const blockings = await blockingCache.fetch(noteUser.id);
|
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
||||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||||
|
|
||||||
if (note.visibility === 'followers') {
|
if (note.visibility === 'followers') {
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
import push from 'web-push';
|
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { Meta } from '@/models/entities/meta.js';
|
import { Meta } from '@/models/entities/meta.js';
|
||||||
import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js';
|
|
||||||
|
|
||||||
let cache: Meta;
|
let cache: Meta;
|
||||||
|
|
||||||
/**
|
export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||||
* Performs the primitive database operation to set the server configuration
|
if (!noCache && cache) return cache;
|
||||||
*/
|
|
||||||
export async function setMeta(meta: Meta): Promise<void> {
|
|
||||||
const unlock = await getFetchInstanceMetadataLock('localhost');
|
|
||||||
|
|
||||||
// try to mitigate older bugs where multiple meta entries may have been created
|
return await db.transaction(async transactionalEntityManager => {
|
||||||
db.manager.clear(Meta);
|
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||||
db.manager.insert(Meta, meta);
|
const metas = await transactionalEntityManager.find(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: {
|
order: {
|
||||||
id: 'DESC',
|
id: 'DESC',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
unlock();
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMeta(noCache = false): Promise<Meta> {
|
setInterval(() => {
|
||||||
if (!noCache && cache) return cache;
|
fetchMeta(true).then(meta => {
|
||||||
|
cache = meta;
|
||||||
await getMeta();
|
});
|
||||||
|
}, 1000 * 10);
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,4 +11,4 @@ export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
|
||||||
* Maximum image description length that can be stored in DB.
|
* Maximum image description length that can be stored in DB.
|
||||||
* Surrogate pairs count as one
|
* Surrogate pairs count as one
|
||||||
*/
|
*/
|
||||||
export const DB_MAX_IMAGE_COMMENT_LENGTH = 2048;
|
export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
const locales = await import('../../../../locales/index.js').then(mod => mod.default);
|
export class I18n<T extends Record<string, any>> {
|
||||||
|
public locale: T;
|
||||||
|
|
||||||
export class I18n {
|
constructor(locale: T) {
|
||||||
public ts: Record<string, any>;
|
this.locale = locale;
|
||||||
|
|
||||||
constructor(locale: string) {
|
//#region BIND
|
||||||
this.ts = locales[locale];
|
|
||||||
this.t = this.t.bind(this);
|
this.t = this.t.bind(this);
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||||
public t(key: string, args?: Record<string, any>): string {
|
public t(key: string, args?: Record<string, any>): string {
|
||||||
try {
|
try {
|
||||||
let str = key.split('.').reduce((o, i) => o[i], this.ts) as string;
|
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
|
||||||
|
|
||||||
if (args) {
|
if (args) {
|
||||||
for (const [k, v] of Object.entries(args)) {
|
for (const [k, v] of Object.entries(args)) {
|
||||||
|
|
|
@ -3,11 +3,8 @@ import { User } from '@/models/entities/user.js';
|
||||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||||
import { Cache } from './cache.js';
|
import { Cache } from './cache.js';
|
||||||
|
|
||||||
const cache = new Cache<UserKeypair>(
|
const cache = new Cache<UserKeypair>(Infinity);
|
||||||
Infinity,
|
|
||||||
(userId) => UserKeypairs.findOneByOrFail({ userId }),
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||||
return await cache.fetch(userId);
|
return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,14 @@ import { Emojis } from '@/models/index.js';
|
||||||
import { Emoji } from '@/models/entities/emoji.js';
|
import { Emoji } from '@/models/entities/emoji.js';
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
import { query } from '@/prelude/url.js';
|
import { query } from '@/prelude/url.js';
|
||||||
import { HOUR } from '@/const.js';
|
|
||||||
import { Cache } from './cache.js';
|
import { Cache } from './cache.js';
|
||||||
import { isSelfHost, toPunyNullable } from './convert-host.js';
|
import { isSelfHost, toPunyNullable } from './convert-host.js';
|
||||||
import { decodeReaction } from './reaction-lib.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 = {
|
type PopulatedEmoji = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -49,22 +36,28 @@ function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||||
|
|
||||||
const name = match[1];
|
const name = match[1];
|
||||||
|
|
||||||
|
// ホスト正規化
|
||||||
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
|
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
|
||||||
|
|
||||||
return { name, host };
|
return { name, host };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve emoji information from ActivityPub attachment.
|
* 添付用絵文字情報を解決する
|
||||||
* @param emojiName custom emoji names attached to notes, user profiles or in rections. Colons should not be included. Localhost is denote by @. (see also `decodeReaction`)
|
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
|
||||||
* @param noteUserHost host that the content is from, to default to
|
* @param noteUserHost ノートやユーザープロフィールの所有者のホスト
|
||||||
* @returns emoji information. `null` means not found.
|
* @returns 絵文字情報, nullは未マッチを意味する
|
||||||
*/
|
*/
|
||||||
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
|
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
|
||||||
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
|
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
|
||||||
if (name == null) return null;
|
if (name == null) return null;
|
||||||
|
|
||||||
const emoji = await cache.fetch(`${host ?? ''}:${name}`);
|
const queryOrNull = async () => (await Emojis.findOneBy({
|
||||||
|
name,
|
||||||
|
host: host ?? IsNull(),
|
||||||
|
})) || null;
|
||||||
|
|
||||||
|
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
|
||||||
|
|
||||||
if (emoji == null) return null;
|
if (emoji == null) return null;
|
||||||
|
|
||||||
|
@ -79,7 +72,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve list of emojis from the cache. Uncached emoji are dropped.
|
* 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
|
||||||
*/
|
*/
|
||||||
export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
|
export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
|
||||||
const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
|
const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
|
||||||
|
@ -110,20 +103,11 @@ export function aggregateNoteEmojis(notes: Note[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query list of emojis in bulk and add them to the cache.
|
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||||
*/
|
*/
|
||||||
export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
|
export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
|
||||||
const notCachedEmojis = emojis.filter(emoji => {
|
const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null);
|
||||||
// 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[] = [];
|
const emojisQuery: any[] = [];
|
||||||
// group by hosts to try to reduce query size
|
|
||||||
const hosts = new Set(notCachedEmojis.map(e => e.host));
|
const hosts = new Set(notCachedEmojis.map(e => e.host));
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
emojisQuery.push({
|
emojisQuery.push({
|
||||||
|
@ -131,14 +115,11 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
|
||||||
host: host ?? IsNull(),
|
host: host ?? IsNull(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const _emojis = emojisQuery.length > 0 ? await Emojis.find({
|
||||||
await Emojis.find({
|
|
||||||
where: emojisQuery,
|
where: emojisQuery,
|
||||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||||
}).then(emojis => {
|
}) : [];
|
||||||
// store all emojis into the cache
|
for (const emoji of _emojis) {
|
||||||
emojis.forEach(emoji => {
|
cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||||
cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji);
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
|
|
||||||
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
|
export function isPureRenote(note: Note): boolean {
|
||||||
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
|
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { Brackets } from 'typeorm';
|
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
|
||||||
import { Instances } from '@/models/index.js';
|
|
||||||
import { Instance } from '@/models/entities/instance.js';
|
|
||||||
import { DAY } from '@/const.js';
|
|
||||||
|
|
||||||
// Threshold from last contact after which an instance will be considered
|
|
||||||
// "dead" and should no longer get activities delivered to it.
|
|
||||||
const deadThreshold = 7 * DAY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the subset of hosts which should be skipped.
|
|
||||||
*
|
|
||||||
* @param hosts array of punycoded instance hosts
|
|
||||||
* @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter)
|
|
||||||
*/
|
|
||||||
export async function skippedInstances(hosts: Array<Instace['host']>): Array<Instance['host']> {
|
|
||||||
// first check for blocked instances since that info may already be in memory
|
|
||||||
const { blockedHosts } = await fetchMeta();
|
|
||||||
|
|
||||||
const skipped = hosts.filter(host => blockedHosts.includes(host));
|
|
||||||
// if possible return early and skip accessing the database
|
|
||||||
if (skipped.length === hosts.length) return hosts;
|
|
||||||
|
|
||||||
const deadTime = new Date(Date.now() - deadThreshold);
|
|
||||||
|
|
||||||
return skipped.concat(
|
|
||||||
await Instances.createQueryBuilder('instance')
|
|
||||||
.where('instance.host in (:...hosts)', {
|
|
||||||
// don't check hosts again that we already know are suspended
|
|
||||||
// also avoids adding duplicates to the list
|
|
||||||
hosts: hosts.filter(host => !skipped.includes(host)),
|
|
||||||
})
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('instance.isSuspended')
|
|
||||||
.orWhere('instance.lastCommunicatedAt < :deadTime', { deadTime })
|
|
||||||
.orWhere('instance.latestStatus = 410');
|
|
||||||
}))
|
|
||||||
.select('host')
|
|
||||||
.getRawMany()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a specific host (punycoded) should be skipped.
|
|
||||||
* Convenience wrapper around skippedInstances which should only be used if there is a single host to check.
|
|
||||||
* If you have multiple hosts, consider using skippedInstances instead to do a bulk check.
|
|
||||||
*
|
|
||||||
* @param host punycoded instance host
|
|
||||||
* @returns whether the given host should be skipped
|
|
||||||
*/
|
|
||||||
export async function shouldSkipInstance(host: Instance['host']): boolean {
|
|
||||||
const skipped = await skippedInstances([host]);
|
|
||||||
return skipped.length > 0;
|
|
||||||
}
|
|
|
@ -62,8 +62,7 @@ export class DriveFile {
|
||||||
public size: number;
|
public size: number;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 2048,
|
length: 512, nullable: true,
|
||||||
nullable: true,
|
|
||||||
comment: 'The comment of the DriveFile.',
|
comment: 'The comment of the DriveFile.',
|
||||||
})
|
})
|
||||||
public comment: string | null;
|
public comment: string | null;
|
||||||
|
|
|
@ -7,7 +7,7 @@ export class Instance {
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date and time this instance was first seen.
|
* このインスタンスを捕捉した日時
|
||||||
*/
|
*/
|
||||||
@Index()
|
@Index()
|
||||||
@Column('timestamp with time zone', {
|
@Column('timestamp with time zone', {
|
||||||
|
@ -16,7 +16,7 @@ export class Instance {
|
||||||
public caughtAt: Date;
|
public caughtAt: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hostname
|
* ホスト
|
||||||
*/
|
*/
|
||||||
@Index({ unique: true })
|
@Index({ unique: true })
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
|
@ -26,7 +26,7 @@ export class Instance {
|
||||||
public host: string;
|
public host: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of users on this instance.
|
* インスタンスのユーザー数
|
||||||
*/
|
*/
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -35,7 +35,7 @@ export class Instance {
|
||||||
public usersCount: number;
|
public usersCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of notes on this instance.
|
* インスタンスの投稿数
|
||||||
*/
|
*/
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -44,7 +44,7 @@ export class Instance {
|
||||||
public notesCount: number;
|
public notesCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of local users who are followed by users from this instance.
|
* このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
|
||||||
*/
|
*/
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -52,7 +52,7 @@ export class Instance {
|
||||||
public followingCount: number;
|
public followingCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of users from this instance who are followed by local users.
|
* このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
|
||||||
*/
|
*/
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -60,7 +60,7 @@ export class Instance {
|
||||||
public followersCount: number;
|
public followersCount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of the latest outgoing HTTP request.
|
* 直近のリクエスト送信日時
|
||||||
*/
|
*/
|
||||||
@Column('timestamp with time zone', {
|
@Column('timestamp with time zone', {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -68,7 +68,7 @@ export class Instance {
|
||||||
public latestRequestSentAt: Date | null;
|
public latestRequestSentAt: Date | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP status code that was received for the last outgoing HTTP request.
|
* 直近のリクエスト送信時のHTTPステータスコード
|
||||||
*/
|
*/
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -76,7 +76,7 @@ export class Instance {
|
||||||
public latestStatus: number | null;
|
public latestStatus: number | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of the latest incoming HTTP request.
|
* 直近のリクエスト受信日時
|
||||||
*/
|
*/
|
||||||
@Column('timestamp with time zone', {
|
@Column('timestamp with time zone', {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -84,13 +84,13 @@ export class Instance {
|
||||||
public latestRequestReceivedAt: Date | null;
|
public latestRequestReceivedAt: Date | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of last communication with this instance (incoming or outgoing).
|
* このインスタンスと最後にやり取りした日時
|
||||||
*/
|
*/
|
||||||
@Column('timestamp with time zone')
|
@Column('timestamp with time zone')
|
||||||
public lastCommunicatedAt: Date;
|
public lastCommunicatedAt: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this instance seems unresponsive.
|
* このインスタンスと不通かどうか
|
||||||
*/
|
*/
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -98,7 +98,7 @@ export class Instance {
|
||||||
public isNotResponding: boolean;
|
public isNotResponding: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether sending activities to this instance has been suspended.
|
* このインスタンスへの配信を停止するか
|
||||||
*/
|
*/
|
||||||
@Index()
|
@Index()
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
|
|
|
@ -77,6 +77,21 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public blockedHosts: string[];
|
public blockedHosts: string[];
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
public secureMode: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
public privateMode: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, array: true, default: '{}'
|
||||||
|
})
|
||||||
|
public allowedHosts: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-foundkey}',
|
length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-foundkey}',
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,16 +6,13 @@ import { Packed } from '@/misc/schema.js';
|
||||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
||||||
import { populateEmojis } from '@/misc/populate-emojis.js';
|
import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { Cache } from '@/misc/cache.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { Instance } from '../entities/instance.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';
|
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>(
|
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
||||||
3 * HOUR,
|
|
||||||
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
|
|
||||||
);
|
|
||||||
|
|
||||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||||
|
@ -30,7 +27,7 @@ const ajv = new Ajv();
|
||||||
const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||||
const passwordSchema = { type: 'string', minLength: 1 } as const;
|
const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 2048 } as const;
|
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
|
||||||
const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
const 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;
|
const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||||
|
|
||||||
|
@ -312,15 +309,17 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
isModerator: user.isModerator || falsy,
|
isModerator: user.isModerator || falsy,
|
||||||
isBot: user.isBot || falsy,
|
isBot: user.isBot || falsy,
|
||||||
isCat: user.isCat || falsy,
|
isCat: user.isCat || falsy,
|
||||||
instance: !user.host ? undefined : userInstanceCache.fetch(user.host)
|
instance: user.host ? userInstanceCache.fetch(user.host,
|
||||||
.then(instance => !instance ? undefined : {
|
() => Instances.findOneBy({ host: user.host! }),
|
||||||
|
v => v != null,
|
||||||
|
).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
iconUrl: instance.iconUrl,
|
iconUrl: instance.iconUrl,
|
||||||
faviconUrl: instance.faviconUrl,
|
faviconUrl: instance.faviconUrl,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
}),
|
} : undefined) : undefined,
|
||||||
emojis: populateEmojis(user.emojis, user.host),
|
emojis: populateEmojis(user.emojis, user.host),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,45 @@ import Logger from '@/services/logger.js';
|
||||||
import { Instances } from '@/models/index.js';
|
import { Instances } from '@/models/index.js';
|
||||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { toPuny } from '@/misc/convert-host.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 { StatusError } from '@/misc/fetch.js';
|
||||||
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
|
||||||
import { DeliverJobData } from '@/queue/types.js';
|
import { DeliverJobData } from '@/queue/types.js';
|
||||||
|
|
||||||
const logger = new Logger('deliver');
|
const logger = new Logger('deliver');
|
||||||
|
|
||||||
let latest: string | null = null;
|
let latest: string | null = null;
|
||||||
|
|
||||||
|
const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
|
||||||
|
|
||||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
const { host } = new URL(job.data.to);
|
const { host } = new URL(job.data.to);
|
||||||
const puny = toPuny(host);
|
|
||||||
|
|
||||||
if (await shouldSkipInstance(puny)) return 'skip';
|
// ブロックしてたら中断
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.blockedHosts.includes(toPuny(host))) {
|
||||||
|
return 'skip (blocked)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.privateMode && !meta.allowedHosts.includes(toPuny(host))) {
|
||||||
|
return 'skip (not allowed)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||||
|
@ -62,8 +87,8 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (res.isClientError) {
|
||||||
// A client error means that something is wrong with the request we are making,
|
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||||
// which means that retrying it makes no sense.
|
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||||
return `${res.statusCode} ${res.statusMessage}`;
|
return `${res.statusCode} ${res.statusMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,11 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only permitted instances if in private mode.
|
||||||
|
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
|
||||||
|
return `Blocked request: ${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
const keyIdLower = signature.keyId.toLowerCase();
|
const keyIdLower = signature.keyId.toLowerCase();
|
||||||
if (keyIdLower.startsWith('acct:')) {
|
if (keyIdLower.startsWith('acct:')) {
|
||||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Bull from 'bull';
|
import Bull from 'bull';
|
||||||
import { In, LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { AttestationChallenges, Mutings, PasswordResetRequests, Signins } from '@/models/index.js';
|
import { AttestationChallenges, Mutings, Signins } from '@/models/index.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
import { MINUTE, DAY } from '@/const.js';
|
import { MINUTE, DAY } from '@/const.js';
|
||||||
import { queueLogger } from '@/queue/logger.js';
|
import { queueLogger } from '@/queue/logger.js';
|
||||||
|
@ -35,11 +35,6 @@ export async function checkExpired(job: Bull.Job<Record<string, unknown>>, done:
|
||||||
createdAt: LessThan(new Date(new Date().getTime() - 5 * MINUTE)),
|
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.');
|
logger.succ('Deleted expired mutes, signins and attestation challenges.');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|
69
packages/backend/src/remote/activitypub/check-fetch.ts
Normal file
69
packages/backend/src/remote/activitypub/check-fetch.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
import { IncomingMessage } from 'http';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
import httpSignature from '@peertube/http-signature';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
|
import DbResolver from '@/remote/activitypub/db-resolver.js';
|
||||||
|
import { getApId } from '@/remote/activitypub/type.js';
|
||||||
|
|
||||||
|
export default async function checkFetch(req: IncomingMessage): Promise<number> {
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
let signature;
|
||||||
|
|
||||||
|
try {
|
||||||
|
signature = httpSignature.parseRequest(req, { 'headers': [] });
|
||||||
|
} catch (e) {
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyId = new URL(signature.keyId);
|
||||||
|
const host = toPuny(keyId.hostname);
|
||||||
|
|
||||||
|
if (meta.blockedHosts.includes(host)) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.privateMode && host !== config.host && !meta.allowedHosts.includes(host)) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyIdLower = signature.keyId.toLowerCase();
|
||||||
|
if (keyIdLower.startsWith('acct:')) {
|
||||||
|
// Old keyId is no longer supported.
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbResolver = new DbResolver();
|
||||||
|
|
||||||
|
// Get user from database based on HTTP-Signature keyId
|
||||||
|
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
||||||
|
|
||||||
|
// If keyid is unknown, try resolving it
|
||||||
|
if (authUser == null) {
|
||||||
|
try {
|
||||||
|
keyId.hash = '';
|
||||||
|
authUser = await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
|
||||||
|
} catch (e) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUser?.key == null) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authUser.user.host !== host) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP-Signature validation
|
||||||
|
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||||
|
|
||||||
|
if (!httpSignatureValidated) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 200;
|
||||||
|
}
|
|
@ -10,14 +10,8 @@ import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||||
import { IObject, getApId } from './type.js';
|
import { IObject, getApId } from './type.js';
|
||||||
import { resolvePerson } from './models/person.js';
|
import { resolvePerson } from './models/person.js';
|
||||||
|
|
||||||
const publicKeyCache = new Cache<UserPublickey>(
|
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||||
Infinity,
|
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(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 = {
|
export type UriParseResult = {
|
||||||
/** wether the URI was generated by us */
|
/** wether the URI was generated by us */
|
||||||
|
@ -105,9 +99,13 @@ export default class DbResolver {
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== 'users') return null;
|
if (parsed.type !== 'users') return null;
|
||||||
|
|
||||||
return await userByIdCache.fetch(parsed.id) ?? null;
|
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
|
||||||
|
id: parsed.id,
|
||||||
|
}).then(x => x ?? undefined)) ?? null;
|
||||||
} else {
|
} else {
|
||||||
return await uriPersonCache.fetch(parsed.uri) ?? null;
|
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
|
||||||
|
uri: parsed.uri,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,12 +116,20 @@ export default class DbResolver {
|
||||||
user: CacheableRemoteUser;
|
user: CacheableRemoteUser;
|
||||||
key: UserPublickey;
|
key: UserPublickey;
|
||||||
} | null> {
|
} | null> {
|
||||||
const key = await publicKeyCache.fetch(keyId);
|
const key = await publicKeyCache.fetch(keyId, async () => {
|
||||||
|
const key = await UserPublickeys.findOneBy({
|
||||||
|
keyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (key == null) return null;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}, key => key != null);
|
||||||
|
|
||||||
if (key == null) return null;
|
if (key == null) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser,
|
user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser,
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -139,7 +145,7 @@ export default class DbResolver {
|
||||||
|
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
|
|
||||||
const key = await publicKeyByUserIdCache.fetch(user.id);
|
const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -2,17 +2,12 @@ import { IsNull, Not } from 'typeorm';
|
||||||
import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js';
|
import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js';
|
||||||
import { Users, Followings } from '@/models/index.js';
|
import { Users, Followings } from '@/models/index.js';
|
||||||
import { deliver } from '@/queue/index.js';
|
import { deliver } from '@/queue/index.js';
|
||||||
import { skippedInstances } from '@/misc/skipped-instances.js';
|
|
||||||
|
|
||||||
//#region types
|
//#region types
|
||||||
interface IRecipe {
|
interface IRecipe {
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEveryoneRecipe extends IRecipe {
|
|
||||||
type: 'Everyone';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IFollowersRecipe extends IRecipe {
|
interface IFollowersRecipe extends IRecipe {
|
||||||
type: 'Followers';
|
type: 'Followers';
|
||||||
}
|
}
|
||||||
|
@ -22,9 +17,6 @@ interface IDirectRecipe extends IRecipe {
|
||||||
to: IRemoteUser;
|
to: IRemoteUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEveryone = (recipe: any): recipe is IEveryoneRecipe =>
|
|
||||||
recipe.type === 'Everyone';
|
|
||||||
|
|
||||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
||||||
recipe.type === 'Followers';
|
recipe.type === 'Followers';
|
||||||
|
|
||||||
|
@ -71,13 +63,6 @@ export default class DeliverManager {
|
||||||
this.addRecipe(recipe);
|
this.addRecipe(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add recipe to send this activity to all known sharedInboxes
|
|
||||||
*/
|
|
||||||
public addEveryone() {
|
|
||||||
this.addRecipe({ type: 'Everyone' } as IEveryoneRecipe);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add recipe
|
* Add recipe
|
||||||
* @param recipe Recipe
|
* @param recipe Recipe
|
||||||
|
@ -97,40 +82,31 @@ export default class DeliverManager {
|
||||||
/*
|
/*
|
||||||
build inbox list
|
build inbox list
|
||||||
|
|
||||||
Processing order matters to avoid duplication.
|
Process follower recipes first to avoid duplication when processing
|
||||||
|
direct recipes later.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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))) {
|
if (this.recipes.some(r => isFollowers(r))) {
|
||||||
// followers deliver
|
// followers deliver
|
||||||
const followers = await Followings.createQueryBuilder('followings')
|
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||||
// return either the shared inbox (if available) or the individual inbox
|
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||||
.select('COALESCE(followings.followerSharedInbox, followings.followerInbox)', 'inbox')
|
const followers = await Followings.find({
|
||||||
// so we don't have to make our inboxes Set work as hard
|
where: {
|
||||||
.distinct(true)
|
followeeId: this.actor.id,
|
||||||
// ...for the specific actors followers
|
followerHost: Not(IsNull()),
|
||||||
.where('followings.followeeId = :actorId', { actorId: this.actor.id })
|
},
|
||||||
// don't deliver to ourselves
|
select: {
|
||||||
.andWhere('followings.followerHost IS NOT NULL')
|
followerSharedInbox: true,
|
||||||
.getRawMany();
|
followerInbox: true,
|
||||||
|
},
|
||||||
|
}) as {
|
||||||
|
followerSharedInbox: string | null;
|
||||||
|
followerInbox: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
followers.forEach(({ inbox }) => inboxes.add(inbox));
|
for (const following of followers) {
|
||||||
|
const inbox = following.followerSharedInbox || following.followerInbox;
|
||||||
|
inboxes.add(inbox);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
||||||
|
@ -143,19 +119,8 @@ export default class DeliverManager {
|
||||||
)
|
)
|
||||||
.forEach(recipe => inboxes.add(recipe.to.inbox!));
|
.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
|
// deliver
|
||||||
for (const inbox of inboxes) {
|
for (const inbox of inboxes) {
|
||||||
// skip instances as indicated
|
|
||||||
if (instancesToSkip.includes(new URL(inbox).host)) continue;
|
|
||||||
|
|
||||||
deliver(this.actor, this.activity, inbox);
|
deliver(this.actor, this.activity, inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { createReaction } from '@/services/note/reaction/create.js';
|
import create from '@/services/note/reaction/create.js';
|
||||||
import { ILike, getApId } from '../type.js';
|
import { ILike, getApId } from '../type.js';
|
||||||
import { fetchNote, extractEmojis } from '../models/note.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);
|
await extractEmojis(activity.tag || [], actor.host).catch(() => null);
|
||||||
|
|
||||||
return await createReaction(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
|
return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
|
||||||
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
|
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
|
||||||
return 'skip: already reacted';
|
return 'skip: already reacted';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
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 { ILike, getApId } from '@/remote/activitypub/type.js';
|
||||||
import { fetchNote } from '@/remote/activitypub/models/note.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 post from '@/services/note/create.js';
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { unique, toArray, toSingle } from '@/prelude/array.js';
|
import { unique, toArray, toSingle } from '@/prelude/array.js';
|
||||||
import { vote } from '@/services/note/polls/vote.js';
|
import vote from '@/services/note/polls/vote.js';
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||||
import { extractDbHost, toPuny } from '@/misc/convert-host.js';
|
import { extractDbHost, toPuny } from '@/misc/convert-host.js';
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
|
||||||
* @param user http-signature user
|
* @param user http-signature user
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
export async function signedGet(url: string, user: { id: User['id'] }) {
|
||||||
const keypair = await getUserKeypair(user.id);
|
const keypair = await getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = createSignedGet({
|
const req = createSignedGet({
|
||||||
|
|
|
@ -72,7 +72,11 @@ export default class Resolver {
|
||||||
throw new Error('Instance is blocked');
|
throw new Error('Instance is blocked');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.user) {
|
if (meta.privateMode && config.host !== host && !meta.allowedHosts.includes(host)) {
|
||||||
|
throw new Error('Instance is not allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.signToActivityPubGet && !this.user) {
|
||||||
this.user = await getInstanceActor();
|
this.user = await getInstanceActor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,14 @@ import renderKey from '@/remote/activitypub/renderer/key.js';
|
||||||
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
|
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
|
||||||
import renderEmoji from '@/remote/activitypub/renderer/emoji.js';
|
import renderEmoji from '@/remote/activitypub/renderer/emoji.js';
|
||||||
import { inbox as processInbox } from '@/queue/index.js';
|
import { inbox as processInbox } from '@/queue/index.js';
|
||||||
import { isSelfHost } from '@/misc/convert-host.js';
|
import { isSelfHost, toPuny } from '@/misc/convert-host.js';
|
||||||
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
||||||
import { ILocalUser, User } from '@/models/entities/user.js';
|
import { ILocalUser, User } from '@/models/entities/user.js';
|
||||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||||
|
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||||
|
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||||
import Outbox, { packActivity } from './activitypub/outbox.js';
|
import Outbox, { packActivity } from './activitypub/outbox.js';
|
||||||
import Followers from './activitypub/followers.js';
|
import Followers from './activitypub/followers.js';
|
||||||
|
@ -23,6 +26,8 @@ import Featured from './activitypub/featured.js';
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
|
//#region Routing
|
||||||
|
|
||||||
function inbox(ctx: Router.RouterContext) {
|
function inbox(ctx: Router.RouterContext) {
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
|
@ -43,8 +48,6 @@ const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystr
|
||||||
|
|
||||||
function isActivityPubReq(ctx: Router.RouterContext) {
|
function isActivityPubReq(ctx: Router.RouterContext) {
|
||||||
ctx.response.vary('Accept');
|
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);
|
const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
|
||||||
return typeof accepted === 'string' && !accepted.match(/html/);
|
return typeof accepted === 'string' && !accepted.match(/html/);
|
||||||
}
|
}
|
||||||
|
@ -66,6 +69,12 @@ router.post('/users/:user/inbox', json(), inbox);
|
||||||
router.get('/notes/:note', async (ctx, next) => {
|
router.get('/notes/:note', async (ctx, next) => {
|
||||||
if (!isActivityPubReq(ctx)) return await next();
|
if (!isActivityPubReq(ctx)) return await next();
|
||||||
|
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const note = await Notes.findOneBy({
|
const note = await Notes.findOneBy({
|
||||||
id: ctx.params.note,
|
id: ctx.params.note,
|
||||||
visibility: In(['public' as const, 'home' as const]),
|
visibility: In(['public' as const, 'home' as const]),
|
||||||
|
@ -77,8 +86,8 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect if remote
|
// リモートだったらリダイレクト
|
||||||
if (note.userHost != null) {
|
if (note.userHost !== null) {
|
||||||
if (note.uri == null || isSelfHost(note.userHost)) {
|
if (note.uri == null || isSelfHost(note.userHost)) {
|
||||||
ctx.status = 500;
|
ctx.status = 500;
|
||||||
return;
|
return;
|
||||||
|
@ -88,18 +97,21 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = renderActivity(await renderNote(note, false));
|
ctx.body = renderActivity(await renderNote(note, false));
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// note activity
|
// note activity
|
||||||
router.get('/notes/:note/activity', async ctx => {
|
router.get('/notes/:note/activity', async ctx => {
|
||||||
if (!isActivityPubReq(ctx)) {
|
const verify = await checkFetch(ctx.req);
|
||||||
/*
|
if (verify !== 200) {
|
||||||
Redirect to the human readable page. in this case using next is not possible,
|
ctx.status = verify;
|
||||||
since there is no human readable page explicitly for the activity.
|
|
||||||
*/
|
|
||||||
ctx.redirect(`/notes/${ctx.params.note}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +128,12 @@ router.get('/notes/:note/activity', async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = renderActivity(await packActivity(note));
|
ctx.body = renderActivity(await packActivity(note));
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -134,6 +151,20 @@ router.get('/users/:user/collections/featured', Featured);
|
||||||
|
|
||||||
// publickey
|
// publickey
|
||||||
router.get('/users/:user/publickey', async ctx => {
|
router.get('/users/:user/publickey', async ctx => {
|
||||||
|
const instanceActor = await getInstanceActor();
|
||||||
|
if (ctx.params.user === instanceActor.id) {
|
||||||
|
ctx.body = renderActivity(renderKey(instanceActor, await getUserKeypair(instanceActor.id)));
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
setResponseType(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = ctx.params.user;
|
const userId = ctx.params.user;
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
|
@ -150,7 +181,12 @@ router.get('/users/:user/publickey', async ctx => {
|
||||||
|
|
||||||
if (Users.isLocalUser(user)) {
|
if (Users.isLocalUser(user)) {
|
||||||
ctx.body = renderActivity(renderKey(user, keypair));
|
ctx.body = renderActivity(renderKey(user, keypair));
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
|
@ -165,13 +201,30 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
|
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/users/:user', async (ctx, next) => {
|
router.get('/users/:user', async (ctx, next) => {
|
||||||
if (!isActivityPubReq(ctx)) return await next();
|
if (!isActivityPubReq(ctx)) return await next();
|
||||||
|
|
||||||
|
const instanceActor = await getInstanceActor();
|
||||||
|
if (ctx.params.user === instanceActor.id) {
|
||||||
|
await userInfo(ctx, instanceActor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = ctx.params.user;
|
const userId = ctx.params.user;
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
|
@ -186,6 +239,18 @@ router.get('/users/:user', async (ctx, next) => {
|
||||||
router.get('/@:user', async (ctx, next) => {
|
router.get('/@:user', async (ctx, next) => {
|
||||||
if (!isActivityPubReq(ctx)) return await next();
|
if (!isActivityPubReq(ctx)) return await next();
|
||||||
|
|
||||||
|
if (ctx.params.user === 'instance.actor') {
|
||||||
|
const instanceActor = await getInstanceActor();
|
||||||
|
await userInfo(ctx, instanceActor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
usernameLower: ctx.params.user.toLowerCase(),
|
usernameLower: ctx.params.user.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
|
@ -195,6 +260,12 @@ router.get('/@:user', async (ctx, next) => {
|
||||||
await userInfo(ctx, user);
|
await userInfo(ctx, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/actor', async (ctx, next) => {
|
||||||
|
const instanceActor = await getInstanceActor();
|
||||||
|
await userInfo(ctx, instanceActor);
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// emoji
|
// emoji
|
||||||
router.get('/emojis/:emoji', async ctx => {
|
router.get('/emojis/:emoji', async ctx => {
|
||||||
const emoji = await Emojis.findOneBy({
|
const emoji = await Emojis.findOneBy({
|
||||||
|
@ -208,12 +279,23 @@ router.get('/emojis/:emoji', async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = renderActivity(await renderEmoji(emoji));
|
ctx.body = renderActivity(await renderEmoji(emoji));
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// like
|
// like
|
||||||
router.get('/likes/:like', async ctx => {
|
router.get('/likes/:like', async ctx => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const note = await Notes.findOneBy({
|
const note = await Notes.findOneBy({
|
||||||
id: reaction.noteId,
|
id: reaction.noteId,
|
||||||
visibility: In(['public' as const, 'home' as const]),
|
visibility: In(['public' as const, 'home' as const]),
|
||||||
|
@ -232,12 +314,22 @@ router.get('/likes/:like', async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = renderActivity(await renderLike(reaction, note));
|
ctx.body = renderActivity(await renderLike(reaction, note));
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// follow
|
// follow
|
||||||
router.get('/follows/:follower/:followee', async ctx => {
|
router.get('/follows/:follower/:followee', async ctx => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
// This may be used before the follow is completed, so we do not
|
// This may be used before the follow is completed, so we do not
|
||||||
// check if the following exists.
|
// check if the following exists.
|
||||||
|
|
||||||
|
@ -258,7 +350,12 @@ router.get('/follows/:follower/:followee', async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = renderActivity(renderFollow(follower, followee));
|
ctx.body = renderActivity(renderFollow(follower, followee));
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,17 @@ import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-colle
|
||||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||||
import { Users, Notes, UserNotePinings } from '@/models/index.js';
|
import { Users, Notes, UserNotePinings } from '@/models/index.js';
|
||||||
import { setResponseType } from '../activitypub.js';
|
import { setResponseType } from '../activitypub.js';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
|
||||||
export default async (ctx: Router.RouterContext) => {
|
export default async (ctx: Router.RouterContext) => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = ctx.params.user;
|
const userId = ctx.params.user;
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
|
@ -36,6 +45,12 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.body = renderActivity(rendered);
|
ctx.body = renderActivity(rendered);
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,12 +9,20 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
|
||||||
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
||||||
import { Following } from '@/models/entities/following.js';
|
import { Following } from '@/models/entities/following.js';
|
||||||
import { setResponseType } from '../activitypub.js';
|
import { setResponseType } from '../activitypub.js';
|
||||||
|
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
|
||||||
export default async (ctx: Router.RouterContext) => {
|
export default async (ctx: Router.RouterContext) => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = ctx.params.user;
|
const userId = ctx.params.user;
|
||||||
|
|
||||||
const cursor = ctx.request.query.cursor;
|
const cursor = ctx.request.query.cursor;
|
||||||
if (cursor != null && typeof cursor !== 'string') {
|
if (cursor !== null && typeof cursor !== 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
// index page
|
// index page
|
||||||
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||||
ctx.body = renderActivity(rendered);
|
ctx.body = renderActivity(rendered);
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,12 +9,20 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
|
||||||
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
||||||
import { Following } from '@/models/entities/following.js';
|
import { Following } from '@/models/entities/following.js';
|
||||||
import { setResponseType } from '../activitypub.js';
|
import { setResponseType } from '../activitypub.js';
|
||||||
|
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
|
||||||
export default async (ctx: Router.RouterContext) => {
|
export default async (ctx: Router.RouterContext) => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = ctx.params.user;
|
const userId = ctx.params.user;
|
||||||
|
|
||||||
const cursor = ctx.request.query.cursor;
|
const cursor = ctx.request.query.cursor;
|
||||||
if (cursor != null && typeof cursor !== 'string') {
|
if (cursor !== null && typeof cursor !== 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
// index page
|
// index page
|
||||||
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||||
ctx.body = renderActivity(rendered);
|
ctx.body = renderActivity(rendered);
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,25 +14,33 @@ import { Note } from '@/models/entities/note.js';
|
||||||
import { isPureRenote } from '@/misc/renote.js';
|
import { isPureRenote } from '@/misc/renote.js';
|
||||||
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
|
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
|
||||||
import { setResponseType } from '../activitypub.js';
|
import { setResponseType } from '../activitypub.js';
|
||||||
|
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
|
||||||
export default async (ctx: Router.RouterContext) => {
|
export default async (ctx: Router.RouterContext) => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const userId = ctx.params.user;
|
const userId = ctx.params.user;
|
||||||
|
|
||||||
const sinceId = ctx.request.query.since_id;
|
const sinceId = ctx.request.query.since_id;
|
||||||
if (sinceId != null && typeof sinceId !== 'string') {
|
if (sinceId !== null && typeof sinceId !== 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const untilId = ctx.request.query.until_id;
|
const untilId = ctx.request.query.until_id;
|
||||||
if (untilId != null && typeof untilId !== 'string') {
|
if (untilId !== null && typeof untilId !== 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = ctx.request.query.page === 'true';
|
const page = ctx.request.query.page === 'true';
|
||||||
|
|
||||||
if (countIf(x => x != null, [sinceId, untilId]) > 1) {
|
if (countIf(x => x !== null, [sinceId, untilId]) > 1) {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -90,9 +98,15 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||||
);
|
);
|
||||||
ctx.body = renderActivity(rendered);
|
ctx.body = renderActivity(rendered);
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
}
|
}
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set('Cache-Control', 'no-store');
|
||||||
|
} else {
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=180');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,51 +5,59 @@ import authenticate, { AuthenticationError } from './authenticate.js';
|
||||||
import call from './call.js';
|
import call from './call.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
|
|
||||||
export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<void> {
|
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
|
||||||
const body = ctx.is('multipart/form-data')
|
const body = ctx.is('multipart/form-data')
|
||||||
? (ctx.request as any).body
|
? (ctx.request as any).body
|
||||||
: ctx.method === 'GET'
|
: ctx.method === 'GET'
|
||||||
? ctx.query
|
? ctx.query
|
||||||
: ctx.request.body;
|
: ctx.request.body;
|
||||||
|
|
||||||
const error = (e: ApiError): void => {
|
const reply = (x?: any, y?: ApiError) => {
|
||||||
ctx.status = e.httpStatusCode;
|
if (x == null) {
|
||||||
if (e.httpStatusCode === 401) {
|
ctx.status = 204;
|
||||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
} else if (typeof x === 'number' && y) {
|
||||||
}
|
ctx.status = x;
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
error: {
|
error: {
|
||||||
message: e!.message,
|
message: y!.message,
|
||||||
code: e!.code,
|
code: y!.code,
|
||||||
...(e!.info ? { info: e!.info } : {}),
|
id: y!.id,
|
||||||
endpoint: endpoint.name,
|
kind: y!.kind,
|
||||||
|
...(y!.info ? { info: y!.info } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
||||||
|
ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
|
||||||
|
}
|
||||||
|
res();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
||||||
await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => {
|
authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(([user, app]) => {
|
||||||
// API invoking
|
// API invoking
|
||||||
await call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
||||||
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
|
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
|
||||||
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||||
}
|
}
|
||||||
if (res == null) {
|
reply(res);
|
||||||
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) => {
|
}).catch((e: ApiError) => {
|
||||||
error(e);
|
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
||||||
});
|
});
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
if (e instanceof AuthenticationError) {
|
if (e instanceof AuthenticationError) {
|
||||||
error(new ApiError('AUTHENTICATION_FAILED', e.message));
|
ctx.response.status = 403;
|
||||||
|
ctx.response.set('WWW-Authenticate', 'Bearer');
|
||||||
|
ctx.response.body = {
|
||||||
|
message: 'Authentication failed: ' + e.message,
|
||||||
|
code: 'AUTHENTICATION_FAILED',
|
||||||
|
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||||
|
kind: 'client',
|
||||||
|
};
|
||||||
|
res();
|
||||||
} else {
|
} else {
|
||||||
error(new ApiError());
|
reply(500, new ApiError());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
|
@ -3,13 +3,10 @@ import { Users, AccessTokens, Apps } from '@/models/index.js';
|
||||||
import { AccessToken } from '@/models/entities/access-token.js';
|
import { AccessToken } from '@/models/entities/access-token.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { Cache } from '@/misc/cache.js';
|
||||||
import { App } from '@/models/entities/app.js';
|
import { App } from '@/models/entities/app.js';
|
||||||
import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
|
import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
|
||||||
import isNativeToken from './common/is-native-token.js';
|
import isNativeToken from './common/is-native-token.js';
|
||||||
|
|
||||||
const appCache = new Cache<App>(
|
const appCache = new Cache<App>(Infinity);
|
||||||
Infinity,
|
|
||||||
(id) => Apps.findOneByOrFail({ id }),
|
|
||||||
);
|
|
||||||
|
|
||||||
export class AuthenticationError extends Error {
|
export class AuthenticationError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
|
@ -18,8 +15,8 @@ export class AuthenticationError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
export default async (authorization: string | null | undefined, bodyToken: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
|
||||||
let maybeToken: string | null = null;
|
let token: string | null = null;
|
||||||
|
|
||||||
// check if there is an authorization header set
|
// check if there is an authorization header set
|
||||||
if (authorization != null) {
|
if (authorization != null) {
|
||||||
|
@ -30,19 +27,19 @@ export default async (authorization: string | null | undefined, bodyToken: strin
|
||||||
// check if OAuth 2.0 Bearer tokens are being used
|
// check if OAuth 2.0 Bearer tokens are being used
|
||||||
// Authorization schemes are case insensitive
|
// Authorization schemes are case insensitive
|
||||||
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
|
if (authorization.substring(0, 7).toLowerCase() === 'bearer ') {
|
||||||
maybeToken = authorization.substring(7);
|
token = authorization.substring(7);
|
||||||
} else {
|
} else {
|
||||||
throw new AuthenticationError('unsupported authentication scheme');
|
throw new AuthenticationError('unsupported authentication scheme');
|
||||||
}
|
}
|
||||||
} else if (bodyToken != null) {
|
} else if (bodyToken != null) {
|
||||||
maybeToken = bodyToken;
|
token = bodyToken;
|
||||||
} else {
|
} else {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
const token: string = maybeToken;
|
|
||||||
|
|
||||||
if (isNativeToken(token)) {
|
if (isNativeToken(token)) {
|
||||||
const user = await localUserByNativeTokenCache.fetch(token);
|
const user = await localUserByNativeTokenCache.fetch(token,
|
||||||
|
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>);
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new AuthenticationError('unknown token');
|
throw new AuthenticationError('unknown token');
|
||||||
|
@ -66,13 +63,14 @@ export default async (authorization: string | null | undefined, bodyToken: strin
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await userByIdCache.fetch(accessToken.userId);
|
const user = await localUserByIdCache.fetch(accessToken.userId,
|
||||||
|
() => Users.findOneBy({
|
||||||
// can't authorize remote users
|
id: accessToken.userId,
|
||||||
if (!Users.isLocalUser(user)) return [null, null];
|
}) as Promise<ILocalUser>);
|
||||||
|
|
||||||
if (accessToken.appId) {
|
if (accessToken.appId) {
|
||||||
const app = await appCache.fetch(accessToken.appId);
|
const app = await appCache.fetch(accessToken.appId,
|
||||||
|
() => Apps.findOneByOrFail({ id: accessToken.appId! }));
|
||||||
|
|
||||||
return [user, {
|
return [user, {
|
||||||
id: accessToken.id,
|
id: accessToken.id,
|
||||||
|
|
|
@ -7,6 +7,14 @@ import { limiter } from './limiter.js';
|
||||||
import endpoints, { IEndpointMeta } from './endpoints.js';
|
import endpoints, { IEndpointMeta } from './endpoints.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
import { apiLogger } from './logger.js';
|
import { apiLogger } from './logger.js';
|
||||||
|
import { AccessToken } from '@/models/entities/access-token.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.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) => {
|
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 isSecure = user != null && token == null;
|
||||||
|
@ -14,10 +22,17 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||||
|
|
||||||
const ep = endpoints.find(e => e.name === endpoint);
|
const ep = endpoints.find(e => e.name === endpoint);
|
||||||
|
|
||||||
if (ep == null) throw new ApiError('NO_SUCH_ENDPOINT');
|
if (ep == null) {
|
||||||
|
throw new ApiError({
|
||||||
|
message: 'No such endpoint.',
|
||||||
|
code: 'NO_SUCH_ENDPOINT',
|
||||||
|
id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709',
|
||||||
|
httpStatusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (ep.meta.secure && !isSecure) {
|
if (ep.meta.secure && !isSecure) {
|
||||||
throw new ApiError('ACCESS_DENIED', 'This operation can only be performed with a native token.');
|
throw new ApiError(accessDenied);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.limit && !isModerator) {
|
if (ep.meta.limit && !isModerator) {
|
||||||
|
@ -36,29 +51,59 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit
|
// Rate limit
|
||||||
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(() => {
|
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
|
||||||
throw new ApiError('RATE_LIMIT_EXCEEDED');
|
throw new ApiError({
|
||||||
|
message: 'Rate limit exceeded. Please try again later.',
|
||||||
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
|
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||||
|
httpStatusCode: 429,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.requireCredential && user == null) {
|
if (ep.meta.requireCredential && user == null) {
|
||||||
throw new ApiError('AUTHENTICATION_REQUIRED');
|
throw new ApiError({
|
||||||
|
message: 'Credential required.',
|
||||||
|
code: 'CREDENTIAL_REQUIRED',
|
||||||
|
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||||
|
httpStatusCode: 401,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.requireCredential && user!.isSuspended) {
|
if (ep.meta.requireCredential && user!.isSuspended) {
|
||||||
throw new ApiError('SUSPENDED');
|
throw new ApiError({
|
||||||
|
message: 'Your account has been suspended.',
|
||||||
|
code: 'YOUR_ACCOUNT_SUSPENDED',
|
||||||
|
id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
|
||||||
|
httpStatusCode: 403,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.requireAdmin && !user!.isAdmin) {
|
if (ep.meta.requireAdmin && !user!.isAdmin) {
|
||||||
throw new ApiError('ACCESS_DENIED', 'This operation requires administrator privileges.');
|
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.requireModerator && !isModerator) {
|
if (ep.meta.requireModerator && !isModerator) {
|
||||||
throw new ApiError('ACCESS_DENIED', 'This operation requires moderator privileges.');
|
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
|
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
|
||||||
throw new ApiError('ACCESS_DENIED', 'This operation requires privileges which this token does not grant.');
|
throw new ApiError({
|
||||||
|
message: 'Your app does not have the necessary permissions to use this endpoint.',
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// private mode
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.privateMode && ep.meta.requireCredentialPrivateMode && user == null) {
|
||||||
|
throw new ApiError({
|
||||||
|
message: 'Credential required.',
|
||||||
|
code: 'CREDENTIAL_REQUIRED',
|
||||||
|
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||||
|
httpStatusCode: 401
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast non JSON input
|
// Cast non JSON input
|
||||||
|
@ -69,7 +114,11 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||||
try {
|
try {
|
||||||
data[k] = JSON.parse(data[k]);
|
data[k] = JSON.parse(data[k]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ApiError('INVALID_PARAM', {
|
throw new ApiError({
|
||||||
|
message: 'Invalid param.',
|
||||||
|
code: 'INVALID_PARAM',
|
||||||
|
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
|
||||||
|
}, {
|
||||||
param: k,
|
param: k,
|
||||||
reason: `cannot cast to ${param.type}`,
|
reason: `cannot cast to ${param.type}`,
|
||||||
});
|
});
|
||||||
|
@ -93,7 +142,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||||
stack: e.stack,
|
stack: e.stack,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
throw new ApiError('INTERNAL_ERROR', {
|
throw new ApiError(null, {
|
||||||
e: {
|
e: {
|
||||||
message: e.message,
|
message: e.message,
|
||||||
code: e.name,
|
code: e.name,
|
||||||
|
|
|
@ -24,13 +24,25 @@ export async function signup(opts: {
|
||||||
|
|
||||||
// Validate username
|
// Validate username
|
||||||
if (!Users.validateLocalUsername(username)) {
|
if (!Users.validateLocalUsername(username)) {
|
||||||
throw new ApiError('INVALID_USERNAME');
|
throw new ApiError({
|
||||||
|
message: 'This username is invalid.',
|
||||||
|
code: 'INVALID_USERNAME',
|
||||||
|
id: 'ece89f3c-d845-4d9a-850b-1735285e8cd4',
|
||||||
|
kind: 'client',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password != null && passwordHash == null) {
|
if (password != null && passwordHash == null) {
|
||||||
// Validate password
|
// Validate password
|
||||||
if (!Users.validatePassword(password)) {
|
if (!Users.validatePassword(password)) {
|
||||||
throw new ApiError('INVALID_PASSWORD');
|
throw new ApiError({
|
||||||
|
message: 'This password is invalid.',
|
||||||
|
code: 'INVALID_PASSWORD',
|
||||||
|
id: 'a941905b-fe7b-43e2-8ecd-50ad3a2287ab',
|
||||||
|
kind: 'client',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
|
@ -41,14 +53,22 @@ export async function signup(opts: {
|
||||||
// Generate secret
|
// Generate secret
|
||||||
const secret = generateUserToken();
|
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
|
// Check username duplication
|
||||||
if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||||
throw new ApiError('USED_USERNAME');
|
throw new ApiError(duplicateUsernameError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deleted username duplication
|
// Check deleted username duplication
|
||||||
if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) {
|
if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) {
|
||||||
throw new ApiError('USED_USERNAME');
|
throw new ApiError(duplicateUsernameError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyPair = await new Promise<string[]>((res, rej) =>
|
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||||
|
@ -77,7 +97,7 @@ export async function signup(opts: {
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) throw new ApiError('USED_USERNAME');
|
if (exist) throw new ApiError(duplicateUsernameError);
|
||||||
|
|
||||||
account = await transactionalEntityManager.save(new User({
|
account = await transactionalEntityManager.save(new User({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
|
|
@ -28,16 +28,22 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa
|
||||||
fs.unlink(file.path, () => {});
|
fs.unlink(file.path, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta.requireFile && file == null) {
|
if (meta.requireFile && file == null) return Promise.reject(new ApiError({
|
||||||
return Promise.reject(new ApiError('FILE_REQUIRED'));
|
message: 'File required.',
|
||||||
}
|
code: 'FILE_REQUIRED',
|
||||||
|
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
||||||
|
}));
|
||||||
|
|
||||||
const valid = validate(params);
|
const valid = validate(params);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
if (file) cleanup();
|
if (file) cleanup();
|
||||||
|
|
||||||
const errors = validate.errors!;
|
const errors = validate.errors!;
|
||||||
const err = new ApiError('INVALID_PARAM', {
|
const err = new ApiError({
|
||||||
|
message: 'Invalid param.',
|
||||||
|
code: 'INVALID_PARAM',
|
||||||
|
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||||
|
}, {
|
||||||
param: errors[0].schemaPath,
|
param: errors[0].schemaPath,
|
||||||
reason: errors[0].message,
|
reason: errors[0].message,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Schema } from '@/misc/schema.js';
|
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_meta from './endpoints/admin/meta.js';
|
||||||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||||
|
@ -271,12 +270,14 @@ import * as ep___serverInfo from './endpoints/server-info.js';
|
||||||
import * as ep___stats from './endpoints/stats.js';
|
import * as ep___stats from './endpoints/stats.js';
|
||||||
import * as ep___sw_register from './endpoints/sw/register.js';
|
import * as ep___sw_register from './endpoints/sw/register.js';
|
||||||
import * as ep___sw_unregister from './endpoints/sw/unregister.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___username_available from './endpoints/username/available.js';
|
||||||
import * as ep___users from './endpoints/users.js';
|
import * as ep___users from './endpoints/users.js';
|
||||||
import * as ep___users_clips from './endpoints/users/clips.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_followers from './endpoints/users/followers.js';
|
||||||
import * as ep___users_following from './endpoints/users/following.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_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_create from './endpoints/users/groups/create.js';
|
||||||
import * as ep___users_groups_delete from './endpoints/users/groups/delete.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';
|
import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js';
|
||||||
|
@ -579,12 +580,14 @@ const eps = [
|
||||||
['stats', ep___stats],
|
['stats', ep___stats],
|
||||||
['sw/register', ep___sw_register],
|
['sw/register', ep___sw_register],
|
||||||
['sw/unregister', ep___sw_unregister],
|
['sw/unregister', ep___sw_unregister],
|
||||||
|
['test', ep___test],
|
||||||
['username/available', ep___username_available],
|
['username/available', ep___username_available],
|
||||||
['users', ep___users],
|
['users', ep___users],
|
||||||
['users/clips', ep___users_clips],
|
['users/clips', ep___users_clips],
|
||||||
['users/followers', ep___users_followers],
|
['users/followers', ep___users_followers],
|
||||||
['users/following', ep___users_following],
|
['users/following', ep___users_following],
|
||||||
['users/gallery/posts', ep___users_gallery_posts],
|
['users/gallery/posts', ep___users_gallery_posts],
|
||||||
|
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
|
||||||
['users/groups/create', ep___users_groups_create],
|
['users/groups/create', ep___users_groups_create],
|
||||||
['users/groups/delete', ep___users_groups_delete],
|
['users/groups/delete', ep___users_groups_delete],
|
||||||
['users/groups/invitations/accept', ep___users_groups_invitations_accept],
|
['users/groups/invitations/accept', ep___users_groups_invitations_accept],
|
||||||
|
@ -622,7 +625,13 @@ export interface IEndpointMeta {
|
||||||
|
|
||||||
readonly tags?: ReadonlyArray<string>;
|
readonly tags?: ReadonlyArray<string>;
|
||||||
|
|
||||||
readonly errors?: ReadonlyArray<keyof typeof errors>;
|
readonly errors?: {
|
||||||
|
readonly [key: string]: {
|
||||||
|
readonly message: string;
|
||||||
|
readonly code: string;
|
||||||
|
readonly id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
readonly res?: Schema;
|
readonly res?: Schema;
|
||||||
|
|
||||||
|
@ -683,6 +692,12 @@ export interface IEndpointMeta {
|
||||||
*/
|
*/
|
||||||
readonly secure?: boolean;
|
readonly secure?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If in private mode, whether credentials are required when making a request to this endpoint.
|
||||||
|
* If omitted, this is interpreted as false.
|
||||||
|
*/
|
||||||
|
readonly requireCredentialPrivateMode?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* エンドポイントの種類
|
* エンドポイントの種類
|
||||||
* パーミッションの実現に利用されます。
|
* パーミッションの実現に利用されます。
|
||||||
|
|
|
@ -8,7 +8,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_ANNOUNCEMENT'],
|
errors: {
|
||||||
|
noSuchAnnouncement: {
|
||||||
|
message: 'No such announcement.',
|
||||||
|
code: 'NO_SUCH_ANNOUNCEMENT',
|
||||||
|
id: 'ecad8040-a276-4e85-bda9-015a708d291e',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,7 +29,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const announcement = await Announcements.findOneBy({ id: ps.id });
|
const announcement = await Announcements.findOneBy({ id: ps.id });
|
||||||
|
|
||||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||||
|
|
||||||
await Announcements.delete(announcement.id);
|
await Announcements.delete(announcement.id);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_ANNOUNCEMENT'],
|
errors: {
|
||||||
|
noSuchAnnouncement: {
|
||||||
|
message: 'No such announcement.',
|
||||||
|
code: 'NO_SUCH_ANNOUNCEMENT',
|
||||||
|
id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -26,7 +32,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const announcement = await Announcements.findOneBy({ id: ps.id });
|
const announcement = await Announcements.findOneBy({ id: ps.id });
|
||||||
|
|
||||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||||
|
|
||||||
await Announcements.update(announcement.id, {
|
await Announcements.update(announcement.id, {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|
|
@ -8,7 +8,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_FILE'],
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: 'No such file.',
|
||||||
|
code: 'NO_SUCH_FILE',
|
||||||
|
id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -174,7 +180,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
if (file == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_FILE'],
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: 'No such file.',
|
||||||
|
code: 'MO_SUCH_FILE',
|
||||||
|
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -28,7 +34,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const file = await DriveFiles.findOneBy({ id: ps.fileId });
|
const file = await DriveFiles.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (file == null) throw new ApiError('NO_SUCH_FILE');
|
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
|
||||||
const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_EMOJI', 'INTERNAL_ERROR'],
|
errors: {
|
||||||
|
noSuchEmoji: {
|
||||||
|
message: 'No such emoji.',
|
||||||
|
code: 'NO_SUCH_EMOJI',
|
||||||
|
id: 'e2785b66-dca3-4087-9cac-b93c541cc425',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -40,7 +46,9 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await Emojis.findOneBy({ id: ps.emojiId });
|
const emoji = await Emojis.findOneBy({ id: ps.emojiId });
|
||||||
|
|
||||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
if (emoji == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchEmoji);
|
||||||
|
}
|
||||||
|
|
||||||
let driveFile: DriveFile;
|
let driveFile: DriveFile;
|
||||||
|
|
||||||
|
@ -48,7 +56,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
// Create file
|
// Create file
|
||||||
driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
|
driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ApiError('INTERNAL_ERROR', e);
|
throw new ApiError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const copied = await Emojis.insert({
|
const copied = await Emojis.insert({
|
||||||
|
|
|
@ -10,7 +10,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_EMOJI'],
|
errors: {
|
||||||
|
noSuchEmoji: {
|
||||||
|
message: 'No such emoji.',
|
||||||
|
code: 'NO_SUCH_EMOJI',
|
||||||
|
id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -25,7 +31,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await Emojis.findOneBy({ id: ps.id });
|
const emoji = await Emojis.findOneBy({ id: ps.id });
|
||||||
|
|
||||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||||
|
|
||||||
await Emojis.delete(emoji.id);
|
await Emojis.delete(emoji.id);
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_EMOJI'],
|
errors: {
|
||||||
|
noSuchEmoji: {
|
||||||
|
message: 'No such emoji.',
|
||||||
|
code: 'NO_SUCH_EMOJI',
|
||||||
|
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -33,7 +39,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const emoji = await Emojis.findOneBy({ id: ps.id });
|
const emoji = await Emojis.findOneBy({ id: ps.id });
|
||||||
|
|
||||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||||
|
|
||||||
await Emojis.update(emoji.id, {
|
await Emojis.update(emoji.id, {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -152,6 +153,22 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowedHosts: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
privateMode: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
secureMode: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
hcaptchaSecretKey: {
|
hcaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -309,7 +326,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
iconUrl: instance.iconUrl,
|
iconUrl: instance.iconUrl,
|
||||||
backgroundImageUrl: instance.backgroundImageUrl,
|
backgroundImageUrl: instance.backgroundImageUrl,
|
||||||
logoImageUrl: instance.logoImageUrl,
|
logoImageUrl: instance.logoImageUrl,
|
||||||
maxNoteTextLength: config.maxNoteTextLength,
|
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
|
||||||
defaultLightTheme: instance.defaultLightTheme,
|
defaultLightTheme: instance.defaultLightTheme,
|
||||||
defaultDarkTheme: instance.defaultDarkTheme,
|
defaultDarkTheme: instance.defaultDarkTheme,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
|
@ -326,6 +343,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
pinnedUsers: instance.pinnedUsers,
|
pinnedUsers: instance.pinnedUsers,
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
blockedHosts: instance.blockedHosts,
|
blockedHosts: instance.blockedHosts,
|
||||||
|
allowedHosts: instance.allowedHosts,
|
||||||
|
privateMode: instance.privateMode,
|
||||||
|
secureMode: instance.secureMode,
|
||||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||||
proxyAccountId: instance.proxyAccountId,
|
proxyAccountId: instance.proxyAccountId,
|
||||||
|
|
|
@ -9,7 +9,13 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
errors: ['INVALID_URL'],
|
errors: {
|
||||||
|
invalidUrl: {
|
||||||
|
message: 'Invalid URL',
|
||||||
|
code: 'INVALID_URL',
|
||||||
|
id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -52,8 +58,8 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
try {
|
try {
|
||||||
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new ApiError('INVALID_URL', e);
|
throw new ApiError(meta.errors.invalidUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await addRelay(ps.inbox);
|
return await addRelay(ps.inbox);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { Meta } from '@/models/entities/meta.js';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
import { fetchMeta, setMeta } from '@/misc/fetch-meta.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -25,6 +26,11 @@ export const paramDef = {
|
||||||
blockedHosts: { type: 'array', nullable: true, items: {
|
blockedHosts: { type: 'array', nullable: true, items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
allowedHosts: { type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
} },
|
||||||
|
secureMode: { type: 'boolean', nullable: true },
|
||||||
|
privateMode: { type: 'boolean', nullable: true },
|
||||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||||
bannerUrl: { type: 'string', nullable: true },
|
bannerUrl: { type: 'string', nullable: true },
|
||||||
iconUrl: { type: 'string', nullable: true },
|
iconUrl: { type: 'string', nullable: true },
|
||||||
|
@ -130,6 +136,18 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.themeColor = ps.themeColor;
|
set.themeColor = ps.themeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.allowedHosts)) {
|
||||||
|
set.allowedHosts = ps.allowedHosts.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ps.privateMode === 'boolean') {
|
||||||
|
set.privateMode = ps.privateMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ps.secureMode === 'boolean') {
|
||||||
|
set.secureMode = ps.secureMode;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.bannerUrl !== undefined) {
|
if (ps.bannerUrl !== undefined) {
|
||||||
set.bannerUrl = ps.bannerUrl;
|
set.bannerUrl = ps.bannerUrl;
|
||||||
}
|
}
|
||||||
|
@ -374,10 +392,20 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.deeplIsPro = ps.deeplIsPro;
|
set.deeplIsPro = ps.deeplIsPro;
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = await fetchMeta();
|
await db.transaction(async transactionalEntityManager => {
|
||||||
await setMeta({
|
const metas = await transactionalEntityManager.find(Meta, {
|
||||||
...meta,
|
order: {
|
||||||
...set,
|
id: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta = metas[0];
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
await transactionalEntityManager.update(Meta, meta.id, set);
|
||||||
|
} else {
|
||||||
|
await transactionalEntityManager.save(Meta, set);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
insertModerationLog(me, 'updateMeta');
|
insertModerationLog(me, 'updateMeta');
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
|
@ -11,7 +11,19 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: ['NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'],
|
errors: {
|
||||||
|
noSuchUserList: {
|
||||||
|
message: 'No such user list.',
|
||||||
|
code: 'NO_SUCH_USER_LIST',
|
||||||
|
id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchUserGroup: {
|
||||||
|
message: 'No such user group.',
|
||||||
|
code: 'NO_SUCH_USER_GROUP',
|
||||||
|
id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -59,14 +71,18 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userList == null) throw new ApiError('NO_SUCH_USER_LIST');
|
if (userList == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUserList);
|
||||||
|
}
|
||||||
} else if (ps.src === 'group' && ps.userGroupId) {
|
} else if (ps.src === 'group' && ps.userGroupId) {
|
||||||
userGroupJoining = await UserGroupJoinings.findOneBy({
|
userGroupJoining = await UserGroupJoinings.findOneBy({
|
||||||
userGroupId: ps.userGroupId,
|
userGroupId: ps.userGroupId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP');
|
if (userGroupJoining == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUserGroup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const antenna = await Antennas.insert({
|
const antenna = await Antennas.insert({
|
||||||
|
|
|
@ -10,7 +10,13 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: ['NO_SUCH_ANTENNA'],
|
errors: {
|
||||||
|
noSuchAntenna: {
|
||||||
|
message: 'No such antenna.',
|
||||||
|
code: 'NO_SUCH_ANTENNA',
|
||||||
|
id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -28,7 +34,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
if (antenna == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
|
}
|
||||||
|
|
||||||
await Antennas.delete(antenna.id);
|
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 { Antennas, Notes, AntennaNotes } from '@/models/index.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
||||||
|
@ -14,7 +14,13 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'read:account',
|
kind: 'read:account',
|
||||||
|
|
||||||
errors: ['NO_SUCH_ANTENNA'],
|
errors: {
|
||||||
|
noSuchAntenna: {
|
||||||
|
message: 'No such antenna.',
|
||||||
|
code: 'NO_SUCH_ANTENNA',
|
||||||
|
id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
@ -47,7 +53,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
if (antenna == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
|
}
|
||||||
|
|
||||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
|
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
|
|
@ -9,7 +9,13 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'read:account',
|
kind: 'read:account',
|
||||||
|
|
||||||
errors: ['NO_SUCH_ANTENNA'],
|
errors: {
|
||||||
|
noSuchAntenna: {
|
||||||
|
message: 'No such antenna.',
|
||||||
|
code: 'NO_SUCH_ANTENNA',
|
||||||
|
id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -34,7 +40,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
if (antenna == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
|
}
|
||||||
|
|
||||||
return await Antennas.pack(antenna);
|
return await Antennas.pack(antenna);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,25 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: ['NO_SUCH_ANTENNA', 'NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'],
|
errors: {
|
||||||
|
noSuchAntenna: {
|
||||||
|
message: 'No such antenna.',
|
||||||
|
code: 'NO_SUCH_ANTENNA',
|
||||||
|
id: '10c673ac-8852-48eb-aa1f-f5b67f069290',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchUserList: {
|
||||||
|
message: 'No such user list.',
|
||||||
|
code: 'NO_SUCH_USER_LIST',
|
||||||
|
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchUserGroup: {
|
||||||
|
message: 'No such user group.',
|
||||||
|
code: 'NO_SUCH_USER_GROUP',
|
||||||
|
id: '109ed789-b6eb-456e-b8a9-6059d567d385',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -56,7 +74,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (antenna == null) throw new ApiError('NO_SUCH_ANTENNA');
|
if (antenna == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchAntenna);
|
||||||
|
}
|
||||||
|
|
||||||
let userList;
|
let userList;
|
||||||
let userGroupJoining;
|
let userGroupJoining;
|
||||||
|
@ -67,14 +87,18 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userList == null) throw new ApiError('NO_SUCH_USER_LIST');
|
if (userList == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUserList);
|
||||||
|
}
|
||||||
} else if (ps.src === 'group' && ps.userGroupId) {
|
} else if (ps.src === 'group' && ps.userGroupId) {
|
||||||
userGroupJoining = await UserGroupJoinings.findOneBy({
|
userGroupJoining = await UserGroupJoinings.findOneBy({
|
||||||
userGroupId: ps.userGroupId,
|
userGroupId: ps.userGroupId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP');
|
if (userGroupJoining == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUserGroup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Antennas.update(antenna.id, {
|
await Antennas.update(antenna.id, {
|
||||||
|
|
|
@ -12,6 +12,9 @@ export const meta = {
|
||||||
max: 30,
|
max: 30,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -24,7 +24,13 @@ export const meta = {
|
||||||
max: 30,
|
max: 30,
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_OBJECT'],
|
errors: {
|
||||||
|
noSuchObject: {
|
||||||
|
message: 'No such object.',
|
||||||
|
code: 'NO_SUCH_OBJECT',
|
||||||
|
id: 'dc94d745-1262-4e63-a17d-fecaa57efc82',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -77,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
if (object) {
|
if (object) {
|
||||||
return object;
|
return object;
|
||||||
} else {
|
} else {
|
||||||
throw new ApiError('NO_SUCH_OBJECT');
|
throw new ApiError(meta.errors.noSuchObject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Apps } from '@/models/index.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { unique } from '@/prelude/array.js';
|
import { unique } from '@/prelude/array.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { kinds } from '@/misc/api-permissions.js';
|
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -22,14 +21,10 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: 'string' },
|
name: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
permission: {
|
permission: { type: 'array', uniqueItems: true, items: {
|
||||||
type: 'array',
|
|
||||||
uniqueItems: true,
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: kinds,
|
// FIXME: add enum of possible permissions
|
||||||
},
|
} },
|
||||||
},
|
|
||||||
callbackUrl: { type: 'string', nullable: true },
|
callbackUrl: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['name', 'description', 'permission'],
|
required: ['name', 'description', 'permission'],
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { ApiError } from '../../error.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['app'],
|
tags: ['app'],
|
||||||
|
|
||||||
errors: ['NO_SUCH_APP'],
|
errors: {
|
||||||
|
noSuchApp: {
|
||||||
|
message: 'No such app.',
|
||||||
|
code: 'NO_SUCH_APP',
|
||||||
|
id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -27,12 +33,14 @@ export default define(meta, paramDef, async (ps, user, token) => {
|
||||||
const isSecure = user != null && token == null;
|
const isSecure = user != null && token == null;
|
||||||
|
|
||||||
// Lookup app
|
// Lookup app
|
||||||
const app = await Apps.findOneBy({ id: ps.appId });
|
const ap = await Apps.findOneBy({ id: ps.appId });
|
||||||
|
|
||||||
if (app == null) throw new ApiError('NO_SUCH_APP');
|
if (ap == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchApp);
|
||||||
|
}
|
||||||
|
|
||||||
return await Apps.pack(app, user, {
|
return await Apps.pack(ap, user, {
|
||||||
detail: true,
|
detail: true,
|
||||||
includeSecret: isSecure && (app.userId === user!.id),
|
includeSecret: isSecure && (ap.userId === user!.id),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,13 @@ export const meta = {
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_SESSION'],
|
errors: {
|
||||||
|
noSuchSession: {
|
||||||
|
message: 'No such session.',
|
||||||
|
code: 'NO_SUCH_SESSION',
|
||||||
|
id: '9c72d8de-391a-43c1-9d06-08d29efde8df',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -29,7 +35,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const session = await AuthSessions
|
const session = await AuthSessions
|
||||||
.findOneBy({ token: ps.token });
|
.findOneBy({ token: ps.token });
|
||||||
|
|
||||||
if (session == null) throw new ApiError('NO_SUCH_SESSION');
|
if (session == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchSession);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate access token
|
// Generate access token
|
||||||
const accessToken = secureRndstr(32, true);
|
const accessToken = secureRndstr(32, true);
|
||||||
|
|
|
@ -26,7 +26,13 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_APP'],
|
errors: {
|
||||||
|
noSuchApp: {
|
||||||
|
message: 'No such app.',
|
||||||
|
code: 'NO_SUCH_APP',
|
||||||
|
id: '92f93e63-428e-4f2f-a5a4-39e1407fe998',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -45,7 +51,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (app == null) {
|
if (app == null) {
|
||||||
throw new ApiError('NO_SUCH_APP');
|
throw new ApiError(meta.errors.noSuchApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate token
|
// Generate token
|
||||||
|
|
|
@ -7,7 +7,13 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
errors: ['NO_SUCH_SESSION'],
|
errors: {
|
||||||
|
noSuchSession: {
|
||||||
|
message: 'No such session.',
|
||||||
|
code: 'NO_SUCH_SESSION',
|
||||||
|
id: 'bd72c97d-eba7-4adb-a467-f171b8847250',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -46,7 +52,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
token: ps.token,
|
token: ps.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (session == null) throw new ApiError('NO_SUCH_SESSION');
|
if (session == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchSession);
|
||||||
|
}
|
||||||
|
|
||||||
return await AuthSessions.pack(session, user);
|
return await AuthSessions.pack(session, user);
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,25 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_APP', 'NO_SUCH_SESSION', 'PENDING_SESSION'],
|
errors: {
|
||||||
|
noSuchApp: {
|
||||||
|
message: 'No such app.',
|
||||||
|
code: 'NO_SUCH_APP',
|
||||||
|
id: 'fcab192a-2c5a-43b7-8ad8-9b7054d8d40d',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchSession: {
|
||||||
|
message: 'No such session.',
|
||||||
|
code: 'NO_SUCH_SESSION',
|
||||||
|
id: '5b5a1503-8bc8-4bd0-8054-dc189e8cdcb3',
|
||||||
|
},
|
||||||
|
|
||||||
|
pendingSession: {
|
||||||
|
message: 'This session is not completed yet.',
|
||||||
|
code: 'PENDING_SESSION',
|
||||||
|
id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -43,7 +61,9 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
secret: ps.appSecret,
|
secret: ps.appSecret,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (app == null) throw new ApiError('NO_SUCH_APP');
|
if (app == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchApp);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch token
|
// Fetch token
|
||||||
const session = await AuthSessions.findOneBy({
|
const session = await AuthSessions.findOneBy({
|
||||||
|
@ -51,9 +71,13 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
appId: app.id,
|
appId: app.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (session == null) throw new ApiError('NO_SUCH_SESSION');
|
if (session == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchSession);
|
||||||
|
}
|
||||||
|
|
||||||
if (session.userId == null) throw new ApiError('PENDING_SESSION');
|
if (session.userId == null) {
|
||||||
|
throw new ApiError(meta.errors.pendingSession);
|
||||||
|
}
|
||||||
|
|
||||||
// Lookup access token
|
// Lookup access token
|
||||||
const accessToken = await AccessTokens.findOneByOrFail({
|
const accessToken = await AccessTokens.findOneByOrFail({
|
||||||
|
|
|
@ -17,7 +17,25 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:blocks',
|
kind: 'write:blocks',
|
||||||
|
|
||||||
errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'ALREADY_BLOCKING'],
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e',
|
||||||
|
},
|
||||||
|
|
||||||
|
blockeeIsYourself: {
|
||||||
|
message: 'Blockee is yourself.',
|
||||||
|
code: 'BLOCKEE_IS_YOURSELF',
|
||||||
|
id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6',
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyBlocking: {
|
||||||
|
message: 'You are already blocking that user.',
|
||||||
|
code: 'ALREADY_BLOCKING',
|
||||||
|
id: '787fed64-acb9-464a-82eb-afbd745b9614',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -39,11 +57,13 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const blocker = await Users.findOneByOrFail({ id: user.id });
|
const blocker = await Users.findOneByOrFail({ id: user.id });
|
||||||
|
|
||||||
// 自分自身
|
// 自分自身
|
||||||
if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF');
|
if (user.id === ps.userId) {
|
||||||
|
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||||
|
}
|
||||||
|
|
||||||
// Get blockee
|
// Get blockee
|
||||||
const blockee = await getUser(ps.userId).catch(e => {
|
const blockee = await getUser(ps.userId).catch(e => {
|
||||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_BLOCKING');
|
if (exist != null) {
|
||||||
|
throw new ApiError(meta.errors.alreadyBlocking);
|
||||||
|
}
|
||||||
|
|
||||||
await create(blocker, blockee);
|
await create(blocker, blockee);
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,25 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:blocks',
|
kind: 'write:blocks',
|
||||||
|
|
||||||
errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'NOT_BLOCKING'],
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '8621d8bf-c358-4303-a066-5ea78610eb3f',
|
||||||
|
},
|
||||||
|
|
||||||
|
blockeeIsYourself: {
|
||||||
|
message: 'Blockee is yourself.',
|
||||||
|
code: 'BLOCKEE_IS_YOURSELF',
|
||||||
|
id: '06f6fac6-524b-473c-a354-e97a40ae6eac',
|
||||||
|
},
|
||||||
|
|
||||||
|
notBlocking: {
|
||||||
|
message: 'You are not blocking that user.',
|
||||||
|
code: 'NOT_BLOCKING',
|
||||||
|
id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -36,14 +54,16 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
// Check if the blockee is yourself
|
|
||||||
if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF');
|
|
||||||
|
|
||||||
const blocker = await Users.findOneByOrFail({ id: user.id });
|
const blocker = await Users.findOneByOrFail({ id: user.id });
|
||||||
|
|
||||||
|
// Check if the blockee is yourself
|
||||||
|
if (user.id === ps.userId) {
|
||||||
|
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||||
|
}
|
||||||
|
|
||||||
// Get blockee
|
// Get blockee
|
||||||
const blockee = await getUser(ps.userId).catch(e => {
|
const blockee = await getUser(ps.userId).catch(e => {
|
||||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER');
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,7 +73,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) throw new ApiError('NOT_BLOCKING');
|
if (exist == null) {
|
||||||
|
throw new ApiError(meta.errors.notBlocking);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete blocking
|
// Delete blocking
|
||||||
await deleteBlocking(blocker, blockee);
|
await deleteBlocking(blocker, blockee);
|
||||||
|
|
|
@ -17,7 +17,13 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_FILE'],
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: 'No such file.',
|
||||||
|
code: 'NO_SUCH_FILE',
|
||||||
|
id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -39,7 +45,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (banner == null) throw new ApiError('NO_SUCH_FILE');
|
if (banner == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = await Channels.insert({
|
const channel = await Channels.insert({
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
||||||
tags: ['channels'],
|
tags: ['channels'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
|
@ -11,7 +11,13 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
errors: ['NO_SUCH_CHANNEL'],
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: 'c0031718-d573-4e85-928e-10039f1fbb68',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -28,7 +34,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
await ChannelFollowings.insert({
|
await ChannelFollowings.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
||||||
tags: ['channels'],
|
tags: ['channels'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -13,7 +14,13 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_CHANNEL'],
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: '6f6c314b-7486-4897-8966-c04a66a02923',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -30,7 +37,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
return await Channels.pack(channel, me);
|
return await Channels.pack(channel, me);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
||||||
tags: ['notes', 'channels'],
|
tags: ['notes', 'channels'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
@ -19,7 +20,13 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_CHANNEL'],
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -41,7 +48,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
|
|
@ -10,7 +10,13 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
errors: ['NO_SUCH_CHANNEL'],
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -27,7 +33,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
await ChannelFollowings.delete({
|
await ChannelFollowings.delete({
|
||||||
followerId: user.id,
|
followerId: user.id,
|
||||||
|
|
|
@ -15,7 +15,25 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: ['ACCESS_DENIED', 'NO_SUCH_CHANNEL', 'NO_SUCH_FILE'],
|
errors: {
|
||||||
|
noSuchChannel: {
|
||||||
|
message: 'No such channel.',
|
||||||
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512',
|
||||||
|
},
|
||||||
|
|
||||||
|
accessDenied: {
|
||||||
|
message: 'You do not have edit privilege of the channel.',
|
||||||
|
code: 'ACCESS_DENIED',
|
||||||
|
id: '1fb7cb09-d46a-4fdf-b8df-057788cce513',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchFile: {
|
||||||
|
message: 'No such file.',
|
||||||
|
code: 'NO_SUCH_FILE',
|
||||||
|
id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -35,9 +53,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel == null) throw new ApiError('NO_SUCH_CHANNEL');
|
if (channel == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
|
}
|
||||||
|
|
||||||
if (channel.userId !== me.id) throw new ApiError('ACCESS_DENIED', 'You are not the owner of this channel.');
|
if (channel.userId !== me.id) {
|
||||||
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint:disable-next-line:no-unnecessary-initializer
|
// eslint:disable-next-line:no-unnecessary-initializer
|
||||||
let banner = undefined;
|
let banner = undefined;
|
||||||
|
@ -47,7 +69,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (banner == null) throw new ApiError('NO_SUCH_FILE');
|
if (banner == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
}
|
||||||
} else if (ps.bannerId === null) {
|
} else if (ps.bannerId === null) {
|
||||||
banner = null;
|
banner = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users'],
|
tags: ['charts', 'users'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(activeUsersChart.schema),
|
res: getJsonSchema(activeUsersChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts'],
|
tags: ['charts'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(apRequestChart.schema),
|
res: getJsonSchema(apRequestChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'drive'],
|
tags: ['charts', 'drive'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(driveChart.schema),
|
res: getJsonSchema(driveChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts'],
|
tags: ['charts'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(federationChart.schema),
|
res: getJsonSchema(federationChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'hashtags'],
|
tags: ['charts', 'hashtags'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(hashtagChart.schema),
|
res: getJsonSchema(hashtagChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts'],
|
tags: ['charts'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(instanceChart.schema),
|
res: getJsonSchema(instanceChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'notes'],
|
tags: ['charts', 'notes'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(notesChart.schema),
|
res: getJsonSchema(notesChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'drive', 'users'],
|
tags: ['charts', 'drive', 'users'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(perUserDriveChart.schema),
|
res: getJsonSchema(perUserDriveChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users', 'following'],
|
tags: ['charts', 'users', 'following'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(perUserFollowingChart.schema),
|
res: getJsonSchema(perUserFollowingChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users', 'notes'],
|
tags: ['charts', 'users', 'notes'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(perUserNotesChart.schema),
|
res: getJsonSchema(perUserNotesChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users', 'reactions'],
|
tags: ['charts', 'users', 'reactions'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(perUserReactionsChart.schema),
|
res: getJsonSchema(perUserReactionsChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users'],
|
tags: ['charts', 'users'],
|
||||||
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: getJsonSchema(usersChart.schema),
|
res: getJsonSchema(usersChart.schema),
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,25 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: ['ALREADY_CLIPPED', 'NO_SUCH_CLIP', 'NO_SUCH_NOTE'],
|
errors: {
|
||||||
|
noSuchClip: {
|
||||||
|
message: 'No such clip.',
|
||||||
|
code: 'NO_SUCH_CLIP',
|
||||||
|
id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchNote: {
|
||||||
|
message: 'No such note.',
|
||||||
|
code: 'NO_SUCH_NOTE',
|
||||||
|
id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b',
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyClipped: {
|
||||||
|
message: 'The note has already been clipped.',
|
||||||
|
code: 'ALREADY_CLIPPED',
|
||||||
|
id: '734806c4-542c-463a-9311-15c512803965',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -30,10 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
if (clip == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
|
}
|
||||||
|
|
||||||
const note = await getNote(ps.noteId, user).catch(err => {
|
const note = await getNote(ps.noteId, user).catch(err => {
|
||||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE');
|
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,7 +62,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_CLIPPED');
|
if (exist != null) {
|
||||||
|
throw new ApiError(meta.errors.alreadyClipped);
|
||||||
|
}
|
||||||
|
|
||||||
await ClipNotes.insert({
|
await ClipNotes.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
|
|
@ -9,7 +9,13 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: ['NO_SUCH_CLIP'],
|
errors: {
|
||||||
|
noSuchClip: {
|
||||||
|
message: 'No such clip.',
|
||||||
|
code: 'NO_SUCH_CLIP',
|
||||||
|
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -27,7 +33,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clip == null) throw new ApiError('NO_SUCH_CLIP');
|
if (clip == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
|
}
|
||||||
|
|
||||||
await Clips.delete(clip.id);
|
await Clips.delete(clip.id);
|
||||||
});
|
});
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue