diff --git a/.config/ci.yml b/.config/ci.yml index d20ede8d35..8730ccab3a 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -229,3 +229,8 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index d8013a1c95..342b0f43da 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -222,3 +222,8 @@ allowedPrivateNetworks: [ # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 5fac3dc41e..ce2daf3aec 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -312,3 +312,8 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/example.yml b/.config/example.yml index 0062b6670c..9debb3bf70 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -99,10 +99,10 @@ db: port: 5432 # Database name - db: misskey + db: sharkey # Auth - user: example-misskey-user + user: sharkey pass: example-misskey-pass # Whether disable Caching queries @@ -334,3 +334,8 @@ checkActivityPubGetSignature: false # PID File of master process #pidFile: /tmp/misskey.pid + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index 6914647570..a909067269 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -3,27 +3,33 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -**What happened?** _(Please give us a brief description of what happened.)_ +# **What happened?** + -**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_ +# **What did you expect to happen?** + -**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ +# **Version** + -**Instance** _(What instance of Sharkey are you using?)_ +# **Instance** + -**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_ +# **What type of issue is this?** + -**What browser are you using? (Client-side issues only)** +# **What browser are you using? (Client-side issues only)** -**What operating system are you using? (Client-side issues only)** +# **What operating system are you using? (Client-side issues only)** -**How do you deploy Sharkey on your server? (Server-side issues only)** +# **How do you deploy Sharkey on your server? (Server-side issues only)** -**What operating system are you using? (Server-side issues only)** +# **What operating system are you using? (Server-side issues only)** -**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_ +# **Relevant log output** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index d4235eb5a3..a77f9335fe 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -3,15 +3,19 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_ +# **What feature would you like implemented?** + -**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_ +# **Why should we add this feature?** + -**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ +# **Version** + -**Instance** _(What instance of Sharkey are you using?)_ +# **Instance** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 18bffa5419..e6977def70 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,11 +1,12 @@ -**What does this PR do?** _(Please give us a brief description of what this PR does.)_ +# **What does this MR do?** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have made sure to test this pull request +- [ ] I have made sure to test this merge request diff --git a/Dockerfile b/Dockerfile index acef95deab..abee7fb098 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,8 @@ RUN apk add ffmpeg tini jemalloc \ && corepack enable \ && addgroup -g "${GID}" sharkey \ && adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \ + && mkdir /sharkey/files \ + && chown sharkey:sharkey /sharkey/files \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \; diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index 8bebd4eb34..c941de6643 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -1,5 +1,38 @@ # Upgrade Notes +## 2024.10.0 + +### Hellspawns + +Sharkey versions before 2024.10 suffered from a bug in the "Mark instance as NSFW" feature. +When a user from such an instance boosted a note, the boost would be converted to a hellspawn (pure renote with Content Warning). +Hellspawns are buggy and do not properly federate, so it may be desirable to correct any that already exist in the database. +The following script will correct any local or remote hellspawns in the database. + +```postgresql +/* Remove "instance is marked as NSFW" hellspawns */ +UPDATE "note" +SET "cw" = null +WHERE + "renoteId" IS NOT NULL + AND "text" IS NULL + AND "cw" = 'Instance is marked as NSFW' + AND "replyId" IS NULL + AND "hasPoll" = false + AND "fileIds" = '{}'; + +/* Fix legacy / user-created hellspawns */ +UPDATE "note" +SET "text" = '.' +WHERE + "renoteId" IS NOT NULL + AND "text" IS NULL + AND "cw" IS NOT NULL + AND "replyId" IS NULL + AND "hasPoll" = false + AND "fileIds" = '{}'; +``` + ## 2024.9.0 ### Following Feed diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..9de322090e --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..e32d28bea5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: { + + nixosConfigurations.container = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = + [ + ( import ./sharkey-service.nix ) + ({ pkgs, ... }: { + boot.isContainer = true; + + # Let 'nixos-version --json' know about the Git revision + # of this flake. + system.configurationRevision = nixpkgs.lib.mkIf (self ? rev) self.rev; + + # Network configuration. + networking.useDHCP = false; + networking.firewall.allowedTCPPorts = [ 3000 ]; + + system.stateVersion = "24.04"; + + services.sharkey = { + enable = true; + package = (pkgs.callPackage ./sharkey.nix {}); + settings = { + url = "https://sharkey.localhost"; + }; + redis.createLocally = true; + database.createLocally = true; + }; + }) + ]; + }; + + }; +} diff --git a/locales/index.d.ts b/locales/index.d.ts index d1cb1f97ea..d8eb90f4e5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9661,6 +9661,10 @@ export interface Locale extends ILocale { * ロールタイムライン */ "roleTimeline": string; + /** + * Following + */ + "following": string; }; }; "_dialog": { @@ -11374,6 +11378,10 @@ export interface Locale extends ILocale { * Remote followers may have incomplete or outdated activity */ "remoteFollowersWarning": string; + /** + * Select a follow relationship... + */ + "selectFollowRelationship": string; } declare const locales: { [lang: string]: Locale; diff --git a/package.json b/package.json index 2ba3881756..02782e5388 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.9.2", + "version": "2024.10.0-dev", "codename": "shonk", "repository": { "type": "git", @@ -66,13 +66,15 @@ "esbuild": "0.23.1", "glob": "11.0.0" }, + "optionalDependencies": { + "cypress": "13.14.2" + }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.0.3", "@types/node": "20.14.12", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "cross-env": "7.0.3", - "cypress": "13.14.2", "eslint": "9.8.0", "globals": "15.9.0", "ncp": "2.0.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 19547c5033..00baef56d8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -93,6 +93,7 @@ "@swc/core": "1.6.6", "@transfem-org/sfm-js": "0.24.5", "@twemoji/parser": "15.1.1", + "@types/psl": "^1.1.3", "accepts": "1.3.8", "ajv": "8.17.1", "archiver": "7.0.1", @@ -135,9 +136,9 @@ "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", + "juice": "11.0.0", "megalodon": "workspace:*", "meilisearch": "0.42.0", - "juice": "11.0.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", @@ -158,6 +159,7 @@ "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "proxy-addr": "^2.0.7", + "psl": "^1.13.0", "pug": "3.0.3", "punycode": "2.3.1", "qrcode": "1.5.4", diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index f33a450325..17b198ef62 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -57,4 +57,4 @@ const promises = Array connectToPostgres() ]); -await Promise.allSettled(promises); +await Promise.all(promises); diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 3dc49c7eb6..9cac058d96 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -115,6 +115,7 @@ type Source = { }; pidFile: string; + filePermissionBits?: string; }; export type Config = { @@ -212,6 +213,7 @@ export type Config = { } | undefined; pidFile: string; + filePermissionBits?: string; }; const _filename = fileURLToPath(import.meta.url); @@ -347,6 +349,7 @@ export function loadConfig(): Config { deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), import: config.import, pidFile: config.pidFile, + filePermissionBits: config.filePermissionBits, }; } @@ -452,7 +455,10 @@ function applyEnvOverrides(config: Source) { } } - const alwaysStrings = { 'chmodSocket': true } as { [key: string]: boolean }; + const alwaysStrings: { [key in string]?: boolean } = { + 'chmodSocket': true, + 'filePermissionBits': true, + }; function _assign(path: (string | number)[], lastStep: string | number, value: string) { let thisConfig = config as any; @@ -490,7 +496,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); - _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]); + _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]); } diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 0e992f05de..05b9e64a37 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -6,7 +6,6 @@ import * as fs from 'node:fs'; import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; -import ipaddr from 'ipaddr.js'; import chalk from 'chalk'; import got, * as Got from 'got'; import { parse } from 'content-disposition'; @@ -70,13 +69,6 @@ export class DownloadService { }, enableUnixSockets: false, }).on('response', (res: Got.Response) => { - if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) { - if (this.isPrivateIp(res.ip)) { - this.logger.warn(`Blocked address: ${res.ip}`); - req.destroy(); - } - } - const contentLength = res.headers['content-length']; if (contentLength != null) { const size = Number(contentLength); @@ -139,18 +131,4 @@ export class DownloadService { cleanup(); } } - - @bindThis - private isPrivateIp(ip: string): boolean { - const parsedIp = ipaddr.parse(ip); - - for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { - return false; - } - } - - return parsedIp.range() !== 'unicast'; - } } diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index a176474b95..da198d0e42 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -312,6 +312,7 @@ export class EmailService { Accept: 'application/json', Authorization: truemailAuthKey, }, + isLocalAddressAllowed: true, }); const json = (await res.json()) as { diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index bea5dee6ab..083153940a 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -6,6 +6,7 @@ import * as http from 'node:http'; import * as https from 'node:https'; import * as net from 'node:net'; +import ipaddr from 'ipaddr.js'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @@ -25,8 +26,102 @@ export type HttpRequestSendOptions = { validators?: ((res: Response) => void)[]; }; +declare module 'node:http' { + interface Agent { + createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket; + } +} + +class HttpRequestServiceAgent extends http.Agent { + constructor( + private config: Config, + options?: http.AgentOptions, + ) { + super(options); + } + + @bindThis + public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { + const socket = super.createConnection(options, callback) + .on('connect', () => { + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } + } + }); + return socket; + } + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } +} + +class HttpsRequestServiceAgent extends https.Agent { + constructor( + private config: Config, + options?: https.AgentOptions, + ) { + super(options); + } + + @bindThis + public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { + const socket = super.createConnection(options, callback) + .on('connect', () => { + const address = socket.remoteAddress; + if (process.env.NODE_ENV === 'production') { + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } + } + }); + return socket; + } + + @bindThis + private isPrivateIp(ip: string): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const net of this.config.allowedPrivateNetworks ?? []) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { + return false; + } + } + + return parsedIp.range() !== 'unicast'; + } +} + @Injectable() export class HttpRequestService { + /** + * Get http non-proxy agent (without local address filtering) + */ + private httpNative: http.Agent; + + /** + * Get https non-proxy agent (without local address filtering) + */ + private httpsNative: https.Agent; + /** * Get http non-proxy agent */ @@ -57,19 +152,20 @@ export class HttpRequestService { lookup: false, // nativeのdns.lookupにfallbackしない }); - this.http = new http.Agent({ + const agentOption = { keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup as unknown as net.LookupFunction, localAddress: config.outgoingAddress, - }); + }; - this.https = new https.Agent({ - keepAlive: true, - keepAliveMsecs: 30 * 1000, - lookup: cache.lookup as unknown as net.LookupFunction, - localAddress: config.outgoingAddress, - }); + this.httpNative = new http.Agent(agentOption); + + this.httpsNative = new https.Agent(agentOption); + + this.http = new HttpRequestServiceAgent(config, agentOption); + + this.https = new HttpsRequestServiceAgent(config, agentOption); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); @@ -104,16 +200,22 @@ export class HttpRequestService { * @param bypassProxy Allways bypass proxy */ @bindThis - public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { + public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent { if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) { + if (isLocalAddressAllowed) { + return url.protocol === 'http:' ? this.httpNative : this.httpsNative; + } return url.protocol === 'http:' ? this.http : this.https; } else { + if (isLocalAddressAllowed && (!this.config.proxy)) { + return url.protocol === 'http:' ? this.httpNative : this.httpsNative; + } return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent; } } @bindThis - public async getActivityJson(url: string): Promise { + public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise { const res = await this.send(url, { method: 'GET', headers: { @@ -121,6 +223,7 @@ export class HttpRequestService { }, timeout: 5000, size: 1024 * 256, + isLocalAddressAllowed: isLocalAddressAllowed, }, { throwErrorWhenResponseNotOk: true, validators: [validateContentTypeSetAsActivityPub], @@ -129,13 +232,13 @@ export class HttpRequestService { const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; - assertActivityMatchesUrls(activity, [url, finalUrl]); + assertActivityMatchesUrls(activity, [finalUrl]); return activity; } @bindThis - public async getJson(url: string, accept = 'application/json, */*', headers?: Record): Promise { + public async getJson(url: string, accept = 'application/json, */*', headers?: Record, isLocalAddressAllowed = false): Promise { const res = await this.send(url, { method: 'GET', headers: Object.assign({ @@ -143,19 +246,21 @@ export class HttpRequestService { }, headers ?? {}), timeout: 5000, size: 1024 * 256, + isLocalAddressAllowed: isLocalAddressAllowed, }); return await res.json() as T; } @bindThis - public async getHtml(url: string, accept = 'text/html, */*', headers?: Record): Promise { + public async getHtml(url: string, accept = 'text/html, */*', headers?: Record, isLocalAddressAllowed = false): Promise { const res = await this.send(url, { method: 'GET', headers: Object.assign({ Accept: accept, }, headers ?? {}), timeout: 5000, + isLocalAddressAllowed: isLocalAddressAllowed, }); return await res.text(); @@ -170,6 +275,7 @@ export class HttpRequestService { headers?: Record, timeout?: number, size?: number, + isLocalAddressAllowed?: boolean, } = {}, extra: HttpRequestSendOptions = { throwErrorWhenResponseNotOk: true, @@ -183,6 +289,8 @@ export class HttpRequestService { controller.abort(); }, timeout); + const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false; + const res = await fetch(url, { method: args.method ?? 'GET', headers: { @@ -191,7 +299,7 @@ export class HttpRequestService { }, body: args.body, size: args.size ?? 10 * 1024 * 1024, - agent: (url) => this.getAgentByUrl(url), + agent: (url) => this.getAgentByUrl(url, false, isLocalAddressAllowed), signal: controller.signal, }); diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index f7371f8e79..b00c5796d2 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -4,7 +4,7 @@ */ import * as fs from 'node:fs'; -import { copyFile, mkdir, unlink, writeFile } from 'node:fs/promises'; +import { copyFile, unlink, writeFile, chmod } from 'node:fs/promises'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -41,12 +41,20 @@ export class InternalStorageService { @bindThis public async saveFromPath(key: string, srcPath: string): Promise { await copyFile(srcPath, this.resolvePath(key)); - return `${this.config.url}/files/${key}`; + return await this.finalizeSavedFile(key); } @bindThis public async saveFromBuffer(key: string, data: Buffer): Promise { await writeFile(this.resolvePath(key), data); + return await this.finalizeSavedFile(key); + } + + private async finalizeSavedFile(key: string): Promise { + if (this.config.filePermissionBits) { + const path = this.resolvePath(key); + await chmod(path, this.config.filePermissionBits); + } return `${this.config.url}/files/${key}`; } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1bc4599a60..35a2d8e290 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -146,6 +146,8 @@ type Option = { app?: MiApp | null; }; +export type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] }); + @Injectable() export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); @@ -412,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (user.host && !data.cw) { await this.federatedInstanceService.fetch(user.host).then(async i => { - if (i.isNSFW) { + if (i.isNSFW && !this.isPureRenote(data)) { data.cw = 'Instance is marked as NSFW'; } }); @@ -627,6 +629,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.queueService.endedPollNotificationQueue.add(note.id, { noteId: note.id, }, { + jobId: `pollEnd:${note.id}`, delay, removeOnComplete: true, }); @@ -821,6 +824,11 @@ export class NoteCreateService implements OnApplicationShutdown { if (!user.noindex) this.index(note); } + @bindThis + public isPureRenote(note: Option): note is PureRenoteOption { + return this.isRenote(note) && !this.isQuote(note); + } + @bindThis private isRenote(note: Option): note is Option & { renote: MiNote } { return note.renote != null; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index d31958e5d4..406d134420 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -442,7 +442,7 @@ export class NoteEditService implements OnApplicationShutdown { if (user.host && !data.cw) { await this.federatedInstanceService.fetch(user.host).then(async i => { - if (i.isNSFW) { + if (i.isNSFW && !this.noteCreateService.isPureRenote(data)) { data.cw = 'Instance is marked as NSFW'; } }); @@ -471,8 +471,9 @@ export class NoteEditService implements OnApplicationShutdown { const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null; + const pollChanged = data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll); - if (Object.keys(update).length > 0 || filesChanged) { + if (Object.keys(update).length > 0 || filesChanged || pollChanged) { const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id }); await this.noteEditRepository.insert({ @@ -544,7 +545,7 @@ export class NoteEditService implements OnApplicationShutdown { })); } - if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) { + if (pollChanged) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, oldnote.id, note); @@ -605,10 +606,11 @@ export class NoteEditService implements OnApplicationShutdown { if (data.poll && data.poll.expiresAt) { const delay = data.poll.expiresAt.getTime() - Date.now(); - this.queueService.endedPollNotificationQueue.remove(note.id); + this.queueService.endedPollNotificationQueue.remove(`pollEnd:${note.id}`); this.queueService.endedPollNotificationQueue.add(note.id, { noteId: note.id, }, { + jobId: `pollEnd:${note.id}`, delay, removeOnComplete: true, }); diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index f5a55eb8bc..098b5e1706 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -56,7 +56,7 @@ export class RemoteUserResolveService { host = this.utilityService.toPuny(host); - if (this.config.host === host) { + if (host === this.utilityService.toPuny(this.config.host)) { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { if (u == null) { diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 009dd4665f..dda4bd5fbd 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -4,9 +4,10 @@ */ import { URL } from 'node:url'; -import { toASCII } from 'punycode'; +import punycode from 'punycode/punycode.js'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; +import psl from 'psl'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; @@ -34,6 +35,11 @@ export class UtilityService { return this.toPuny(this.config.host) === this.toPuny(host); } + @bindThis + public isUriLocal(uri: string): boolean { + return this.punyHost(uri) === this.toPuny(this.config.host); + } + @bindThis public isBlockedHost(blockedHosts: string[], host: string | null): boolean { if (host == null) return false; @@ -101,13 +107,13 @@ export class UtilityService { @bindThis public toPuny(host: string): string { - return toASCII(host.toLowerCase()); + return punycode.toASCII(host.toLowerCase()); } @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; - return toASCII(host.toLowerCase()); + return punycode.toASCII(host.toLowerCase()); } @bindThis @@ -117,6 +123,26 @@ export class UtilityService { return host; } + private specialSuffix(hostname: string): string | null { + // masto.host provides domain names for its clients, we have to + // treat it as if it were a public suffix + const mastoHost = hostname.match(/\.?([a-zA-Z0-9-]+\.masto\.host)$/i); + if (mastoHost) { + return mastoHost[1]; + } + + return null; + } + + @bindThis + public punyHostPSLDomain(url: string): string { + const urlObj = new URL(url); + const hostname = urlObj.hostname; + const domain = this.specialSuffix(hostname) ?? psl.get(hostname) ?? hostname; + const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; + return host; + } + public isFederationAllowedHost(host: string): boolean { if (this.meta.federation === 'none') return false; if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false; diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 8c97cc8ce8..f6b50ec704 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -10,6 +10,7 @@ import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { CacheService } from '@/core/CacheService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; @@ -55,6 +56,7 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, private apLoggerService: ApLoggerService, + private utilityService: UtilityService, ) { this.publicKeyCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h this.publicKeyByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h @@ -64,8 +66,11 @@ export class ApDbResolverService implements OnApplicationShutdown { public parseUri(value: string | IObject | [string | IObject]): UriParseResult { const separator = '/'; - const uri = new URL(getApId(value)); - if (uri.origin !== this.config.url) return { local: false, uri: uri.href }; + const apId = getApId(value); + const uri = new URL(apId); + if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) { + return { local: false, uri: apId }; + } const [, type, id, ...rest] = uri.pathname.split(separator); return { diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f..f045333d2a 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -128,7 +129,7 @@ class DeliverManager { for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; - if (inbox === null) throw new Error('inbox is null'); + if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`); inboxes.set(inbox, following.followerSharedInbox != null); } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index d54c9544c3..75ba5e97b9 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -30,7 +30,9 @@ import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { fromTuple } from '@/misc/from-tuple.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -39,8 +41,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; -import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; -import { fromTuple } from '@/misc/from-tuple.js'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; @Injectable() export class ApInboxService { @@ -93,15 +94,26 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: MiRemoteUser, activity: IObject): Promise { + public async performActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise { let result = undefined as string | void; if (isCollectionOrOrderedCollection(activity)) { const results = [] as [string, string | void][]; - const resolver = this.apResolverService.createResolver(); - for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); + + const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems); + if (items.length >= resolver.getRecursionLimit()) { + throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractDbHost(actor.uri)}`); + } + + for (const item of items) { const act = await resolver.resolve(item); + if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) { + this.logger.debug('skipping activity: activity id is null or mismatching'); + continue; + } try { - results.push([getApId(item), await this.performOneActivity(actor, act)]); + results.push([getApId(item), await this.performOneActivity(actor, act, resolver)]); } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); @@ -116,7 +128,7 @@ export class ApInboxService { result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n'); } } else { - result = await this.performOneActivity(actor, activity); + result = await this.performOneActivity(actor, activity, resolver); } // ついでにリモートユーザーの情報が古かったら更新しておく @@ -131,37 +143,39 @@ export class ApInboxService { } @bindThis - public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise { + public async performOneActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise { if (actor.isSuspended) return; if (isCreate(activity)) { - return await this.create(actor, activity); + return await this.create(actor, activity, resolver); } else if (isDelete(activity)) { return await this.delete(actor, activity); } else if (isUpdate(activity)) { - return await this.update(actor, activity); + return await this.update(actor, activity, resolver); } else if (isFollow(activity)) { return await this.follow(actor, activity); } else if (isAccept(activity)) { - return await this.accept(actor, activity); + return await this.accept(actor, activity, resolver); } else if (isReject(activity)) { - return await this.reject(actor, activity); + return await this.reject(actor, activity, resolver); } else if (isAdd(activity)) { - return await this.add(actor, activity); + return await this.add(actor, activity, resolver); } else if (isRemove(activity)) { - return await this.remove(actor, activity); + return await this.remove(actor, activity, resolver); } else if (isAnnounce(activity)) { - return await this.announce(actor, activity); + return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { - return await this.like(actor, activity); + return await this.like(actor, activity, resolver); + } else if (isDislike(activity)) { + return await this.dislike(actor, activity); } else if (isUndo(activity)) { - return await this.undo(actor, activity); + return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { return await this.block(actor, activity); } else if (isFlag(activity)) { return await this.flag(actor, activity); } else if (isMove(activity)) { - return await this.move(actor, activity); + return await this.move(actor, activity, resolver); } else { return `unrecognized activity type: ${activity.type}`; } @@ -185,30 +199,42 @@ export class ApInboxService { } @bindThis - private async like(actor: MiRemoteUser, activity: ILike): Promise { + private async like(actor: MiRemoteUser, activity: ILike, resolver?: Resolver): Promise { const targetUri = getApId(activity.object); - const note = await this.apNoteService.fetchNote(targetUri); + const object = fromTuple(activity.object); + if (!object) return 'skip: activity has no object property'; + + const note = await this.apNoteService.resolveNote(object, { resolver }); if (!note) return `skip: target note not found ${targetUri}`; await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); - return await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name).catch(err => { - if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { + try { + await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name); + return 'ok'; + } catch (err) { + if (err instanceof IdentifiableError && err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') { return 'skip: already reacted'; } else { throw err; } - }).then(() => 'ok'); + } } @bindThis - private async accept(actor: MiRemoteUser, activity: IAccept): Promise { + private async dislike(actor: MiRemoteUser, dislike: IDislike): Promise { + return await this.undoLike(actor, dislike); + } + + @bindThis + private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise { const uri = activity.id ?? activity; this.logger.info(`Accept: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(err => { this.logger.error(`Resolution failed: ${err}`); @@ -245,7 +271,7 @@ export class ApInboxService { } @bindThis - private async add(actor: MiRemoteUser, activity: IAdd): Promise { + private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'invalid actor'; } @@ -255,8 +281,12 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const object = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(object); + const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isPost(activityObject)) { + return `unsupported featured object type: ${getApType(activityObject)}`; + } + + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -266,12 +296,13 @@ export class ApInboxService { } @bindThis - private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { + private async announce(actor: MiRemoteUser, activity: IAnnounce, resolver?: Resolver): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const activityObject = fromTuple(activity.object); if (!activityObject) return 'skip: activity has no object property'; @@ -280,7 +311,7 @@ export class ApInboxService { const target = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); - return e; + throw e; }); if (isPost(target)) return await this.announceNote(actor, activity, target); @@ -289,7 +320,7 @@ export class ApInboxService { } @bindThis - private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise { + private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise { const uri = getApId(activity); if (actor.isSuspended) { @@ -311,13 +342,13 @@ export class ApInboxService { // Announce対象をresolve let renote; try { - renote = await this.apNoteService.resolveNote(target); + renote = await this.apNoteService.resolveNote(target, { resolver }); if (renote == null) return 'announce target is null'; } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { if (!err.isRetryable) { - return `Ignored announce target ${target.id} - ${err.statusCode}`; + return `skip: ignored announce target ${target.id} - ${err.statusCode}`; } return `Error in announce target ${target.id} - ${err.statusCode}`; } @@ -330,7 +361,7 @@ export class ApInboxService { this.logger.info(`Creating the (Re)Note: ${uri}`); - const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc); + const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc, resolver); const createdAt = activity.published ? new Date(activity.published) : null; if (createdAt && createdAt < this.idService.parse(renote.id).date) { @@ -368,7 +399,7 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate): Promise { + private async create(actor: MiRemoteUser, activity: ICreate | IUpdate, resolver?: Resolver): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -394,7 +425,8 @@ export class ApInboxService { activityObject.attributedTo = activity.actor; } - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -402,14 +434,14 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false); } else { - return `Unknown type: ${getApType(object)}`; + return `skip: Unsupported type for Create: ${getApType(object)} ${getNullableApId(object)}`; } } @bindThis - private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -421,6 +453,8 @@ export class ApInboxService { if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) { return 'skip: host in actor.uri !== note.id'; } + } else { + return 'skip: note.id is not a string'; } } @@ -430,11 +464,11 @@ export class ApInboxService { const exist = await this.apNoteService.fetchNote(note); if (exist) return 'skip: note exists'; - await this.apNoteService.createNote(note, resolver, silent); + await this.apNoteService.createNote(note, actor, resolver, silent); return 'ok'; } catch (err) { if (err instanceof StatusError && !err.isRetryable) { - return `skip ${err.statusCode}`; + return `skip: ${err.statusCode}`; } else { throw err; } @@ -521,7 +555,7 @@ export class ApInboxService { const note = await this.apDbResolverService.getNoteFromApId(uri); if (note == null) { - return 'message not found'; + return 'skip: ignoring deleted note on both ends'; } if (note.userId !== actor.id) { @@ -568,12 +602,13 @@ export class ApInboxService { } @bindThis - private async reject(actor: MiRemoteUser, activity: IReject): Promise { + private async reject(actor: MiRemoteUser, activity: IReject, resolver?: Resolver): Promise { const uri = activity.id ?? activity; this.logger.info(`Reject: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -610,7 +645,7 @@ export class ApInboxService { } @bindThis - private async remove(actor: MiRemoteUser, activity: IRemove): Promise { + private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'invalid actor'; } @@ -621,7 +656,11 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(activityObject); + if (isApObject(activityObject) && !isPost(activityObject)) { + return `unsupported featured object type: ${getApType(activityObject)}`; + } + + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); return; @@ -631,7 +670,7 @@ export class ApInboxService { } @bindThis - private async undo(actor: MiRemoteUser, activity: IUndo): Promise { + private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'invalid actor'; } @@ -640,11 +679,12 @@ export class ApInboxService { this.logger.info(`Undo: ${uri}`); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); - return e; + throw e; }); // don't queue because the sender may attempt again when timeout @@ -654,7 +694,7 @@ export class ApInboxService { if (isAnnounce(object)) return await this.undoAnnounce(actor, object); if (isAccept(object)) return await this.undoAccept(actor, object); - return `skip: unknown object type ${getApType(object)}`; + return `skip: unknown activity type ${getApType(object)}`; } @bindThis @@ -749,7 +789,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: MiRemoteUser, activity: ILike): Promise { + private async undoLike(actor: MiRemoteUser, activity: ILike | IDislike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); @@ -764,14 +804,15 @@ export class ApInboxService { } @bindThis - private async update(actor: MiRemoteUser, activity: IUpdate): Promise { + private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } this.logger.debug('Update'); - const resolver = this.apResolverService.createResolver(); + // eslint-disable-next-line no-param-reassign + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); @@ -782,22 +823,32 @@ export class ApInboxService { await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { - await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); + // If we get an Update(Question) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + + await this.apQuestionService.updateQuestion(object, actor, resolver); return 'ok: Question updated'; - } else if (getApType(object) === 'Note') { - await this.apNoteService.updateNote(object, resolver).catch(err => console.error(err)); + } else if (isPost(object)) { + // If we get an Update(Note) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + + await this.apNoteService.updateNote(object, actor, resolver); return 'ok: Note updated'; } else { - return `skip: Unknown type: ${getApType(object)}`; + return `skip: Unsupported type for Update: ${getApType(object)} ${getNullableApId(object)}`; } } @bindThis - private async move(actor: MiRemoteUser, activity: IMove): Promise { + private async move(actor: MiRemoteUser, activity: IMove, resolver?: Resolver): Promise { // fetch the new and old accounts const targetUri = getApHrefNullable(activity.target); if (!targetUri) return 'skip: invalid activity target'; - return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do'; + return await this.apPersonService.updatePerson(actor.uri, resolver) ?? 'skip: nothing to do'; } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 42ee5bc58a..89e5217285 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -7,6 +7,7 @@ import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import * as mfm from '@transfem-org/sfm-js'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -30,6 +31,7 @@ import { IdService } from '@/core/IdService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; +import { getApId } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() @@ -106,7 +108,7 @@ export class ApRendererService { to = [`${attributedTo}/followers`]; cc = []; } else { - throw new Error('renderAnnounce: cannot render non-public note'); + throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`); } return { @@ -469,6 +471,7 @@ export class ApRendererService { }; } + // if you change this, also change `server/api/endpoints/i/update.ts` @bindThis public async renderPerson(user: MiLocalUser) { const id = this.userEntityService.genLocalUserUri(user.id); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 38c78cf900..0dea615cb0 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -18,6 +18,7 @@ import type Logger from '@/logger.js'; import type { IObject } from './type.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; +import { UtilityService } from "@/core/UtilityService.js"; type Request = { url: string; @@ -147,6 +148,7 @@ export class ApRequestService { private userKeypairService: UserKeypairService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, + private utilityService: UtilityService, ) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる @@ -241,7 +243,9 @@ export class ApRequestService { if (alternate) { const href = alternate.getAttribute('href'); if (href) { - return await this.signedGet(href, user, false); + if (this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) { + return await this.signedGet(href, user, false); + } } } } catch (e) { @@ -257,7 +261,7 @@ export class ApRequestService { const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; - assertActivityMatchesUrls(activity, [url, finalUrl]); + assertActivityMatchesUrls(activity, [finalUrl]); return activity; } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 5d5c61ce2c..e56b9ebc99 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; @@ -15,12 +16,12 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; +import { fromTuple } from '@/misc/from-tuple.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; -import { fromTuple } from '@/misc/from-tuple.js'; export class Resolver { private history: Set; @@ -42,7 +43,7 @@ export class Resolver { private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, private loggerService: LoggerService, - private recursionLimit = 100, + private recursionLimit = 256, ) { this.history = new Set(); this.logger = this.loggerService.getLogger('ap-resolve'); @@ -53,6 +54,11 @@ export class Resolver { return Array.from(this.history); } + @bindThis + public getRecursionLimit(): number { + return this.recursionLimit; + } + @bindThis public async resolveCollection(value: string | IObject): Promise { const collection = typeof value === 'string' @@ -62,7 +68,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new Error(`unrecognized collection type: ${collection.type}`); + throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`); } } @@ -79,15 +85,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new Error(`cannot resolve URL with fragment: ${value}`); + throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`); } if (this.history.has(value)) { - throw new Error('cannot resolve already resolved one'); + throw new Error(`cannot resolve already resolved URL: ${value}`); } if (this.history.size > this.recursionLimit) { - throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`); + throw new Error(`hit recursion limit: ${value}`); } this.history.add(value); @@ -98,7 +104,7 @@ export class Resolver { } if (!this.utilityService.isFederationAllowedHost(host)) { - throw new Error('Instance is blocked'); + throw new UnrecoverableError(`instance is blocked: ${value}`); } if (this.config.signToActivityPubGet && !this.user) { @@ -114,15 +120,19 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new Error('invalid response'); + throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`); } // HttpRequestService / ApRequestService have already checked that // `object.id` or `object.url` matches the URL used to fetch the // object after redirects; here we double-check that no redirects // bounced between hosts - if (object.id && (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value))) { - throw new Error(`invalid AP object ${value}: id ${object.id} has different host`); + if (object.id == null) { + throw new UnrecoverableError(`invalid AP object ${value}: missing id`); + } + + if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) { + throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`); } return object; @@ -131,7 +141,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new Error('resolveLocal: not local'); + if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`); switch (parsed.type) { case 'notes': @@ -160,7 +170,7 @@ export class Resolver { case 'follows': return this.followRequestsRepository.findOneBy({ id: parsed.id }) .then(async followRequest => { - if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID'); + if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); const [follower, followee] = await Promise.all([ this.usersRepository.findOneBy({ id: followRequest.followerId, @@ -172,12 +182,12 @@ export class Resolver { }), ]); if (follower == null || followee == null) { - throw new Error('resolveLocal: follower or followee does not exist'); + throw new Error(`resolveLocal - follower or followee does not exist: ${url}`); } return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); }); default: - throw new Error(`resolveLocal: type ${parsed.type} unhandled`); + throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`); } } } diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts index 100d4fa19f..9d1e2e06cc 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/JsonLdService.ts @@ -5,6 +5,7 @@ import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; @@ -109,7 +110,7 @@ class JsonLd { @bindThis private getLoader() { return async (url: string): Promise => { - if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); + if (!/^https?:\/\//.test(url)) throw new UnrecoverableError(`Invalid URL: ${url}`); if (this.preLoad) { if (url in PRELOADED_CONTEXTS) { @@ -148,7 +149,7 @@ class JsonLd { }, ).then(res => { if (!res.ok) { - throw new Error(`${res.status} ${res.statusText}`); + throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`); } else { return res.json(); } diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts index 34e4907267..edfab5a216 100644 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ b/packages/backend/src/core/activitypub/misc/check-against-url.ts @@ -2,26 +2,30 @@ * SPDX-FileCopyrightText: dakkar and sharkey-project * SPDX-License-Identifier: AGPL-3.0-only */ + +import { UnrecoverableError } from 'bullmq'; import type { IObject } from '../type.js'; -function getHrefFrom(one: IObject|string): string | undefined { - if (typeof(one) === 'string') return one; - return one.href; +function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] { + if (Array.isArray(one)) { + return one.flatMap(h => getHrefsFrom(h)); + } + return [ + typeof(one) === 'object' ? one.href : one, + ]; } export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { - const idOk = activity.id !== undefined && urls.includes(activity.id); - if (idOk) return; + const expectedUrls = new Set(urls + .filter(u => URL.canParse(u)) + .map(u => new URL(u).href), + ); - const url = activity.url; - if (url) { - // `activity.url` can be an `ApObject = IObject | string | (IObject - // | string)[]`, we have to look inside it - const activityUrls = Array.isArray(url) ? url.map(getHrefFrom) : [getHrefFrom(url)]; - const goodUrl = activityUrls.find(u => u && urls.includes(u)); + const actualUrls = [activity.id, ...getHrefsFrom(activity.url)] + .filter(u => u && URL.canParse(u)) + .map(u => new URL(u as string).href); - if (goodUrl) return; + if (!actualUrls.some(u => expectedUrls.has(u))) { + throw new UnrecoverableError(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`); } - - throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${JSON.stringify(activity?.url)}) match location(${urls})`); } diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts index 690beeffef..4292b7e0f7 100644 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ b/packages/backend/src/core/activitypub/misc/validator.ts @@ -9,7 +9,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error('Validate content type of AP response: No content-type header'); + throw new Error(`invalid content type of AP response - no content-type header: ${response.url}`); } if ( contentType.startsWith('application/activity+json') || @@ -17,7 +17,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { ) { return; } - throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json'); + throw new Error(`invalid content type of AP response - content type is not application/activity+json or application/ld+json: ${response.url}`); } const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; @@ -26,7 +26,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error('Validate content type of JSON LD: No content-type header'); + throw new Error(`invalid content type of JSON LD - no content-type header: ${response.url}`); } if ( contentType.startsWith('application/ld+json') || @@ -35,5 +35,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { ) { return; } - throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json'); + throw new Error(`invalid content type of JSON LD - content type is not application/ld+json or application/json: ${response.url}`); } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 259889d945..c5c3b736d4 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { Config } from '@/config.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { isDocument, type IObject } from '../type.js'; @@ -47,7 +48,7 @@ export class ApImageService { public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new Error('actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`); } const image = await this.apResolverService.createResolver().resolve(value); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index f404a77fbb..b8aa67e9ea 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -5,8 +5,9 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -49,6 +50,9 @@ export class ApNoteService { @Inject(DI.meta) private meta: MiMeta, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -82,7 +86,13 @@ export class ApNoteService { } @bindThis - public validateNote(object: IObject, uri: string): Error | null { + public validateNote( + object: IObject, + uri: string, + actor?: MiRemoteUser, + user?: MiRemoteUser, + note?: MiNote, + ): Error | null { const expectHost = this.utilityService.extractDbHost(uri); const apType = getApType(object); @@ -99,10 +109,27 @@ export class ApNoteService { return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } + if (actor) { + const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri; + if (attribution !== actor.uri) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); + } + if (user && attribution !== user.uri) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`); + } + } + if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); } + if (note) { + const url = (object.url) ? getOneApId(object.url) : note.url; + if (url && url !== note.url) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated url does not match original url. updated url: ${url}, original url: ${note.url}`); + } + } + return null; } @@ -116,18 +143,27 @@ export class ApNoteService { return await this.apDbResolverService.getNoteFromApId(object); } + /** + * Returns true if the provided object / ID exists in the local database. + */ + @bindThis + public async hasNote(object: string | IObject | [string | IObject]): Promise { + const uri = getApId(object); + return await this.notesRepository.existsBy({ uri }); + } + /** * Noteを作成します。 */ @bindThis - public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(value); const entryUri = getApId(value); - const err = this.validateNote(object, entryUri); + const err = this.validateNote(object, entryUri, actor); if (err) { this.logger.error(err.message, { resolver: { history: resolver.getHistory() }, @@ -141,29 +177,40 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - if (note.id && !checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + if (note.id == null) { + throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`); + } + + if (!checkHttps(note.id)) { + throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`); } const url = getOneApHrefNullable(note.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + if (url != null) { + if (!checkHttps(url)) { + throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`); + } + + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) { + throw new UnrecoverableError(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); + } } this.logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); + throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`); } const uri = getOneApId(note.attributedTo); // ローカルで投稿者を検索し、もし凍結されていたらスキップ - const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; - if (cachedActor && cachedActor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + // eslint-disable-next-line no-param-reassign + actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined; + if (actor && actor.isSuspended) { + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${entryUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -190,15 +237,16 @@ export class ApNoteService { */ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${entryUri}`); } //#endregion - const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; + // eslint-disable-next-line no-param-reassign + actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -228,13 +276,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); throw err; }) : null; @@ -243,16 +291,25 @@ export class ApNoteService { let quote: MiNote | undefined | null = null; if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) { - const tryResolveNote = async (uri: string): Promise< + const tryResolveNote = async (uri: unknown): Promise< | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`); + return { status: 'permerror' }; + } try { const res = await this.resolveNote(uri, { resolver }); - if (res == null) return { status: 'permerror' }; + if (res == null) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`); + return { status: 'permerror' }; + } return { status: 'ok', res }; } catch (e) { + const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`); + return { status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; @@ -265,7 +322,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw new Error(`temporary error resolving quote for ${entryUri}`); } } } @@ -325,7 +382,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`); } return duplicate; } @@ -335,16 +392,18 @@ export class ApNoteService { * Noteを作成します。 */ @bindThis - public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { - const noteUri = typeof value === 'string' ? value : value.id; - if (noteUri == null) throw new Error('uri is null'); + public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { + const noteUri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (noteUri.startsWith(this.config.url + '/')) throw new Error('uri points local'); + if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`); //#region このサーバーに既に登録されているか - const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); - if (UpdatedNote == null) throw new Error('Note is not registered'); + const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); + if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`); + + const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null; + if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`); // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); @@ -362,33 +421,38 @@ export class ApNoteService { throw err; } + // `validateNote` checks that the actor and user are one and the same + // eslint-disable-next-line no-param-reassign + actor ??= user; + const note = object as IPost; this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - if (note.id && !checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + if (note.id == null) { + throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`); + } + + if (!checkHttps(note.id)) { + throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`); } const url = getOneApHrefNullable(note.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + if (url != null) { + if (!checkHttps(url)) { + throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`); + } + + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) { + throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`); + } } this.logger.info(`Creating the Note: ${note.id}`); - // 投稿者をフェッチ - if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); - } - - const uri = getOneApId(note.attributedTo); - - // ローカルで投稿者を検索し、もし凍結されていたらスキップ - const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser; - if (cachedActor && cachedActor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + if (actor.isSuspended) { + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -415,17 +479,10 @@ export class ApNoteService { */ const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${noteUri}`); } //#endregion - const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser; - - // 投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); - } - const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); let visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; @@ -453,13 +510,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); throw err; }) : null; @@ -468,16 +525,25 @@ export class ApNoteService { let quote: MiNote | undefined | null = null; if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) { - const tryResolveNote = async (uri: string): Promise< + const tryResolveNote = async (uri: unknown): Promise< | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`); + return { status: 'permerror' }; + } try { const res = await this.resolveNote(uri, { resolver }); - if (res == null) return { status: 'permerror' }; + if (res == null) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`); + return { status: 'permerror' }; + } return { status: 'ok', res }; } catch (e) { + const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`); + return { status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; @@ -490,7 +556,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw new Error(`temporary error resolving quote for ${entryUri}`); } } } @@ -525,7 +591,7 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); try { - return await this.noteEditService.edit(actor, UpdatedNote.id, { + return await this.noteEditService.edit(actor, updatedNote.id, { createdAt: note.published ? new Date(note.published) : null, files, reply, @@ -550,7 +616,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw new Error(`The note creation failed with duplication error even when there is no duplication: ${noteUri}`); } return duplicate; } @@ -567,7 +633,7 @@ export class ApNoteService { const uri = getApId(value); if (!this.utilityService.isFederationAllowedUri(uri)) { - throw new StatusError('blocked host', 451); + throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host'); } const unlock = await this.appLockService.getApLock(uri); @@ -578,15 +644,15 @@ export class ApNoteService { if (exist) return exist; //#endregion - if (uri.startsWith(this.config.url)) { - throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); + if (this.utilityService.isUriLocal(uri)) { + throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note'); } // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri; - return await this.createNote(createFrom, options.resolver, true); + return await this.createNote(createFrom, undefined, options.resolver, true); } finally { unlock(); } @@ -627,7 +693,7 @@ export class ApNoteService { }); const emoji = await this.emojisRepository.findOneBy({ host, name }); - if (emoji == null) throw new Error('emoji update failed'); + if (emoji == null) throw new Error(`emoji update failed: ${name}:${host}`); return emoji; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 2046dad099..598486cd84 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -7,6 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; +import { AbortError } from 'node-fetch'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -136,35 +138,49 @@ export class ApPersonService implements OnModuleInit { */ @bindThis private validateActor(x: IObject, uri: string): IActor { - const expectHost = this.utilityService.punyHost(uri); + const expectHost = this.utilityService.punyHostPSLDomain(uri); if (!isActor(x)) { - throw new Error(`invalid Actor type '${x.type}'`); + throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`); } if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new Error('invalid Actor: wrong id'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`); } if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new Error('invalid Actor: wrong inbox'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`); } - if (this.utilityService.punyHost(x.inbox) !== expectHost) { - throw new Error('invalid Actor: inbox has different host'); + const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox); + if (inboxHost !== expectHost) { + throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`); + } + + const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); + if (sharedInboxObject != null) { + const sharedInbox = getApId(sharedInboxObject); + if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) { + throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`); + } } for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) { - const collectionUri = (x as IActor)[collection]; - if (typeof collectionUri === 'string' && collectionUri.length > 0) { - if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new Error(`invalid Actor: ${collection} has different host`); + const xCollection = (x as IActor)[collection]; + if (xCollection != null) { + const collectionUri = getApId(xCollection); + if (typeof collectionUri === 'string' && collectionUri.length > 0) { + if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) { + throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`); + } + } else if (collectionUri != null) { + throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`); } } } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new Error('invalid Actor: wrong username'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`); } // These fields are only informational, and some AP software allows these @@ -172,7 +188,7 @@ export class ApPersonService implements OnModuleInit { // we can at least see these users and their activities. if (x.name) { if (!(typeof x.name === 'string' && x.name.length > 0)) { - throw new Error('invalid Actor: wrong name'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong name`); } x.name = truncate(x.name, nameLength); } else if (x.name === '') { @@ -181,24 +197,24 @@ export class ApPersonService implements OnModuleInit { } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new Error('invalid Actor: wrong summary'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`); } x.summary = truncate(x.summary, summaryLength); } - const idHost = this.utilityService.punyHost(x.id); + const idHost = this.utilityService.punyHostPSLDomain(x.id); if (idHost !== expectHost) { - throw new Error('invalid Actor: id has different host'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`); } if (x.publicKey) { if (typeof x.publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`); } - const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); + const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id); if (publicKeyIdHost !== expectHost) { - throw new Error('invalid Actor: publicKey.id has different host'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`); } } @@ -284,24 +300,23 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async createPerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`); - if (uri.startsWith(this.config.url)) { - throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); + const host = this.utilityService.punyHost(uri); + if (host === this.utilityService.toPuny(this.config.host)) { + throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user'); } // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(uri); - if (object.id == null) throw new Error('invalid object.id: ' + object.id); + if (object.id == null) throw new UnrecoverableError(`null object.id in ${uri}`); const person = this.validateActor(object, uri); this.logger.info(`Creating the Person: ${person.id}`); - const host = this.utilityService.punyHost(object.id); - const fields = this.analyzeAttachments(person.attachment ?? []); const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); @@ -327,8 +342,18 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + if (person.id == null) { + throw new UnrecoverableError(`Refusing to create person without id: ${uri}`); + } + + if (url != null) { + if (!checkHttps(url)) { + throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); + } + + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) { + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); + } } // Create user @@ -419,7 +444,7 @@ export class ApPersonService implements OnModuleInit { if (isDuplicateKeyValueError(e)) { // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 const u = await this.usersRepository.findOneBy({ uri: person.id }); - if (u == null) throw new Error('already registered'); + if (u == null) throw new UnrecoverableError(`already registered a user with conflicting data: ${uri}`); user = u as MiRemoteUser; } else { @@ -428,7 +453,7 @@ export class ApPersonService implements OnModuleInit { } } - if (user == null) throw new Error('failed to create user: user is null'); + if (user == null) throw new Error(`failed to create user - user is null: ${uri}`); // Register to the cache this.cacheService.uriPersonCache.set(user.uri, user); @@ -477,10 +502,10 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string'); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(`${this.config.url}/`)) return; + if (this.utilityService.isUriLocal(uri)) return; //#region このサーバーに既に登録されているか const exist = await this.fetchPerson(uri) as MiRemoteUser | null; @@ -529,8 +554,18 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); - if (url && !checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + if (person.id == null) { + throw new UnrecoverableError(`Refusing to update person without id: ${uri}`); + } + + if (url != null) { + if (!checkHttps(url)) { + throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); + } + + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) { + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); + } } const updates = { @@ -640,7 +675,7 @@ export class ApPersonService implements OnModuleInit { }); } - return 'skip'; + return 'skip: too soon to migrate accounts'; } /** @@ -690,8 +725,16 @@ export class ApPersonService implements OnModuleInit { const _resolver = resolver ?? this.apResolverService.createResolver(); // Resolve to (Ordered)Collection Object - const collection = await _resolver.resolveCollection(user.featured); - if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); + const collection = await _resolver.resolveCollection(user.featured).catch(err => { + if (err instanceof AbortError || err instanceof StatusError) { + this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`); + } else { + this.logger.error('Failed to update featured notes:', err); + } + }); + if (!collection) return; + + if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; @@ -699,9 +742,10 @@ export class ApPersonService implements OnModuleInit { // Resolve and regist Notes const limit = promiseLimit(2); + const maxPinned = (await this.roleService.getUserPolicies(user.id)).pinLimit; const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも - .slice(0, 5) + .slice(0, maxPinned) .map(item => limit(() => this.apNoteService.resolveNote(item, { resolver: _resolver, sentFrom: new URL(user.uri), @@ -747,7 +791,7 @@ export class ApPersonService implements OnModuleInit { await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]); dst = await this.fetchPerson(src.movedToUri) ?? dst; } else { - if (src.movedToUri.startsWith(`${this.config.url}/`)) { + if (this.utilityService.isUriLocal(src.movedToUri)) { // ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている return 'failed: movedTo is local but not found'; } diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 9246398fde..79a4f49d92 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -4,17 +4,20 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, PollsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { IPoll } from '@/models/Poll.js'; +import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { isQuestion } from '../type.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { getApId, getApType, getNullableApId, getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; -import type { IObject, IQuestion } from '../type.js'; +import type { IObject } from '../type.js'; @Injectable() export class ApQuestionService { @@ -24,6 +27,9 @@ export class ApQuestionService { @Inject(DI.config) private config: Config, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -32,6 +38,7 @@ export class ApQuestionService { private apResolverService: ApResolverService, private apLoggerService: ApLoggerService, + private utilityService: UtilityService, ) { this.logger = this.apLoggerService.logger; } @@ -42,10 +49,10 @@ export class ApQuestionService { if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); - if (!isQuestion(question)) throw new Error('invalid type'); + if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error('invalid question'); + if (multiple && question.anyOf === undefined) throw new UnrecoverableError(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; @@ -65,38 +72,48 @@ export class ApQuestionService { * @returns true if updated */ @bindThis - public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise { - const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('uri is null'); + public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise { + const uri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local'); + if (this.utilityService.isUriLocal(uri)) throw new Error(`uri points local: ${uri}`); //#region このサーバーに既に登録されているか const note = await this.notesRepository.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registered'); + if (note == null) throw new Error(`Question is not registered (no note): ${uri}`); const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registered'); + if (poll == null) throw new Error(`Question is not registered (no poll): ${uri}`); + + const user = await this.usersRepository.findOneBy({ id: poll.userId }); + if (user == null) throw new Error(`Question is not registered (no user): ${uri}`); //#endregion // resolve new Question object // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); - const question = await resolver.resolve(value) as IQuestion; + const question = await resolver.resolve(value); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (question.type !== 'Question') throw new Error('object is not a Question'); + if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`); + + const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri; + const attributionMatchesExisting = attribution === user.uri; + const actorMatchesAttribution = (actor) ? attribution === actor.uri : true; + + if (!attributionMatchesExisting || !actorMatchesAttribution) { + throw new UnrecoverableError(`Refusing to ingest update for poll by different user: ${uri}`); + } const apChoices = question.oneOf ?? question.anyOf; - if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices); + if (apChoices == null) throw new UnrecoverableError(`poll has no choices: ${uri}`); let changed = false; for (const choice of poll.choices) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; - if (newCount == null) throw new Error('invalid newCount: ' + newCount); + if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new UnrecoverableError(`invalid newCount: ${newCount} in ${uri}`); if (oldCount <= newCount) { changed = true; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index af5aba9c16..c9e20e0168 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { UnrecoverableError } from 'bullmq'; import { fromTuple } from '@/misc/from-tuple.js'; export type Obj = { [x: string]: any }; @@ -61,7 +62,19 @@ export function getApId(value: string | IObject | [string | IObject]): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot determine id'); + throw new UnrecoverableError('cannot determine id'); +} + +/** + * Get ActivityStreams Object id, or null if not present + */ +export function getNullableApId(value: string | IObject | [string | IObject]): string | null { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + + if (typeof value === 'string') return value; + if (typeof value.id === 'string') return value.id; + return null; } /** @@ -323,6 +336,10 @@ export interface ILike extends IActivity { _misskey_reaction?: string; } +export interface IDislike extends IActivity { + type: 'Dislike'; +} + export interface IAnnounce extends IActivity { type: 'Announce'; } @@ -340,6 +357,7 @@ export interface IMove extends IActivity { target: IObject | string; } +export const isApObject = (object: string | IObject): object is IObject => typeof(object) === 'object'; export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; @@ -354,6 +372,7 @@ export const isLike = (object: IObject): object is ILike => { const type = getApType(object); return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type); }; +export const isDislike = (object: IObject): object is IDislike => getApType(object) === 'Dislike'; export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts index 79681370a1..316feec6ee 100644 --- a/packages/backend/src/core/chart/ChartManagementService.ts +++ b/packages/backend/src/core/chart/ChartManagementService.ts @@ -6,6 +6,8 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; +import Logger from '@/logger.js'; import FederationChart from './charts/federation.js'; import NotesChart from './charts/notes.js'; import UsersChart from './charts/users.js'; @@ -24,6 +26,7 @@ import type { OnApplicationShutdown } from '@nestjs/common'; export class ChartManagementService implements OnApplicationShutdown { private charts; private saveIntervalId: NodeJS.Timeout; + private readonly logger: Logger; constructor( private federationChart: FederationChart, @@ -38,6 +41,7 @@ export class ChartManagementService implements OnApplicationShutdown { private perUserFollowingChart: PerUserFollowingChart, private perUserDriveChart: PerUserDriveChart, private apRequestChart: ApRequestChart, + private chartLoggerService: ChartLoggerService, ) { this.charts = [ this.federationChart, @@ -53,6 +57,7 @@ export class ChartManagementService implements OnApplicationShutdown { this.perUserDriveChart, this.apRequestChart, ]; + this.logger = chartLoggerService.logger; } @bindThis @@ -62,6 +67,7 @@ export class ChartManagementService implements OnApplicationShutdown { for (const chart of this.charts) { chart.save(); } + this.logger.info('All charts saved'); }, 1000 * 60 * 20); } @@ -72,6 +78,7 @@ export class ChartManagementService implements OnApplicationShutdown { await Promise.all( this.charts.map(chart => chart.save()), ); + this.logger.info('All charts saved'); } } diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index af5485a46e..234c1d63b4 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -368,7 +368,7 @@ export default abstract class Chart { // 初期ログデータを作成 data = this.getNewLog(null); - this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`); + this.logger.debug(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`); } const date = Chart.dateToTimestamp(current); @@ -398,7 +398,7 @@ export default abstract class Chart { ...columns, }) as RawRecord; - this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); + this.logger.debug(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); return log; } finally { @@ -418,7 +418,7 @@ export default abstract class Chart { @bindThis public async save(): Promise { if (this.buffer.length === 0) { - this.logger.info(`${this.name}: Write skipped`); + this.logger.debug(`${this.name}: Write skipped`); return; } @@ -519,7 +519,7 @@ export default abstract class Chart { .execute(), ]); - this.logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); + this.logger.debug(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`); // TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group)); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 4dd17c5af3..7327e5e125 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,7 +16,9 @@ import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { OnModuleInit } from '@nestjs/common'; +import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; @@ -27,6 +29,7 @@ import type { Config } from '@/config.js'; export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; private driveFileEntityService: DriveFileEntityService; + private cacheService: CacheService; private customEmojiService: CustomEmojiService; private reactionService: ReactionService; private reactionsBufferingService: ReactionsBufferingService; @@ -75,6 +78,7 @@ export class NoteEntityService implements OnModuleInit { onModuleInit() { this.userEntityService = this.moduleRef.get('UserEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); + this.cacheService = this.moduleRef.get('CacheService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.reactionService = this.moduleRef.get('ReactionService'); this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); @@ -119,29 +123,32 @@ export class NoteEntityService implements OnModuleInit { } else if (packedNote.renote && (meId === packedNote.renote.userId)) { hide = false; } else { - if (packedNote.renote) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.renote.userId, - followerId: meId, - }, - }); + // フォロワーかどうか + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: packedNote.userId, + followerId: meId, + }, + }); - hide = !isFollowing; - } else { - // フォロワーかどうか - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.userId, - followerId: meId, - }, - }); - - hide = !isFollowing; - } + hide = !isFollowing; } } + // If this is a pure renote (boost), then we should *also* check the boosted note's visibility. + // Otherwise we can have empty notes on the timeline, which is not good. + // Notes are packed in depth-first order, so we can safely grab the "isHidden" property to avoid duplicated checks. + // This is pulled out to ensure that we check both the renote *and* the boosted note. + if (packedNote.renote?.isHidden && isPackedPureRenote(packedNote)) { + hide = true; + } + + if (!hide && meId && packedNote.userId !== meId) { + const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId); + + if (isBlocked) hide = true; + } + if (hide) { packedNote.visibleUserIds = undefined; packedNote.fileIds = []; @@ -149,6 +156,12 @@ export class NoteEntityService implements OnModuleInit { packedNote.text = null; packedNote.poll = undefined; packedNote.cw = null; + packedNote.repliesCount = 0; + packedNote.reactionAcceptance = null; + packedNote.reactionAndUserPairCache = undefined; + packedNote.reactionCount = 0; + packedNote.reactionEmojis = {}; + packedNote.reactions = {}; packedNote.isHidden = true; } } @@ -262,7 +275,8 @@ export class NoteEntityService implements OnModuleInit { return true; } else { // フォロワーかどうか - const [following, user] = await Promise.all([ + const [blocked, following, user] = await Promise.all([ + this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)), this.followingsRepository.count({ where: { followeeId: note.userId, @@ -273,6 +287,8 @@ export class NoteEntityService implements OnModuleInit { this.usersRepository.findOneByOrFail({ id: meId }), ]); + if (blocked) return false; + /* If we know the following, everyhting is fine. But if we do not know the following, it might be that both the @@ -284,6 +300,12 @@ export class NoteEntityService implements OnModuleInit { } } + if (meId != null) { + const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(note.userId); + + if (isBlocked) return false; + } + return true; } diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index c128fded14..99c755e38f 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -69,6 +69,14 @@ type PackedQuote = fileIds: NonNullable['fileIds']> }); +type PackedPureRenote = PackedRenote & { + text: NonNullable['text']>; + cw: NonNullable['cw']>; + replyId: NonNullable['replyId']>; + poll: NonNullable['poll']>; + fileIds: NonNullable['fileIds']>; +} + export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { return note.renoteId != null; } @@ -80,3 +88,7 @@ export function isQuotePacked(note: PackedRenote): note is PackedQuote { note.poll != null || (note.fileIds != null && note.fileIds.length > 0); } + +export function isPackedPureRenote(note: Packed<'Note'>): note is PackedPureRenote { + return isRenotePacked(note) && !isQuotePacked(note); +} diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 7853100d89..1fa686c0dd 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -14,10 +14,7 @@ export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string { let str = ''; for (let i = 0; i < length; i++) { - let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len); - if (rand === chars_len) { - rand = chars_len - 1; - } + const rand = crypto.randomInt(0, chars_len); str += chars.charAt(rand); } diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index eaeb6d58df..f130314e74 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -10,6 +10,7 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { StatusError } from '@/misc/status-error.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -132,7 +133,7 @@ export class QueueProcessorService implements OnApplicationShutdown { // 何故かeがundefinedで来ることがある if (!e) return '?'; - if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { + if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError' || e instanceof StatusError) { return `${e.name}: ${e.message}`; } @@ -146,12 +147,15 @@ export class QueueProcessorService implements OnApplicationShutdown { function renderJob(job?: Bull.Job) { if (!job) return '?'; - return { - name: job.name || undefined, + const info: Record = { info: getJobInfo(job), - failedReason: job.failedReason || undefined, data: job.data, }; + + if (job.name) info.name = job.name; + if (job.failedReason) info.failedReason = job.failedReason; + + return info; } //#region system diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 11b00bb683..eaf2aa3c4c 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -7,6 +7,7 @@ import { URL } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { AbortError } from 'node-fetch'; import type Logger from '@/logger.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -192,6 +193,9 @@ export class InboxProcessorService implements OnApplicationShutdown { if (signerHost !== activityIdHost) { throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } + } else { + // Activity ID should only be string or undefined. + delete activity.id; } // Update stats @@ -215,7 +219,11 @@ export class InboxProcessorService implements OnApplicationShutdown { try { const result = await this.apInboxService.performActivity(authUser.user, activity); if (result && !result.startsWith('ok')) { - this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`); + if (result.startsWith('skip:')) { + this.logger.info(`inbox activity ignored: id=${activity.id} reason=${result}`); + } else { + this.logger.warn(`inbox activity failed: id=${activity.id} reason=${result}`); + } return result; } } catch (e) { @@ -230,6 +238,19 @@ export class InboxProcessorService implements OnApplicationShutdown { return e.message; } } + + if (e instanceof StatusError) { + if (e.isRetryable) { + return `temporary error ${e.statusCode}`; + } else { + return `skip: permanent error ${e.statusCode}`; + } + } + + if (e instanceof AbortError) { + return 'request aborted'; + } + throw e; } return 'ok'; diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 52592c47c6..f955329fd1 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -152,7 +152,7 @@ export class ActivityPubServerService { let signature; try { - signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' }); } catch (e) { // not signed, or malformed signature: refuse this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`); @@ -229,7 +229,7 @@ export class ActivityPubServerService { let signature; try { - signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'digest', 'host', 'date'], authorizationHeaderName: 'signature' }); } catch (e) { reply.code(401); return; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index be196373c4..a4d0588fbe 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -31,6 +31,7 @@ import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers. import { RateLimiterService } from '@/server/api/RateLimiterService.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import { AuthenticateService } from '@/server/api/AuthenticateService.js'; +import type { IEndpointMeta } from '@/server/api/endpoints.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type Limiter from 'ratelimiter'; @@ -82,7 +83,7 @@ export class FileServerService { }); fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { - if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return; + if (!await this.checkRateLimit(request, reply, '/files/', request.params.key)) return; return await this.sendDriveFile(request, reply) .catch(err => this.errorHandler(request, reply, err)); @@ -109,7 +110,7 @@ export class FileServerService { keyUrl.username = ''; keyUrl.password = ''; - if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return; + if (!await this.checkRateLimit(request, reply, '/proxy/', keyUrl.href)) return; return await this.proxyHandler(request, reply) .catch(err => this.errorHandler(request, reply, err)); @@ -603,7 +604,8 @@ export class FileServerService { Params?: Record | unknown, }>, reply: FastifyReply, - rateLimitKey: string, + group: string, + resource: string, ): Promise { const body = request.method === 'GET' ? request.query @@ -622,32 +624,48 @@ export class FileServerService { const [user] = await this.authenticateService.authenticate(token); const actor = user?.id ?? getIpHash(request.ip); + // Call both limits: the per-resource limit and the shared cross-resource limit + return await this.checkResourceLimit(reply, actor, group, resource) && await this.checkSharedLimit(reply, actor, group); + } + + private async checkResourceLimit(reply: FastifyReply, actor: string, group: string, resource: string): Promise { const limit = { // Group by resource - key: rateLimitKey, + key: `${group}${resource}`, // Maximum of 10 requests / 10 minutes max: 10, duration: 1000 * 60 * 10, - - // Minimum of 250 ms between each request - minInterval: 250, }; - // Rate limit proxy requests + return await this.checkLimit(reply, actor, limit); + } + + private async checkSharedLimit(reply: FastifyReply, actor: string, group: string): Promise { + const limit = { + key: group, + + // Maximum of 3600 requests per hour, which is an average of 1 per second. + max: 3600, + duration: 1000 * 60 * 60, + }; + + return await this.checkLimit(reply, actor, limit); + } + + private async checkLimit(reply: FastifyReply, actor: string, limit: IEndpointMeta['limit'] & { key: NonNullable }): Promise { try { await this.rateLimiterService.limit(limit, actor); return true; } catch (err) { // errはLimiter.LimiterInfoであることが期待される - reply.code(429); - if (hasRateLimitInfo(err)) { const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000); // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); } + reply.code(429); reply.send({ message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED', diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 8e326da89a..70f672e5d9 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -140,6 +140,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { } const subject = `acct:${user.username}@${this.config.host}`; + const profileLink = `${this.config.url}/@${user.username}`; const self = { rel: 'self', type: 'application/activity+json', @@ -148,7 +149,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => { const profilePage = { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', - href: `${this.config.url}/@${user.username}`, + href: profileLink, }; const subscribe = { rel: 'http://ostatus.org/schema/1.0/subscribe', @@ -164,12 +165,14 @@ fastify.get('/.well-known/change-password', async (request, reply) => { { element: 'Subject', value: subject }, { element: 'Link', attributes: self }, { element: 'Link', attributes: profilePage }, - { element: 'Link', attributes: subscribe }); + { element: 'Link', attributes: subscribe }, + { element: 'Alias', value: profileLink }); } else { reply.type(jrd); return { subject, links: [self, profilePage, subscribe], + aliases: [profileLink], }; } }); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 016db6ac19..6f51825494 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -311,7 +311,15 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError(accessDenied); } - if (ep.meta.limit) { + // For endpoints without a limit, the default is 10 calls per second + const endpointLimit: IEndpointMeta['limit'] = ep.meta.limit ?? { + duration: 1000, + max: 10, + }; + + // We don't need this check, but removing it would cause a big merge conflict. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (endpointLimit) { // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. let limitActor: string; if (user) { @@ -320,7 +328,7 @@ export class ApiCallService implements OnApplicationShutdown { limitActor = getIpHash(request.ip); } - const limit = Object.assign({}, ep.meta.limit); + const limit = Object.assign({}, endpointLimit); if (limit.key == null) { (limit as any).key = ep.name; diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index ff8dd73605..b2faf675b0 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Announcement', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts index 6312a0a54c..20ae6cc2e5 100644 --- a/packages/backend/src/server/api/endpoints/announcements/show.ts +++ b/packages/backend/src/server/api/endpoints/announcements/show.ts @@ -27,6 +27,12 @@ export const meta = { id: 'b57b5e1d-4f49-404a-9edb-46b00268f121', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index e0c8ddcc84..9a6caa3317 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -47,6 +47,12 @@ export const meta = { optional: false, nullable: false, ref: 'Antenna', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts index 2258954b56..70a9f819f5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts @@ -24,6 +24,12 @@ export const meta = { id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts index 83d29f9c8c..ed7d1c9daa 100644 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ b/packages/backend/src/server/api/endpoints/antennas/list.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Antenna', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index f4dfe1ecc4..be4cf1e0ca 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -41,6 +41,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts index a40f187d0b..3ca20dfb3e 100644 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ b/packages/backend/src/server/api/endpoints/antennas/show.ts @@ -30,6 +30,12 @@ export const meta = { optional: false, nullable: false, ref: 'Antenna', }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 10f26b1912..919a4cb3f5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -45,6 +45,12 @@ export const meta = { optional: false, nullable: false, ref: 'Antenna', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts index d8c55de7ec..14286bc23e 100644 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ b/packages/backend/src/server/api/endpoints/ap/get.ts @@ -11,6 +11,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; export const meta = { tags: ['federation'], + requireAdmin: true, requireCredential: true, kind: 'read:federation', diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index a877d1ce0d..4232bc6e39 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -140,7 +140,7 @@ export default class extends Endpoint { // eslint- return await this.mergePack( me, isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, - isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null, + isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null, ); } diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index ba847fc4f0..6d595289de 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -22,6 +22,12 @@ export const meta = { optional: false, nullable: false, ref: 'App', }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts index 3db9a0d0d4..a8a3e79af1 100644 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ b/packages/backend/src/server/api/endpoints/app/show.ts @@ -26,6 +26,12 @@ export const meta = { optional: false, nullable: false, ref: 'App', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 2e62f04df0..0000ce16ef 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -26,6 +26,12 @@ export const meta = { id: '9c72d8de-391a-43c1-9d06-08d29efde8df', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index f8ddfdb75c..a0ee1bfc73 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -40,6 +40,12 @@ export const meta = { id: '92f93e63-428e-4f2f-a5a4-39e1407fe998', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts index 13e02a2541..ba7ad04f37 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts @@ -43,6 +43,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts index b490c5832d..8e9aff8058 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts @@ -51,6 +51,12 @@ export const meta = { id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts index 8431fa6b34..ecbfb10d53 100644 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ b/packages/backend/src/server/api/endpoints/blocking/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Blocking', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts index ab877bbe20..5597570f8e 100644 --- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts +++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts @@ -40,6 +40,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts index a1ae9b80a7..7ae5eb3437 100644 --- a/packages/backend/src/server/api/endpoints/channels/favorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts @@ -26,6 +26,12 @@ export const meta = { id: '4938f5f3-6167-4c04-9149-6607b7542861', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts index a9a79ba8fc..24323cbe63 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Channel', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 1812820ba2..5505f3ed24 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -26,6 +26,12 @@ export const meta = { id: 'c0031718-d573-4e85-928e-10039f1fbb68', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts index d2f36f251e..e667b0b881 100644 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ b/packages/backend/src/server/api/endpoints/channels/followed.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Channel', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts index d96e6c3ad2..72a1cc0cf9 100644 --- a/packages/backend/src/server/api/endpoints/channels/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/channels/my-favorites.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Channel', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts index daab685f1b..6e51add6b2 100644 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ b/packages/backend/src/server/api/endpoints/channels/owned.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Channel', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts index ae32203603..9476c494a3 100644 --- a/packages/backend/src/server/api/endpoints/channels/search.ts +++ b/packages/backend/src/server/api/endpoints/channels/search.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Channel', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts index 332ce2c9dc..e9c0c392c0 100644 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ b/packages/backend/src/server/api/endpoints/channels/show.ts @@ -28,6 +28,12 @@ export const meta = { id: '6f6c314b-7486-4897-8966-c04a66a02923', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 06130464a9..0bd01d712c 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -38,6 +38,12 @@ export const meta = { id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts index fc6b75e295..a5db833704 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts @@ -25,6 +25,12 @@ export const meta = { id: '353c68dd-131a-476c-aa99-88a345e83668', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts index 48c5261135..aea34d9d47 100644 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts @@ -26,6 +26,12 @@ export const meta = { id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index dba2938b39..d2a75225ed 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -10,6 +10,7 @@ import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['channels'], @@ -43,6 +44,11 @@ export const meta = { id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index fd21e3d9fe..f6c0c045df 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index cbe792376b..4c5c0d5d20 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index d32bc765a4..8210ec8fe7 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index dad21e9e8e..56a5dbea31 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 68aa12ac0e..7f79e1356d 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index e1979cfe8b..b3660b558b 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index dcb72084b7..716c41f385 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 0a019ce4fb..b67b5ca338 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 06b15bca18..e5587cab86 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index d359b491e2..cbae3a21c1 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 4355aa5348..d734240742 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 1f5f5fea54..6e1a8ebd4f 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -16,6 +16,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index d7c9ea3964..7816ea3ac9 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -18,9 +18,10 @@ export const meta = { kind: 'write:account', + // 60 calls per hour limit: { duration: ms('1hour'), - max: 20, + max: 60, }, errors: { diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index ceebc8ba5e..c2f72ad9ae 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -32,6 +32,12 @@ export const meta = { id: '920f7c2d-6208-4b76-8082-e632020f5883', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts index ca8ff2e1f1..f3f8705c44 100644 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ b/packages/backend/src/server/api/endpoints/clips/delete.ts @@ -22,6 +22,12 @@ export const meta = { id: '70ca08ba-6865-4630-b6fb-8494759aa754', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index 11f8ec3e92..a6b97a6a80 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -32,6 +32,12 @@ export const meta = { id: '92658936-c625-4273-8326-2d790129256e', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts index 2e4a3ff820..cd4c6bb2fc 100644 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ b/packages/backend/src/server/api/endpoints/clips/list.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Clip', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts index 44719592d1..1f9b24e6c9 100644 --- a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Clip', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 943c31c894..6175c4d0e5 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -35,6 +35,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts index 33f9ecd25b..2c503d4af5 100644 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts @@ -30,6 +30,12 @@ export const meta = { id: 'aff017de-190e-434b-893e-33a9ff5049d8', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts index 1078a1b176..949f0e70aa 100644 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ b/packages/backend/src/server/api/endpoints/clips/show.ts @@ -30,6 +30,12 @@ export const meta = { optional: false, nullable: false, ref: 'Clip', }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts index a458fda4a0..e436e5b94e 100644 --- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts @@ -31,6 +31,12 @@ export const meta = { id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 603a3ccf3d..b776f1357d 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -31,6 +31,12 @@ export const meta = { optional: false, nullable: false, ref: 'Clip', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index eb45e29f9e..a05372909c 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -29,6 +29,12 @@ export const meta = { }, }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index d615036ce8..8e821da0da 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index b86059b5e7..32c2620915 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -38,6 +38,12 @@ export const meta = { id: 'c118ece3-2e4b-4296-99d1-51756e32d232', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts index cc7920505f..dfb83efc45 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts @@ -21,6 +21,12 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts index fa6e11da49..7a009b12a1 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts @@ -11,6 +11,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -34,6 +35,12 @@ export const meta = { id: '5eb8d909-2540-4970-90b8-dd6f86088121', }, }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index 090cff6875..e8fba144b5 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 502d42f9e0..377af3db69 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index e8f4539d61..64f6a8b2ba 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -40,6 +40,12 @@ export const meta = { id: '25b73c73-68b1-41d0-bad1-381cfdf6579f', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 1501abf9ce..94a0e673a3 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -11,6 +11,7 @@ import { RoleService } from '@/core/RoleService.js'; import { DriveService } from '@/core/DriveService.js'; import type { Config } from '@/config.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -63,6 +64,12 @@ export const meta = { optional: false, nullable: false, ref: 'DriveFile', }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 9bcd824882..1245706b0d 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'DriveFolder', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts index 85d63873a4..29be2121cd 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts @@ -9,6 +9,7 @@ import type { DriveFoldersRepository, DriveFilesRepository } from '@/models/_.js import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -30,6 +31,12 @@ export const meta = { id: 'b0fc8a17-963c-405d-bfbc-859a487295e1', }, }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts index eb45a30bc0..950aeacea0 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'DriveFolder', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts index a1c0df6697..520d6b1b58 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts @@ -30,6 +30,12 @@ export const meta = { id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 62b04e1df3..cd47c0fc68 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -10,6 +10,7 @@ import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityServi import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['drive'], @@ -43,6 +44,12 @@ export const meta = { optional: false, nullable: false, ref: 'DriveFolder', }, + + // 100 calls per minute + limit: { + duration: 1000 * 60, + max: 100, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts index f7c1ed39b5..4cb0a91546 100644 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ b/packages/backend/src/server/api/endpoints/drive/stream.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'DriveFile', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts index 1d7dacd60e..b4982fc8cf 100644 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ b/packages/backend/src/server/api/endpoints/email-address/available.ts @@ -26,6 +26,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts index ccfbda0d44..45cc4a10f3 100644 --- a/packages/backend/src/server/api/endpoints/emoji.ts +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -22,6 +22,12 @@ export const meta = { optional: false, nullable: false, ref: 'EmojiDetailed', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 46ef4eca1b..3cc7f89ab9 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -32,6 +32,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { @@ -50,16 +56,11 @@ export default class extends Endpoint { // eslint- private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { - const emojis = await this.emojisRepository.find({ - where: { - host: IsNull(), - }, - order: { - category: 'ASC', - name: 'ASC', - }, - }); - + const emojis = await this.emojisRepository.createQueryBuilder() + .where('host IS NULL') + .orderBy('LOWER(category)', 'ASC') + .orderBy('LOWER(name)', 'ASC') + .getMany(); return { emojis: await this.emojiEntityService.packSimpleMany(emojis), }; diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index fe7e9c36f3..7629cd7a67 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -28,6 +28,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 4aedf62a84..8878f5d570 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -26,6 +26,12 @@ export const meta = { '...', ], }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index d5b80035df..e7b528dda1 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -22,6 +22,12 @@ export const meta = { ref: 'Following', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 215f94fbcc..070a1c8407 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -22,6 +22,12 @@ export const meta = { ref: 'Following', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index c1ce3f2238..97384d2498 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'FederationInstance', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts index 2972861a4b..8168f9296f 100644 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts @@ -20,6 +20,12 @@ export const meta = { optional: false, nullable: true, ref: 'FederationInstance', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index 69900bff9a..54d29c2faa 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -50,6 +50,12 @@ export const meta = { otherFollowingCount: { type: 'number' }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts index f8430ef431..3ec9522c44 100644 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts @@ -12,6 +12,12 @@ export const meta = { tags: ['federation'], requireCredential: false, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts index 71b1aeb07b..e3bc2989df 100644 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ b/packages/backend/src/server/api/endpoints/federation/users.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'UserDetailedNotMe', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index ba48b0119e..03f35f16a5 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -203,6 +203,12 @@ export const meta = { }, }, }, + + // 20 calls per 10 seconds + limit: { + duration: 1000 * 10, + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index 6912450abf..1010567113 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['flashs'], @@ -31,6 +32,11 @@ export const meta = { id: '1036ad7b-9f92-4fff-89c3-0e50dc941704', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index c2d6ab5085..2e8cbffe2a 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Flash', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts index e4dc5b61c5..378f9280f7 100644 --- a/packages/backend/src/server/api/endpoints/flash/like.ts +++ b/packages/backend/src/server/api/endpoints/flash/like.ts @@ -38,6 +38,12 @@ export const meta = { id: '010065cf-ad43-40df-8067-abff9f4686e3', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/my-likes.ts b/packages/backend/src/server/api/endpoints/flash/my-likes.ts index 755cc5acfc..22eae381da 100644 --- a/packages/backend/src/server/api/endpoints/flash/my-likes.ts +++ b/packages/backend/src/server/api/endpoints/flash/my-likes.ts @@ -36,6 +36,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/my.ts b/packages/backend/src/server/api/endpoints/flash/my.ts index 5746096232..48f464c337 100644 --- a/packages/backend/src/server/api/endpoints/flash/my.ts +++ b/packages/backend/src/server/api/endpoints/flash/my.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Flash', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/show.ts b/packages/backend/src/server/api/endpoints/flash/show.ts index a6fbd8e76e..03d9710773 100644 --- a/packages/backend/src/server/api/endpoints/flash/show.ts +++ b/packages/backend/src/server/api/endpoints/flash/show.ts @@ -28,6 +28,12 @@ export const meta = { id: 'f0d34a1a-d29a-401d-90ba-1982122b5630', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts index 7869bcdf52..6f45198e67 100644 --- a/packages/backend/src/server/api/endpoints/flash/unlike.ts +++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts @@ -31,6 +31,12 @@ export const meta = { id: '755f25a7-9871-4f65-9f34-51eaad9ae0ac', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts index 2d1446681c..4cf7413001 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts @@ -28,6 +28,12 @@ export const meta = { id: 'bcde4f8b-0913-4614-8881-614e522fb041', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts index 6d663d480c..9bcd726b12 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts @@ -37,6 +37,12 @@ export const meta = { optional: false, nullable: false, ref: 'UserLite', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index fa59e38976..7ec217fdc4 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -42,6 +42,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index 4f78eae677..0c49b27aad 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -23,6 +23,12 @@ export const meta = { id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts index 6325f01bb8..7f99c228f4 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/sent.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/sent.ts @@ -42,6 +42,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts index 7d2878e03f..abbfb9b83b 100644 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts index 4ee252104a..71b979ab9f 100644 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts index d398418ab4..630b6cede5 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts @@ -22,6 +22,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index b6b94db161..68478ba55c 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['gallery'], @@ -31,6 +32,11 @@ export const meta = { id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496', }, }, + + limit: { + duration: ms('1hour'), + max: 300, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index 91e49e6463..e73110648c 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -39,6 +39,12 @@ export const meta = { id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts index bd69898229..fd637febaa 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts @@ -28,6 +28,12 @@ export const meta = { optional: false, nullable: false, ref: 'GalleryPost', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index f44e2c7afc..b0fad1eff2 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -33,6 +33,12 @@ export const meta = { id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 52acee1cfb..adc46b2f63 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -52,6 +52,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts index a57774be73..ede777c890 100644 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts @@ -26,6 +26,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts index 5cd3c6584d..f378c5558e 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Hashtag', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index d4eb851054..ae6c98faf1 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -22,6 +22,12 @@ export const meta = { optional: false, nullable: false, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts index 940e3bd69d..588f76fbb5 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts @@ -29,6 +29,12 @@ export const meta = { id: '110ee688-193e-4a3a-9ecf-c167b2e6981e', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index cb8065e3a6..57db4f3e6e 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -42,6 +42,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index 30f0c1b0c8..eb2289960a 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index d324e3e64a..9347c9ca27 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -30,6 +30,12 @@ export const meta = { kind: 'permission', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index 2a30e8b0c3..34b6907338 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -10,6 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import ms from 'ms'; export const meta = { requireCredential: true, @@ -28,6 +29,12 @@ export const meta = { }, }, }, + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts index bf039ccd16..a1c965f603 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts @@ -10,6 +10,7 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { requireCredential: true, @@ -23,6 +24,12 @@ export const meta = { id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts index deb56a3ac4..eb8a63b3dc 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts @@ -11,6 +11,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { requireCredential: true, @@ -30,6 +31,12 @@ export const meta = { id: '1fb7cb09-d46a-4fff-b8df-057708cce513', }, }, + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 91c8597b1b..661fa257a6 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -49,6 +49,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 0b4faf5ef8..447fd18dcf 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -48,6 +48,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts index 603df44733..ebb25ebf1c 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: true, prohibitMoved: true, kind: 'write:account', + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts index 3558035eca..49d47e1624 100644 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ b/packages/backend/src/server/api/endpoints/i/favorites.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'NoteFavorite', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts index d492585ffa..9baa01ae1e 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts @@ -37,6 +37,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts index 73a6fcc98b..350491a73a 100644 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts index d4c09426a7..19baa9726d 100644 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts @@ -36,6 +36,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts index 1b6359a633..65a19ac5ab 100644 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ b/packages/backend/src/server/api/endpoints/i/pages.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Page', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts index b7cafd74df..e324485277 100644 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ b/packages/backend/src/server/api/endpoints/i/pin.ts @@ -42,6 +42,12 @@ export const meta = { optional: false, nullable: false, ref: 'MeDetailed', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts index d1a8eccb1d..edf570fcc5 100644 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -15,6 +15,12 @@ export const meta = { requireCredential: true, kind: 'write:account', + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts index 4db1ca73c1..4111e90a19 100644 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts @@ -16,6 +16,12 @@ export const meta = { errors: { }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index f1797cfde7..98b993d271 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -14,6 +14,12 @@ export const meta = { res: { type: 'object', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index d53c390460..4a3d38efa3 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -32,6 +32,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts index 3aa256077e..268c650f27 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts @@ -22,6 +22,12 @@ export const meta = { id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index d9a8fdd449..dff33016e0 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -22,7 +22,13 @@ export const meta = { res: { type: 'object', - } + }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index 3fe339606d..8216728549 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -17,6 +17,12 @@ export const meta = { type: 'string', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 28f158c62d..001cd22236 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -17,6 +17,12 @@ export const meta = { type: 'string', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index cf965ba0cf..62338965e0 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -21,6 +21,12 @@ export const meta = { id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts index 67a99b028a..f23335ed46 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -31,7 +31,13 @@ export const meta = { }, }, }, - } + }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 8723035d84..35414ca2a9 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -10,6 +10,12 @@ import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, kind: 'write:account', + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts index c05ee93c6f..07acddaa54 100644 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts @@ -12,6 +12,12 @@ export const meta = { requireCredential: true, secure: true, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts index 76ad0bbe21..8b39b87a7f 100644 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Signin', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts index 74825cf9f3..e88e3fbc01 100644 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ b/packages/backend/src/server/api/endpoints/i/unpin.ts @@ -29,6 +29,12 @@ export const meta = { optional: false, nullable: false, ref: 'MeDetailed', }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8994c3fff6..8e61b8f784 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -539,7 +539,9 @@ export default class extends Endpoint { // eslint- } // フォロワーにUpdateを配信 - this.accountUpdateService.publishToFollowers(user.id); + if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) { + this.accountUpdateService.publishToFollowers(user.id); + } const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://')); for (const url of urls) { @@ -581,4 +583,52 @@ export default class extends Endpoint { // eslint- // なにもしない } } + + // these two methods need to be kept in sync with + // `ApRendererService.renderPerson` + private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial): boolean { + for (const field of ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs'] as (keyof MiUser)[]) { + if ((field in newUser) && oldUser[field] !== newUser[field]) { + return true; + } + } + for (const arrayField of ['emojis', 'tags'] as (keyof MiUser)[]) { + if ((arrayField in newUser) !== (arrayField in oldUser)) { + return true; + } + + const oldArray = oldUser[arrayField] ?? []; + const newArray = newUser[arrayField] ?? []; + if (!Array.isArray(oldArray) || !Array.isArray(newArray)) { + return true; + } + if (oldArray.join("\0") !== newArray.join("\0")) { + return true; + } + } + return false; + } + + private profileNeedsPublishing(oldProfile: MiUserProfile, newProfile: Partial): boolean { + for (const field of ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz'] as (keyof MiUserProfile)[]) { + if ((field in newProfile) && oldProfile[field] !== newProfile[field]) { + return true; + } + } + for (const arrayField of ['fields'] as (keyof MiUserProfile)[]) { + if ((arrayField in newProfile) !== (arrayField in oldProfile)) { + return true; + } + + const oldArray = oldProfile[arrayField] ?? []; + const newArray = newProfile[arrayField] ?? []; + if (!Array.isArray(oldArray) || !Array.isArray(newArray)) { + return true; + } + if (oldArray.join("\0") !== newArray.join("\0")) { + return true; + } + } + return false; + } } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 6e84603f7a..b0c736937c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -55,6 +55,12 @@ export const meta = { latestStatus: { type: 'integer', nullable: true }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts index 1b1ac00670..1b76a01ca9 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -24,6 +24,12 @@ export const meta = { id: 'bae73e5a-5522-4965-ae19-3a8688e71d82', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index 394c178f2a..f1c0f53fec 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -46,6 +46,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 4a0c09ff0c..5c7c84a73c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -52,6 +52,12 @@ export const meta = { latestStatus: { type: 'integer', nullable: true }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 07a25bd82a..632fd2b671 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -26,6 +26,11 @@ export const meta = { }, }, + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index a70b587da7..d661eab364 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -34,6 +34,12 @@ export const meta = { optional: false, nullable: false, ref: 'InviteCode', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts index e960ff9f4e..408200164c 100644 --- a/packages/backend/src/server/api/endpoints/invite/delete.ts +++ b/packages/backend/src/server/api/endpoints/invite/delete.ts @@ -36,6 +36,12 @@ export const meta = { id: '5eb8d909-2540-4970-90b8-dd6f86088121', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts index 2ffd41ae28..8d92c4957c 100644 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -28,6 +28,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index 23aefe83a2..b63b41edd3 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'InviteCode', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 5460635e1d..9774fb55e6 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -19,6 +19,12 @@ export const meta = { { type: 'object', ref: 'MetaDetailed' }, ], }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index fc9a8f3ebe..c42df7ca80 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -27,6 +27,12 @@ export const meta = { }, }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts index d11832858e..1e14bafc87 100644 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/mute/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['account'], @@ -37,6 +38,12 @@ export const meta = { id: '5467d020-daa9-4553-81e1-135c0c35a96d', }, }, + + // 20 calls per hour (match create) + limit: { + duration: ms('1hour'), + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts index 23204f2829..3efbe349e0 100644 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ b/packages/backend/src/server/api/endpoints/mute/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'Muting', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts index c04a92626f..3fb0d1b3b7 100644 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ b/packages/backend/src/server/api/endpoints/my/apps.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'App', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 9938322a2a..f6c37023e1 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -22,6 +22,14 @@ export const meta = { ref: 'Note', }, }, + + // 120 calls per minute + // 200 ms between calls + limit: { + duration: 1000 * 60, + max: 120, + minInterval: 200, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts index 94ec8c37ec..d36d1dfc15 100644 --- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts @@ -30,6 +30,12 @@ export const meta = { id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6c', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index 2654e196b2..e69ba9be7e 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 29cab9f212..b3dcdcef12 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -34,6 +34,12 @@ export const meta = { id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 37bc5cc878..80aea580ec 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -34,6 +34,12 @@ export const meta = { id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 2036facdba..19a6a5af54 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -9,6 +9,7 @@ import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import type { NoteFavoritesRepository } from '@/models/_.js'; import { ApiError } from '../../../error.js'; +import ms from 'ms'; export const meta = { tags: ['notes', 'favorites'], @@ -30,6 +31,12 @@ export const meta = { id: 'b625fc69-635e-45e9-86f4-dbefbef35af5', }, }, + + // 20 calls per hour (match create) + limit: { + duration: ms('1hour'), + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index dcd971360d..4853489827 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index b6604b9798..228793fbf6 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -42,6 +42,12 @@ export const meta = { id: '7a1b9cb6-235b-4e58-9c00-32c1796f502c', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { @@ -103,6 +109,15 @@ export default class extends Endpoint { // eslint- sub.andWhere('latest.is_quote = false'); } + // Select the appropriate collection of users + if (ps.list === 'followers') { + addFollower(sub); + } else if (ps.list === 'following') { + addFollowee(sub); + } else { + addMutual(sub); + } + return sub; }, 'latest', @@ -118,15 +133,6 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.channel', 'channel') ; - // Select the appropriate collection of users - if (ps.list === 'followers') { - addFollower(query); - } else if (ps.list === 'following') { - addFollowee(query); - } else { - addMutual(query); - } - // Limit to files, if requested if (ps.filesOnly) { query.andWhere('note."fileIds" != \'{}\''); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index d660f3fb69..c45fcd7c5c 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -35,6 +35,12 @@ export const meta = { id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { @@ -98,7 +104,7 @@ export default class extends Endpoint { // eslint- } if (!ps.withBots) query.andWhere('user.isBot = FALSE'); - + if (ps.withRenotes === false) { query.andWhere(new Brackets(qb => { qb.where('note.renoteId IS NULL'); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 75be7b9888..3c66154e19 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -49,6 +49,12 @@ export const meta = { id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts index 593463aea0..9068de2865 100644 --- a/packages/backend/src/server/api/endpoints/notes/like.ts +++ b/packages/backend/src/server/api/endpoints/notes/like.ts @@ -34,6 +34,12 @@ export const meta = { id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index d4c806d7e2..1f986079c2 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -43,6 +43,12 @@ export const meta = { id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 5558dd3a8b..38912421a4 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index 4fd6f8682d..33a9c281b3 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -25,6 +25,12 @@ export const meta = { ref: 'Note', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts index b96691f894..69e718fc81 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts @@ -45,6 +45,12 @@ export const meta = { id: '85a5377e-b1e9-4617-b0b9-5bea73331e49', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index f33f49075b..a5014a490f 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -63,6 +63,12 @@ export const meta = { id: '85a5377e-b1e9-4617-b0b9-5bea73331e49', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 7e334df93e..e683cc87bd 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -37,6 +37,12 @@ export const meta = { id: '263fff3d-d0e1-4af4-bea7-8408059b451a', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 0f0dcca605..559ca43eae 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -43,6 +43,12 @@ export const meta = { id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index a88c286f64..15f114266a 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -34,6 +34,13 @@ export const meta = { id: '12908022-2e21-46cd-ba6a-3edaf6093f46', }, }, + + // 100 calls per 10 seconds. + // This is high because the frontend calls this in a tight loop while loading timelines. + limit: { + duration: 1000 * 10, + max: 100, + }, } as const; export const paramDef = { @@ -72,7 +79,7 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('note.renote', 'renote') .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - + if (ps.userId) { query.andWhere("user.id = :userId", { userId: ps.userId }); } diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts index 5f32332a6a..3f0a8157c4 100644 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ b/packages/backend/src/server/api/endpoints/notes/replies.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 2b4885a194..227ac0ebbf 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -27,6 +27,12 @@ export const meta = { ref: 'Note', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index e140436d6b..eca55cd085 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -32,6 +32,12 @@ export const meta = { id: '0b44998d-77aa-4427-80d0-d2c9b8523011', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index f82ba5473d..49c51cb33c 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -29,6 +29,12 @@ export const meta = { id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { @@ -44,7 +50,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, - + private noteEntityService: NoteEntityService, private queryService: QueryService, ) { @@ -56,7 +62,7 @@ export default class extends Endpoint { // eslint- if (me) { this.queryService.generateBlockedUserQuery(query, me); } - + const note = await query.getOne(); if (note === null) { diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 4c1eb86542..448e704528 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -28,6 +28,12 @@ export const meta = { }, }, }, + + // 10 calls per second + limit: { + duration: 1000, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index d94d6cd652..50ce4fb89a 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -24,6 +24,12 @@ export const meta = { id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a', }, }, + + // 10 calls per hour (match create) + limit: { + duration: 1000 * 60 * 60, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index d40a04c1b1..5a46f66f9e 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -32,6 +32,12 @@ export const meta = { ref: 'Note', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 234248db5c..61a511510c 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -46,6 +46,12 @@ export const meta = { id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 87f9b322a6..55cda135e2 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -39,6 +39,12 @@ export const meta = { id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notes/versions.ts b/packages/backend/src/server/api/endpoints/notes/versions.ts index 2b774ae2b0..343417f0e2 100644 --- a/packages/backend/src/server/api/endpoints/notes/versions.ts +++ b/packages/backend/src/server/api/endpoints/notes/versions.ts @@ -28,6 +28,12 @@ export const meta = { id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, } as const; export const paramDef = { @@ -43,7 +49,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.notesRepository) private notesRepository: NotesRepository, - + private getterService: GetterService, private queryService: QueryService, ) { @@ -53,7 +59,7 @@ export default class extends Endpoint { // eslint- .where('note.id = :noteId', { noteId: ps.noteId }); this.queryService.generateVisibilityQuery(query, me); - + const note = await query.getOne(); if (note === null) { diff --git a/packages/backend/src/server/api/endpoints/notifications/flush.ts b/packages/backend/src/server/api/endpoints/notifications/flush.ts index 47c0642fd1..ab78435b89 100644 --- a/packages/backend/src/server/api/endpoints/notifications/flush.ts +++ b/packages/backend/src/server/api/endpoints/notifications/flush.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: true, kind: 'write:notifications', + + // 2 calls per 10 seconds + limit: { + duration: 1000 * 10, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 6565125c00..bc83f8d794 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: true, kind: 'write:notifications', + + // 2 calls per 10 seconds + limit: { + duration: 1000 * 10, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts index ce454ab24a..61f9862734 100644 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ b/packages/backend/src/server/api/endpoints/page-push.ts @@ -22,6 +22,12 @@ export const meta = { id: '4a13ad31-6729-46b4-b9af-e86b265c2e74', }, }, + + // 120 calls per minute + limit: { + duration: 1000 * 60, + max: 120, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index f2bc946788..c2c3215f49 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import ms from 'ms'; export const meta = { tags: ['pages'], @@ -31,6 +32,12 @@ export const meta = { id: '8b741b3e-2c22-44b3-a15f-29949aa1601e', }, }, + + // 300 calls per hour (match update) + limit: { + duration: ms('1hour'), + max: 300, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index a47b69e56e..1886e7be28 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'Page', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts index 11eed693ad..b92faec5e2 100644 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ b/packages/backend/src/server/api/endpoints/pages/like.ts @@ -38,6 +38,12 @@ export const meta = { id: 'd4c1edbe-7da2-4eae-8714-1acfd2d63941', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index e08b832a3f..b763a471fa 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -30,6 +30,12 @@ export const meta = { id: '222120c0-3ead-4528-811b-b96f233388d7', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts index 70c965e0ad..727535bb4e 100644 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts @@ -31,6 +31,12 @@ export const meta = { id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts index e218a8f755..ed6f5207a0 100644 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ b/packages/backend/src/server/api/endpoints/ping.ts @@ -21,6 +21,12 @@ export const meta = { }, }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 5b0b656c63..2e89d81404 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index 9f7d078014..2c31291243 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -24,6 +24,12 @@ export const meta = { id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932', }, }, + + // 10 calls per 2 seconds + limit: { + duration: 1000 * 2, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 1a584b8404..074fcbb56c 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -37,6 +37,12 @@ export const meta = { id: '2e4ef874-8bf0-4b4b-b069-4598f6d05817', }, }, + + // 20 calls per hour (match create) + limit: { + duration: 1000 * 60 * 60, + max: 20, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts index 3be01f989a..aabee237f4 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts @@ -26,6 +26,12 @@ export const meta = { ref: 'RenoteMuting', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 67d5fabd86..bf40e1e3e0 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -20,6 +20,12 @@ export const meta = { errors: { }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 1639b57bc5..d9240dec5e 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -21,6 +21,12 @@ export const meta = { errors: { }, + + // 2 calls per 30 minutes + limit: { + duration: 1000 * 60 * 30, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts index 4695f32042..0215af1e89 100644 --- a/packages/backend/src/server/api/endpoints/retention.ts +++ b/packages/backend/src/server/api/endpoints/retention.ts @@ -44,6 +44,12 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts index dd6f273e01..90e7371f91 100644 --- a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts @@ -14,6 +14,12 @@ export const meta = { errors: { }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts index 6b06068727..2cbdc26f63 100644 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -19,6 +19,12 @@ export const meta = { optional: false, nullable: false, items: { ref: 'ReversiGameLite' }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/reversi/invitations.ts index 5b3b9da75b..b5abad73e2 100644 --- a/packages/backend/src/server/api/endpoints/reversi/invitations.ts +++ b/packages/backend/src/server/api/endpoints/reversi/invitations.ts @@ -19,6 +19,12 @@ export const meta = { optional: false, nullable: false, items: { ref: 'UserLite' }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index aa8b8a7d72..7dd82182fa 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -34,6 +34,12 @@ export const meta = { optional: true, ref: 'ReversiGameDetailed', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts index fc3b96eb51..11931d9d3d 100644 --- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -25,6 +25,12 @@ export const meta = { optional: false, nullable: false, ref: 'ReversiGameDetailed', }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/surrender.ts b/packages/backend/src/server/api/endpoints/reversi/surrender.ts index 75e5372862..2b6af532e5 100644 --- a/packages/backend/src/server/api/endpoints/reversi/surrender.ts +++ b/packages/backend/src/server/api/endpoints/reversi/surrender.ts @@ -32,6 +32,12 @@ export const meta = { id: '6e04164b-a992-4c93-8489-2123069973e1', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/reversi/verify.ts b/packages/backend/src/server/api/endpoints/reversi/verify.ts index 981735a3d7..2f43c91d77 100644 --- a/packages/backend/src/server/api/endpoints/reversi/verify.ts +++ b/packages/backend/src/server/api/endpoints/reversi/verify.ts @@ -30,6 +30,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts index b087aa242b..44dbe97b1d 100644 --- a/packages/backend/src/server/api/endpoints/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Role', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 71f2782a5d..b3c73e0391 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -37,6 +37,12 @@ export const meta = { ref: 'Note', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts index 38477c5e8e..dd9f9481d4 100644 --- a/packages/backend/src/server/api/endpoints/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -28,6 +28,12 @@ export const meta = { optional: false, nullable: false, ref: 'Role', }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 48d350af59..774a6f889b 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -43,6 +43,12 @@ export const meta = { required: ['id', 'user'], }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index 8301c85f2e..e765163f8e 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -63,6 +63,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts index 2a8a461a8f..401d9292bc 100644 --- a/packages/backend/src/server/api/endpoints/sponsors.ts +++ b/packages/backend/src/server/api/endpoints/sponsors.ts @@ -13,6 +13,12 @@ export const meta = { requireCredential: false, requireCredentialPrivateMode: false, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts index 1e6983177f..907dddb9d3 100644 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ b/packages/backend/src/server/api/endpoints/stats.ts @@ -49,6 +49,12 @@ export const meta = { }, }, }, + + // 3 call per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index fd76df2d3c..f447b5598b 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -45,6 +45,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts index 797e4fd34d..c8783cded8 100644 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/show-registration.ts @@ -34,6 +34,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts index 2edf7fab1b..aa7e03dceb 100644 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts @@ -15,6 +15,12 @@ export const meta = { requireCredential: false, description: 'Unregister from receiving push notifications.', + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts index 839a07c770..78b9323b7b 100644 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ b/packages/backend/src/server/api/endpoints/sw/update-registration.ts @@ -43,6 +43,12 @@ export const meta = { id: ' b09d8066-8064-5613-efb6-0e963b21d012', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts index 9231f0ab94..e9ae9df2cc 100644 --- a/packages/backend/src/server/api/endpoints/test.ts +++ b/packages/backend/src/server/api/endpoints/test.ts @@ -40,6 +40,12 @@ export const meta = { }, }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index 4944be9b05..ae7902f912 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -25,6 +25,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index e845853017..089d346cd2 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts index f7139b3684..6c0811d3f0 100644 --- a/packages/backend/src/server/api/endpoints/users/achievements.ts +++ b/packages/backend/src/server/api/endpoints/users/achievements.ts @@ -25,6 +25,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts index 7f7d2ea8cc..a457a6c434 100644 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ b/packages/backend/src/server/api/endpoints/users/clips.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Clip', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts index e01f19ba7a..e6acae08b1 100644 --- a/packages/backend/src/server/api/endpoints/users/featured-notes.ts +++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'Note', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/flashs.ts b/packages/backend/src/server/api/endpoints/users/flashs.ts index e5ea450215..2da46e8747 100644 --- a/packages/backend/src/server/api/endpoints/users/flashs.ts +++ b/packages/backend/src/server/api/endpoints/users/flashs.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Flash', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index a8b4319a61..c1617e14e5 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -44,6 +44,12 @@ export const meta = { id: '3c6a84db-d619-26af-ca14-06232a21df8a', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index feda5bb353..c292c6d6a3 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -51,6 +51,12 @@ export const meta = { id: 'a2b007b9-4782-4eba-abd3-93b05ed4130d', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts index 553886374c..931685e32a 100644 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'GalleryPost', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 9248a2fa68..99568cfa12 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -47,6 +47,12 @@ export const meta = { id: 'e6965129-7b2a-40a4-bae2-cd84cd434822', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index 7e44d501ab..2be3197d88 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -60,6 +60,12 @@ export const meta = { id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index 7daf05ba4e..c3ea392e89 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -37,6 +37,12 @@ export const meta = { id: '0cf21a28-7715-4f39-a20d-777bfdb8d138', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts index dc0d28a0eb..941ce77877 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts @@ -25,6 +25,12 @@ export const meta = { id: '78436795-db79-42f5-b1e2-55ea2cf19166', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts index fd142d5a01..fa898b0dc7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -26,6 +26,12 @@ export const meta = { id: '6425bba0-985b-461e-af1b-518070e72081', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts index 6d6e8d34ea..18373fdf07 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts @@ -54,6 +54,12 @@ export const meta = { }, }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 4241ef1cd0..7f17863a63 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -45,6 +45,12 @@ export const meta = { id: 'ab36de0e-29e9-48cb-9732-d82f1281620d', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 94f06f3bea..1eb4d4ef42 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -35,6 +35,12 @@ export const meta = { id: '588e7f72-c744-4a61-b180-d354e912bda2', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 8756801fe4..c7f4128b56 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -32,6 +32,12 @@ export const meta = { id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts index 3f4bd5af8c..4d38f7d0a7 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -25,6 +25,12 @@ export const meta = { id: '835c4b27-463d-4cfa-969b-a9058678d465', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts index 3948ae1685..0539fadd35 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update-membership.ts @@ -33,6 +33,12 @@ export const meta = { id: '588e7f72-c744-4a61-b180-d354e912bda2', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index a38f84d7b0..ad2f8c02e0 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -32,6 +32,12 @@ export const meta = { id: '796666fe-3dff-4d39-becb-8a5932c1d5b7', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 263d062961..92d8032fa6 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -44,6 +44,12 @@ export const meta = { id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index bb7de0e0b5..3cb958066e 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -24,6 +24,12 @@ export const meta = { ref: 'Page', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 7805ae3288..49c1190197 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -44,6 +44,12 @@ export const meta = { id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805', }, }, + + // 5 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts index 5b3b4527f7..46af1f38ac 100644 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts @@ -29,6 +29,12 @@ export const meta = { ref: 'UserDetailed', }, }, + + // 2 calls per second + limit: { + duration: 1000, + max: 2, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 1d75437b81..e659c46713 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -108,6 +108,12 @@ export const meta = { }, ], }, + + // 10 calls per 2 seconds + limit: { + duration: 1000 * 2, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 5ff6de37d2..81c0c526f0 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -37,6 +37,12 @@ export const meta = { id: '35e166f5-05fb-4f87-a2d5-adb42676d48f', }, }, + + // 10 calls per minute + limit: { + duration: 1000 * 60, + max: 10, + }, } as const; export const paramDef = { @@ -66,10 +72,6 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.cannotReportYourself); } - if (await this.roleService.isAdministrator(targetUser)) { - throw new ApiError(meta.errors.cannotReportAdmin); - } - await this.abuseReportService.report([{ targetUserId: targetUser.id, targetUserHost: targetUser.host, diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 8ff952dcb5..fda56ea6fe 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -23,6 +23,12 @@ export const meta = { ref: 'User', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 0b0136066d..2d17c91e1d 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -28,6 +28,12 @@ export const meta = { ref: 'User', }, }, + + // 3 calls per second + limit: { + duration: 1000, + max: 3, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 062326e28d..7ebca78a7d 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -56,6 +56,12 @@ export const meta = { httpStatusCode: 404, }, }, + + // 5 calls per 2 seconds + limit: { + duration: 1000 * 2, + max: 5, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/users/update-memo.ts b/packages/backend/src/server/api/endpoints/users/update-memo.ts index 5a10de0c40..35369a04e8 100644 --- a/packages/backend/src/server/api/endpoints/users/update-memo.ts +++ b/packages/backend/src/server/api/endpoints/users/update-memo.ts @@ -25,6 +25,12 @@ export const meta = { id: '6fef56f3-e765-4957-88e5-c6f65329b8a5', }, }, + + // 10 calls per second + limit: { + duration: 1000, + max: 10, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index fb5954fee0..82ee0f47d7 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -183,7 +183,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }, ...(endpoint.meta.limit ? { '429': { - description: 'To many requests', + description: 'Too many requests', content: { 'application/json': { schema: { diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 981fbb4353..19dac1dfb8 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from '@misskey-dev/summaly'; import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; +import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -15,9 +16,9 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; import { MiMeta } from '@/models/Meta.js'; -import type { FastifyRequest, FastifyReply } from 'fastify'; -import * as Redis from 'ioredis'; import { RedisKVCache } from '@/misc/cache.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() export class UrlPreviewService { @@ -36,12 +37,13 @@ export class UrlPreviewService { private httpRequestService: HttpRequestService, private loggerService: LoggerService, + private utilityService: UtilityService, ) { this.logger = this.loggerService.getLogger('url-preview'); this.previewCache = new RedisKVCache(this.redisClient, 'summaly', { lifetime: 1000 * 60 * 60 * 24, // 1d memoryCacheLifetime: 1000 * 60 * 10, // 10m - fetcher: (key: string) => { throw new Error('the UrlPreview cache should never fetch'); }, + fetcher: () => { throw new Error('the UrlPreview cache should never fetch'); }, toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), }); @@ -65,7 +67,7 @@ export class UrlPreviewService { reply: FastifyReply, ): Promise { const url = request.query.url; - if (typeof url !== 'string') { + if (typeof url !== 'string' || !URL.canParse(url)) { reply.code(400); return; } @@ -87,6 +89,18 @@ export class UrlPreviewService { }; } + const host = new URL(url).host; + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { + reply.code(403); + return { + error: new ApiError({ + message: 'URL is blocked', + code: 'URL_PREVIEW_BLOCKED', + id: '50294652-857b-4b13-9700-8e5c7a8deae8', + }), + }; + } + const key = `${url}@${lang}`; const cached = await this.previewCache.get(key); if (cached !== undefined) { @@ -170,6 +184,6 @@ export class UrlPreviewService { contentLengthRequired: meta.urlPreviewRequireContentLength, }); - return this.httpRequestService.getJson(`${proxy}?${queryStr}`); + return this.httpRequestService.getJson(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true); } } diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index a7cd0293cd..53cff6bcd3 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -24,13 +24,14 @@ block og meta(property='og:video:url' content= video.url) meta(property='og:video:secure_url' content= video.url) meta(property='og:video:type' content= video.type) + meta(property='og:image' content= video.thumbnailUrl) // FIXME: add width and height // FIXME: add embed player for Twitter if images.length meta(property='twitter:card' content='summary_large_image') each image in images meta(property='og:image' content= image.url) - else + else if !videos.length meta(property='twitter:card' content='summary') meta(property='og:image' content= avatarUrl) diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts new file mode 100644 index 0000000000..d86e794f2f --- /dev/null +++ b/packages/backend/test/unit/UtilityService.ts @@ -0,0 +1,43 @@ +import * as assert from 'assert'; +import { Test } from '@nestjs/testing'; + +import { CoreModule } from '@/core/CoreModule.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { GlobalModule } from '@/GlobalModule.js'; + +describe('UtilityService', () => { + let utilityService: UtilityService; + + beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + utilityService = app.get(UtilityService); + }); + + describe('punyHost', () => { + test('simple', () => { + assert.equal(utilityService.punyHost('http://www.foo.com'), 'www.foo.com'); + }); + test('japanese', () => { + assert.equal(utilityService.punyHost('http://www.新聞.com'), 'www.xn--efvv70d.com'); + }); + }); + + describe('punyHostPSLDomain', () => { + test('simple', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.foo.com'), 'foo.com'); + }); + test('japanese', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com'), 'xn--efvv70d.com'); + }); + test('lower', () => { + assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'), 'foo.github.io'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'), 'bar.github.io'); + }); + test('special', () => { + assert.equal(utilityService.punyHostPSLDomain('http://foo.masto.host'), 'foo.masto.host'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.masto.host'), 'bar.masto.host'); + }); + }); +}); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 53ced3dab3..73d6186edf 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -176,7 +176,7 @@ describe('ActivityPub', () => { resolver.register(actor.id, actor); resolver.register(post.id, post); - const note = await noteService.createNote(post.id, resolver, true); + const note = await noteService.createNote(post.id, undefined, resolver, true); assert.deepStrictEqual(note?.uri, post.id); assert.deepStrictEqual(note.visibility, 'public'); @@ -336,7 +336,7 @@ describe('ActivityPub', () => { resolver.register(actor.featured, featured); resolver.register(firstNote.id, firstNote); - const note = await noteService.createNote(firstNote.id as string, resolver); + const note = await noteService.createNote(firstNote.id as string, undefined, resolver); assert.strictEqual(note?.uri, firstNote.id); }); }); diff --git a/packages/backend/test/unit/misc/check-against-url.ts b/packages/backend/test/unit/misc/check-against-url.ts index 88edaed839..70ee957ab1 100644 --- a/packages/backend/test/unit/misc/check-against-url.ts +++ b/packages/backend/test/unit/misc/check-against-url.ts @@ -3,49 +3,53 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { IObject } from '@/core/activitypub/type.js'; import { describe, expect, test } from '@jest/globals'; +import type { IObject } from '@/core/activitypub/type.js'; import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js'; -function assertOne(activity: IObject) { +function assertOne(activity: IObject, good = 'http://good') { // return a function so we can use `.toThrow` - return () => assertActivityMatchesUrls(activity, ['good']); + return () => assertActivityMatchesUrls(activity, [good]); } describe('assertActivityMatchesUrls', () => { + it('should throw when no ids are URLs', () => { + expect(assertOne({ type: 'Test', id: 'bad' }, 'bad')).toThrow(/bad Activity/); + }); + test('id', () => { - expect(assertOne({ type: 'Test', id: 'bad' })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', id: 'good' })).not.toThrow(); + expect(assertOne({ type: 'Test', id: 'http://bad' })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', id: 'http://good' })).not.toThrow(); }); test('simple url', () => { - expect(assertOne({ type: 'Test', url: 'bad' })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: 'good' })).not.toThrow(); + expect(assertOne({ type: 'Test', url: 'http://bad' })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: 'http://good' })).not.toThrow(); }); test('array of urls', () => { - expect(assertOne({ type: 'Test', url: ['bad'] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: ['bad', 'other'] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: ['good'] })).not.toThrow(); - expect(assertOne({ type: 'Test', url: ['bad', 'good'] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: ['http://bad'] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: ['http://bad', 'http://other'] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: ['http://good'] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: ['http://bad', 'http://good'] })).not.toThrow(); }); test('array of objects', () => { - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, { type: 'Test', href: 'other' }] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'good' }] })).not.toThrow(); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, { type: 'Test', href: 'good' }] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, { type: 'Test', href: 'http://other' }] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://good' }] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, { type: 'Test', href: 'http://good' }] })).not.toThrow(); }); test('mixed array', () => { - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, 'other'] })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'bad' }, 'good'] })).not.toThrow(); - expect(assertOne({ type: 'Test', url: ['bad', { type: 'Test', href: 'good' }] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, 'http://other'] })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', url: [{ type: 'Test', href: 'http://bad' }, 'http://good'] })).not.toThrow(); + expect(assertOne({ type: 'Test', url: ['http://bad', { type: 'Test', href: 'http://good' }] })).not.toThrow(); }); test('id and url', () => { - expect(assertOne({ type: 'Test', id: 'other', url: 'bad' })).toThrow(/bad Activity/); - expect(assertOne({ type: 'Test', id: 'bad', url: 'good' })).not.toThrow(); - expect(assertOne({ type: 'Test', id: 'good', url: 'bad' })).not.toThrow(); + expect(assertOne({ type: 'Test', id: 'http://other', url: 'http://bad' })).toThrow(/bad Activity/); + expect(assertOne({ type: 'Test', id: 'http://bad', url: 'http://good' })).not.toThrow(); + expect(assertOne({ type: 'Test', id: 'http://good', url: 'http://bad' })).not.toThrow(); }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 26de19eaf1..cf97473d14 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -612,8 +612,8 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { username: config.db.user, password: config.db.pass, database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, + synchronize: !justBorrow, + dropSchema: !justBorrow, entities: initEntities ?? entities, }); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1b4b5d5bd1..6198e02f61 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -76,6 +76,9 @@ "vue": "3.5.10", "vuedraggable": "next" }, + "optionalDependencies": { + "cypress": "13.15.0" + }, "devDependencies": { "@misskey-dev/summaly": "5.1.0", "@storybook/addon-actions": "8.3.3", @@ -115,7 +118,6 @@ "@vue/runtime-core": "3.5.10", "acorn": "8.12.1", "cross-env": "7.0.3", - "cypress": "13.15.0", "eslint-plugin-import": "2.30.0", "eslint-plugin-vue": "9.28.0", "fast-glob": "3.3.2", diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index c6f4699b3e..40199c12ff 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -14,7 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.followsYou }} + {{ i18n.ts.mutuals }} + {{ i18n.ts.followsYou }} + {{ i18n.ts.following }}
@@ -119,7 +121,7 @@ onMounted(() => { } const rect = props.source.getBoundingClientRect(); - const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX; + const x = Math.max(1, ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX); const y = rect.top + props.source.offsetHeight + window.scrollY; top.value = y; diff --git a/packages/frontend/src/components/SkFollowingFeedEntry.vue b/packages/frontend/src/components/SkFollowingFeedEntry.vue index 8fa5e014d8..75539f1de7 100644 --- a/packages/frontend/src/components/SkFollowingFeedEntry.vue +++ b/packages/frontend/src/components/SkFollowingFeedEntry.vue @@ -54,6 +54,7 @@ defineEmits<{ overflow-wrap: break-word; display: flex; contain: content; + cursor: pointer; } .avatar { diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue new file mode 100644 index 0000000000..6daa8feba5 --- /dev/null +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -0,0 +1,144 @@ + + +
+ +
{{ i18n.ts.noNotes }}
+
+ + + + + + + + + + diff --git a/packages/frontend/src/components/SkModPlayer.vue b/packages/frontend/src/components/SkModPlayer.vue index 58a96cfb63..6ba983645e 100644 --- a/packages/frontend/src/components/SkModPlayer.vue +++ b/packages/frontend/src/components/SkModPlayer.vue @@ -451,6 +451,7 @@ onDeactivated(() => { overflow: hidden; display: flex; flex-direction: column; + justify-content: center; > i { display: block; diff --git a/packages/frontend/src/components/SkRemoteFollowersWarning.vue b/packages/frontend/src/components/SkRemoteFollowersWarning.vue new file mode 100644 index 0000000000..ceebbd59dd --- /dev/null +++ b/packages/frontend/src/components/SkRemoteFollowersWarning.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue index 2cdb4b6586..7cfa0fb3f6 100644 --- a/packages/frontend/src/components/SkUserRecentNotes.vue +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -101,7 +101,7 @@ onMounted(async () => { margin-bottom: 12px; } -@container (min-width: 451px) { +@container (min-width: 750px) { .userInfo { margin-bottom: 24px; } diff --git a/packages/frontend/src/components/global/SkLazy.vue b/packages/frontend/src/components/global/SkLazy.vue new file mode 100644 index 0000000000..40add97db7 --- /dev/null +++ b/packages/frontend/src/components/global/SkLazy.vue @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index d45f572739..91f74b2cf9 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -7,61 +7,41 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.remoteFollowersWarning }} +
- - - - - - - +
-
+ -
+
@@ -257,7 +158,13 @@ definePageMetadata(() => ({ margin-bottom: 12px; } -@media (min-width: 750px) { +@container (max-width: 749px) { + .user { + display: none; + } +} + +@container (min-width: 750px) { .root { grid-template-columns: min-content 4fr 6fr min-content; grid-template-rows: min-content 1fr; @@ -275,8 +182,4 @@ definePageMetadata(() => ({ margin-bottom: 24px; } } - -.panel { - background: var(--panel); -} diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 279f301d78..f2621f4ad0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -141,8 +141,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - + + + + diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts index 064d6b72e3..39f17949d6 100644 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -3,19 +3,75 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed } from 'vue'; +import { computed, Ref, WritableComputedRef } from 'vue'; import { defaultStore } from '@/store.js'; import { deepMerge } from '@/scripts/merge.js'; import { PageHeaderItem } from '@/types/page-header.js'; import { i18n } from '@/i18n.js'; import { popupMenu } from '@/os.js'; +import { MenuItem } from '@/types/menu.js'; export const followingTab = 'following' as const; export const mutualsTab = 'mutuals' as const; export const followersTab = 'followers' as const; -export type FollowingFeedTab = typeof followingTab | typeof mutualsTab | typeof followersTab; +export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const; +export type FollowingFeedTab = typeof followingFeedTabs[number]; -export function createOptions(): PageHeaderItem { +export function followingTabName(tab: FollowingFeedTab): string; +export function followingTabName(tab: FollowingFeedTab | null | undefined): null; +export function followingTabName(tab: FollowingFeedTab | null | undefined): string | null { + if (tab === followingTab) return i18n.ts.following; + if (tab === followersTab) return i18n.ts.followers; + if (tab === mutualsTab) return i18n.ts.mutuals; + return null; +} + +export function followingTabIcon(tab: FollowingFeedTab | null | undefined): string { + if (tab === followersTab) return 'ph-user ph-bold ph-lg'; + if (tab === mutualsTab) return 'ph-user-switch ph-bold ph-lg'; + return 'ph-user-check ph-bold ph-lg'; +} + +export type FollowingFeedModel = { + [Key in keyof FollowingFeedState]: WritableComputedRef; +} + +export interface FollowingFeedState { + withNonPublic: boolean, + withQuotes: boolean, + withBots: boolean, + withReplies: boolean, + onlyFiles: boolean, + userList: FollowingFeedTab, + remoteWarningDismissed: boolean, +} + +export const defaultFollowingFeedState: FollowingFeedState = { + withNonPublic: false, + withQuotes: false, + withBots: true, + withReplies: false, + onlyFiles: false, + userList: followingTab, + remoteWarningDismissed: false, +}; + +interface StorageInterface = Partial> { + readonly state: Partial; + readonly reactiveState: Ref>; + save(updated: T): void; +} + +export function createHeaderItem(storage?: Ref): PageHeaderItem { + const menu = createOptionsMenu(storage); + return { + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: ev => popupMenu(menu, ev.currentTarget ?? ev.target), + }; +} + +export function createOptionsMenu(storage?: Ref): MenuItem[] { const { userList, withNonPublic, @@ -23,80 +79,83 @@ export function createOptions(): PageHeaderItem { withBots, withReplies, onlyFiles, - } = createModel(); + } = createModel(storage); - return { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: ev => - popupMenu([ - { - type: 'switch', - text: i18n.ts.showNonPublicNotes, - ref: withNonPublic, - disabled: userList.value === 'followers', - }, - { - type: 'switch', - text: i18n.ts.showQuotes, - ref: withQuotes, - }, - { - type: 'switch', - text: i18n.ts.showBots, - ref: withBots, - }, - { - type: 'switch', - text: i18n.ts.showReplies, - ref: withReplies, - disabled: onlyFiles, - }, - { - type: 'divider', - }, - { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: withReplies, - }, - ], ev.currentTarget ?? ev.target), - }; + return [ + { + type: 'switch', + text: i18n.ts.showNonPublicNotes, + ref: withNonPublic, + disabled: computed(() => userList.value === followersTab), + }, + { + type: 'switch', + text: i18n.ts.showQuotes, + ref: withQuotes, + }, + { + type: 'switch', + text: i18n.ts.showBots, + ref: withBots, + }, + { + type: 'switch', + text: i18n.ts.showReplies, + ref: withReplies, + disabled: onlyFiles, + }, + { + type: 'divider', + }, + { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: withReplies, + }, + ]; } -export function createModel() { - const userList = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.userList, +export function createModel(storage?: Ref): FollowingFeedModel { + // eslint-disable-next-line no-param-reassign + storage ??= createDefaultStorage(); + + // Based on timeline.saveTlFilter() + const saveFollowingFilter = (key: K, value: FollowingFeedState[K]) => { + const state = deepMerge(storage.value.state, defaultFollowingFeedState); + const out = deepMerge({ [key]: value }, state); + storage.value.save(out); + }; + + const userList: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList, set: value => saveFollowingFilter('userList', value), }); - - const withNonPublic = computed({ + const withNonPublic: WritableComputedRef = computed({ get: () => { if (userList.value === 'followers') return false; - return defaultStore.reactiveState.followingFeed.value.withNonPublic; + return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic; }, set: value => saveFollowingFilter('withNonPublic', value), }); - const withQuotes = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, + const withQuotes: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, set: value => saveFollowingFilter('withQuotes', value), }); - const withBots = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withBots, + const withBots: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, set: value => saveFollowingFilter('withBots', value), }); - const withReplies = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withReplies, + const withReplies: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, set: value => saveFollowingFilter('withReplies', value), }); - const onlyFiles = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles, + const onlyFiles: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, set: value => saveFollowingFilter('onlyFiles', value), }); - - const remoteWarningDismissed = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed, + const remoteWarningDismissed: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, set: value => saveFollowingFilter('remoteWarningDismissed', value), }); @@ -111,8 +170,12 @@ export function createModel() { }; } -// Based on timeline.saveTlFilter() -function saveFollowingFilter(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) { - const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed); - return defaultStore.set('followingFeed', out); +function createDefaultStorage() { + return computed(() => ({ + state: defaultStore.state.followingFeed, + reactiveState: defaultStore.reactiveState.followingFeed, + save(updated: typeof defaultStore.state.followingFeed) { + return defaultStore.set('followingFeed', updated); + }, + })); } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index d15279d633..090cffe203 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -103,8 +103,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter async function getConfirmed(text: string): Promise { const confirm = await os.confirm({ - type: 'warning', - title: 'confirm', + type: 'question', text, }); diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 9794a300da..89fdda0cbb 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -18,7 +18,7 @@ function isPureObject(value: unknown): value is Record>(value: DeepPartial, def: X): X { +export function deepMerge(value: DeepPartial, def: X): X { if (isPureObject(value) && isPureObject(def)) { const result = deepClone(value as Cloneable) as X; for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index 3dad41a8b3..be49532cf8 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -15,6 +15,8 @@ export function calcPopupPosition(el: HTMLElement, props: { const contentWidth = el.offsetWidth; const contentHeight = el.offsetHeight; + const HORIZONTAL_MARGIN = 1; + let rect: DOMRect; if (props.anchorElement) { @@ -36,9 +38,11 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - 1; + left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; } + left = Math.max(HORIZONTAL_MARGIN, left); + return [left, top]; }; @@ -57,9 +61,11 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - 1; + left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; } + left = Math.max(HORIZONTAL_MARGIN, left); + return [left, top]; }; @@ -75,6 +81,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top = props.y; } + left = Math.max(HORIZONTAL_MARGIN, left); + top -= (el.offsetHeight / 2); if (top + contentHeight - window.scrollY > window.innerHeight) { @@ -106,6 +114,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); } + left = Math.max(HORIZONTAL_MARGIN, left); + if (top + contentHeight - window.scrollY > window.innerHeight) { top = window.innerHeight - contentHeight + window.scrollY - 1; } diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index fd84ad2e4d..bbd9873ad8 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -11,7 +11,7 @@ import darkTheme from '@@/themes/d-ice.json5'; import { miLocalStorage } from './local-storage.js'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; -import type { FollowingFeedTab } from '@/scripts/following-feed-utils.js'; +import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; interface PostFormAction { @@ -244,15 +244,7 @@ export const defaultStore = markRaw(new Storage('base', { }, followingFeed: { where: 'account', - default: { - withNonPublic: false, - withQuotes: false, - withBots: true, - withReplies: false, - onlyFiles: false, - userList: 'following' as FollowingFeedTab, - remoteWarningDismissed: false, - }, + default: defaultFollowingFeedState, }, overridedDeviceKind: { diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index cd4e256056..b42a63e090 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -18,16 +18,21 @@ SPDX-License-Identifier: AGPL-3.0-only :style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }" @wheel.self="onWheel" > - + + + +
{{ i18n.ts._deck.introduction }}
@@ -117,6 +122,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; +import XFollowingColumn from '@/ui/deck/following-column.vue'; import { mainRouter } from '@/router/main.js'; import type { MenuItem } from '@/types/menu.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); @@ -133,6 +139,7 @@ const columnComponents = { mentions: XMentionsColumn, direct: XDirectColumn, roleTimeline: XRoleTimelineColumn, + following: XFollowingColumn, }; mainRouter.navHook = (path, flag): boolean => { diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index eb587554b9..ccc9af8d12 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -4,7 +4,7 @@ */ import { throttle } from 'throttle-debounce'; -import { markRaw } from 'vue'; +import { computed, markRaw, Ref } from 'vue'; import { notificationTypes } from 'misskey-js'; import type { BasicTimelineType } from '@/timelines.js'; import { Storage } from '@/pizzax.js'; @@ -29,6 +29,7 @@ export const columnTypes = [ 'mentions', 'direct', 'roleTimeline', + 'following', ] as const; export type ColumnType = typeof columnTypes[number]; @@ -112,8 +113,8 @@ export const loadDeck = async () => { }; // TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する -export const saveDeck = throttle(1000, () => { - misskeyApi('i/registry/set', { +export const saveDeck = throttle(1000, async () => { + await misskeyApi('i/registry/set', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, value: { @@ -313,7 +314,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat saveDeck(); } -export function updateColumn(id: Column['id'], column: Partial) { +export async function updateColumn(id: Column['id'], column: Partial) { const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const currentColumn = deepClone(deckStore.state.columns[columnIndex]); @@ -322,6 +323,18 @@ export function updateColumn(id: Column['id'], column: Partial) { currentColumn[k] = v; } columns[columnIndex] = currentColumn; - deckStore.set('columns', columns); - saveDeck(); + await Promise.all([ + deckStore.set('columns', columns), + saveDeck(), + ]); +} + +export function getColumn(id: Column['id']): TColumn { + return deckStore.state.columns.find(c => c.id === id) as TColumn; +} + +export function getReactiveColumn(id: Column['id']): Ref { + return computed(() => { + return deckStore.reactiveState.columns.value.find(c => c.id === id) as TColumn; + }); } diff --git a/packages/frontend/src/ui/deck/following-column.vue b/packages/frontend/src/ui/deck/following-column.vue new file mode 100644 index 0000000000..6b8c9db917 --- /dev/null +++ b/packages/frontend/src/ui/deck/following-column.vue @@ -0,0 +1,124 @@ + + + + + + + + + + + diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json index bd9e4fc284..c65797b78c 100644 --- a/packages/megalodon/package.json +++ b/packages/megalodon/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc -p ./", "doc": "typedoc --out ../docs ./src", - "test": "NODE_ENV=test jest -u --maxWorkers=3" + "test": "cross-env NODE_ENV=test jest -u --maxWorkers=3" }, "engines": { "node": ">=15.0.0" diff --git a/packages/megalodon/test/integration/detector.spec.ts b/packages/megalodon/test/integration/detector.spec.ts index 86c32622e9..a7667d1c57 100644 --- a/packages/megalodon/test/integration/detector.spec.ts +++ b/packages/megalodon/test/integration/detector.spec.ts @@ -49,13 +49,14 @@ describe('detector', () => { }) }) - describe('wildebeest', () => { - const url = 'https://wildebeest.mirror-kt.dev' - it('should be mastodon', async () => { - const wildebeest = await detector(url) - expect(wildebeest).toEqual('mastodon') - }) - }) + // This domain no longer resolves, and resolution failures apparently crash jest + // describe('wildebeest', () => { + // const url = 'https://wildebeest.mirror-kt.dev' + // it('should be mastodon', async () => { + // const wildebeest = await detector(url) + // expect(wildebeest).toEqual('mastodon') + // }) + // }) describe('unknown', () => { const url = 'https://google.com' diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index e275e4b475..c821c8eed7 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -10976,7 +10976,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11042,6 +11042,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11096,6 +11102,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11161,6 +11173,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11213,6 +11231,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11259,6 +11283,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11321,6 +11351,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11375,6 +11411,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11442,6 +11484,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11495,7 +11543,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11562,7 +11610,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11624,6 +11672,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11678,6 +11732,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11730,6 +11790,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11787,6 +11853,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11845,6 +11917,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11902,6 +11980,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -11956,7 +12040,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12016,7 +12100,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12080,6 +12164,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12139,7 +12229,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12191,6 +12281,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12243,6 +12339,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12301,6 +12403,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12359,6 +12467,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12413,6 +12527,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12484,6 +12604,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12536,6 +12662,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12599,6 +12731,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12651,6 +12789,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12703,6 +12847,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12749,6 +12899,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12813,6 +12969,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12881,6 +13043,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -12943,6 +13111,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13014,6 +13188,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13081,6 +13261,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13179,6 +13365,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13260,6 +13452,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13327,6 +13525,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13412,6 +13616,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13482,6 +13692,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13551,6 +13767,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13618,6 +13840,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13687,6 +13915,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13741,7 +13975,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -13801,6 +14035,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13857,6 +14097,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13909,6 +14155,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -13955,6 +14207,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14015,6 +14273,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14069,6 +14333,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14126,6 +14396,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14178,6 +14454,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14230,6 +14512,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14276,6 +14564,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14325,6 +14619,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14393,6 +14693,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14453,6 +14759,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14506,6 +14818,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14576,7 +14894,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -14634,6 +14952,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14687,6 +15011,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14745,6 +15075,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14800,6 +15136,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14859,6 +15201,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -14923,7 +15271,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -14994,6 +15342,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15050,7 +15404,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15108,6 +15462,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15166,6 +15526,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15220,6 +15586,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15277,6 +15649,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15336,6 +15714,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15392,6 +15776,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15454,6 +15844,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15500,6 +15896,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15545,7 +15947,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15614,6 +16016,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15677,6 +16085,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15746,6 +16160,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15803,6 +16223,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15855,6 +16281,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15914,6 +16346,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -15973,6 +16411,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16028,7 +16472,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16088,7 +16532,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16151,7 +16595,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16210,7 +16654,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16270,7 +16714,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16328,6 +16772,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16382,6 +16832,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16445,6 +16901,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16508,6 +16970,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16560,6 +17028,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16616,6 +17090,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16662,6 +17142,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16720,6 +17206,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16777,7 +17269,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16835,6 +17327,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16887,6 +17385,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16941,6 +17445,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16993,6 +17503,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17052,7 +17568,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17106,6 +17622,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17162,6 +17684,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17224,6 +17752,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17281,6 +17815,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17334,6 +17874,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17384,6 +17930,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17451,6 +18003,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17497,6 +18055,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17553,6 +18117,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17613,7 +18183,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17671,6 +18241,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17760,7 +18336,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17827,7 +18403,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17886,6 +18462,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17940,7 +18522,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17999,7 +18581,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18069,6 +18651,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18138,6 +18726,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18190,6 +18784,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18244,7 +18844,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18303,7 +18903,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18354,7 +18954,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18405,7 +19005,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18466,7 +19066,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18517,7 +19117,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18568,7 +19168,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18619,7 +19219,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18670,7 +19270,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18721,7 +19321,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18772,7 +19372,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18836,6 +19436,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18898,6 +19504,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18956,6 +19568,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19009,7 +19627,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19069,7 +19687,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19129,7 +19747,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19188,7 +19806,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19247,7 +19865,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19306,7 +19924,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19374,7 +19992,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19442,7 +20060,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19510,6 +20128,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19568,6 +20192,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19622,6 +20252,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19666,6 +20302,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19718,6 +20360,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19770,7 +20418,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19831,6 +20479,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19884,6 +20538,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19943,6 +20603,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19999,6 +20665,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20056,6 +20728,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20111,6 +20789,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20165,6 +20849,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20215,6 +20905,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20270,6 +20966,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20324,6 +21026,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20383,6 +21091,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20437,6 +21151,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20493,7 +21213,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20734,7 +21454,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20794,7 +21514,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20870,6 +21590,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20929,6 +21655,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -20996,6 +21728,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21053,6 +21791,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21105,6 +21849,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21164,7 +21914,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21216,6 +21966,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21268,6 +22024,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21326,6 +22088,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21374,6 +22142,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21428,6 +22202,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21476,6 +22256,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21529,6 +22315,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21589,6 +22381,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21643,7 +22441,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21701,6 +22499,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21759,6 +22563,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21811,7 +22621,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21869,6 +22679,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21927,6 +22743,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -21983,6 +22805,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22047,6 +22875,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22109,6 +22943,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22163,6 +23003,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22221,6 +23067,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22310,7 +23162,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22368,7 +23220,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22426,7 +23278,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22484,6 +23336,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22542,6 +23400,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22617,6 +23481,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22683,6 +23553,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22749,6 +23625,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22825,6 +23707,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22895,6 +23783,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -22956,6 +23850,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23014,6 +23914,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23067,6 +23973,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23119,6 +24031,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23180,6 +24098,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23233,6 +24157,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23285,7 +24215,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -23344,6 +24274,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23408,6 +24344,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23468,6 +24410,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23540,6 +24488,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23615,6 +24569,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23669,6 +24629,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23726,6 +24692,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23778,7 +24750,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -23836,6 +24808,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23910,6 +24888,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -23972,6 +24956,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24026,7 +25016,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24109,6 +25099,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24200,7 +25196,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24260,6 +25256,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24313,7 +25315,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24363,6 +25365,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24407,6 +25415,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24451,7 +25465,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24512,6 +25526,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24585,7 +25605,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24643,6 +25663,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24689,6 +25715,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24741,6 +25773,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24797,6 +25835,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24849,6 +25893,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -24917,7 +25967,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24984,7 +26034,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25042,6 +26092,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25088,6 +26144,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25140,6 +26202,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25194,6 +26262,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25246,6 +26320,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25304,7 +26384,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25368,6 +26448,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25430,6 +26516,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25478,6 +26570,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25524,6 +26622,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25576,6 +26680,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25622,6 +26732,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25676,6 +26792,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25740,6 +26862,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25802,6 +26930,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25854,7 +26988,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25904,6 +27038,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -25956,6 +27096,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26015,6 +27161,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26069,6 +27221,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26131,6 +27289,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26190,6 +27354,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26255,6 +27425,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26306,6 +27482,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26374,6 +27556,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26429,6 +27617,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26502,6 +27696,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26562,6 +27762,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26625,6 +27831,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26689,6 +27901,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26749,6 +27967,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26808,6 +28032,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26866,6 +28096,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26919,6 +28155,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -26971,6 +28213,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27025,6 +28273,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27079,6 +28333,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27133,7 +28393,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -27195,6 +28455,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27247,6 +28513,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27299,6 +28571,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27355,6 +28633,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27410,6 +28694,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27465,6 +28755,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27536,6 +28832,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27616,6 +28918,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27676,6 +28984,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27736,6 +29050,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27798,6 +29118,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27854,6 +29180,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27929,6 +29261,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -27982,6 +29320,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28040,6 +29384,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28104,6 +29454,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28162,6 +29518,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28219,6 +29581,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28273,6 +29641,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28373,6 +29747,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28431,7 +29811,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -28490,6 +29870,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28544,6 +29930,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28599,7 +29991,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -28663,6 +30055,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28715,6 +30113,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28775,6 +30179,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28837,6 +30247,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28883,6 +30299,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28937,6 +30359,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -28989,6 +30417,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -29047,6 +30481,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bda23dfd32..76e399d5dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,10 @@ importers: typescript: specifier: 5.6.2 version: 5.6.2 + optionalDependencies: + cypress: + specifier: 13.14.2 + version: 13.14.2 devDependencies: '@misskey-dev/eslint-plugin': specifier: 2.0.3 @@ -61,9 +65,6 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 - cypress: - specifier: 13.14.2 - version: 13.14.2 eslint: specifier: 9.8.0 version: 9.8.0 @@ -169,6 +170,9 @@ importers: '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 + '@types/psl': + specifier: ^1.1.3 + version: 1.1.3 accepts: specifier: 1.3.8 version: 1.3.8 @@ -364,6 +368,9 @@ importers: proxy-addr: specifier: ^2.0.7 version: 2.0.7 + psl: + specifier: ^1.13.0 + version: 1.13.0 pug: specifier: 3.0.3 version: 3.0.3 @@ -879,6 +886,10 @@ importers: vuedraggable: specifier: next version: 4.1.0(vue@3.5.10(typescript@5.6.2)) + optionalDependencies: + cypress: + specifier: 13.15.0 + version: 13.15.0 devDependencies: '@misskey-dev/summaly': specifier: 5.1.0 @@ -994,9 +1005,6 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 - cypress: - specifier: 13.15.0 - version: 13.15.0 eslint-plugin-import: specifier: 2.30.0 version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) @@ -4833,6 +4841,9 @@ packages: '@types/proxy-addr@2.0.3': resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==} + '@types/psl@1.1.3': + resolution: {integrity: sha512-Iu174JHfLd7i/XkXY6VDrqSlPvTDQOtQI7wNAXKKOAADJ9TduRLkNdMgjGiMxSttUIZnomv81JAbAbC0DhggxA==} + '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} @@ -5305,6 +5316,7 @@ packages: acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -9734,8 +9746,8 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + psl@1.13.0: + resolution: {integrity: sha512-BFwmFXiJoFqlUpZ5Qssolv15DMyc84gTBds1BjsV1BfXEo1UyyD7GsmN67n7J77uRhoSNW1AXtXKPLcBFQn9Aw==} pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -11361,6 +11373,9 @@ packages: vue-component-type-helpers@2.0.16: resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==} + vue-component-type-helpers@2.1.10: + resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==} + vue-component-type-helpers@2.1.6: resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==} @@ -12701,6 +12716,7 @@ snapshots: tough-cookie: 4.1.4 tunnel-agent: 0.6.0 uuid: 8.3.2 + optional: true '@cypress/xvfb@1.2.4(supports-color@8.1.1)': dependencies: @@ -12708,6 +12724,7 @@ snapshots: lodash.once: 4.1.1 transitivePeerDependencies: - supports-color + optional: true '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@4.0.0)': dependencies: @@ -13096,7 +13113,7 @@ snapshots: '@eslint/config-array@0.17.1': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13118,7 +13135,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 espree: 10.1.0 globals: 14.0.0 ignore: 5.3.1 @@ -15114,7 +15131,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.10(typescript@5.6.2) - vue-component-type-helpers: 2.1.6 + vue-component-type-helpers: 2.1.10 '@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)': dependencies: @@ -15641,6 +15658,8 @@ snapshots: dependencies: '@types/node': 20.14.12 + '@types/psl@1.1.3': {} + '@types/pug@2.0.10': {} '@types/punycode@2.1.4': {} @@ -15700,11 +15719,13 @@ snapshots: dependencies: '@types/sinonjs__fake-timers': 8.1.5 - '@types/sinonjs__fake-timers@8.1.1': {} + '@types/sinonjs__fake-timers@8.1.1': + optional: true '@types/sinonjs__fake-timers@8.1.5': {} - '@types/sizzle@2.3.3': {} + '@types/sizzle@2.3.3': + optional: true '@types/stack-utils@2.0.1': {} @@ -16637,7 +16658,8 @@ snapshots: dependencies: tslib: 2.6.3 - astral-regex@2.0.0: {} + astral-regex@2.0.0: + optional: true astring@1.9.0: {} @@ -16651,7 +16673,8 @@ snapshots: asynckit@0.4.0: {} - at-least-node@1.0.0: {} + at-least-node@1.0.0: + optional: true atomic-sleep@1.0.0: {} @@ -16672,9 +16695,11 @@ snapshots: sinon: 16.1.3 tslib: 2.6.2 - aws-sign2@0.7.0: {} + aws-sign2@0.7.0: + optional: true - aws4@1.12.0: {} + aws4@1.12.0: + optional: true axios@0.24.0: dependencies: @@ -16828,7 +16853,8 @@ snapshots: binary-extensions@2.2.0: {} - blob-util@2.0.2: {} + blob-util@2.0.2: + optional: true bluebird@3.7.2: {} @@ -16922,7 +16948,8 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-crc32@0.2.13: {} + buffer-crc32@0.2.13: + optional: true buffer-crc32@1.0.0: {} @@ -16939,6 +16966,7 @@ snapshots: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + optional: true buffer@6.0.3: dependencies: @@ -17026,7 +17054,8 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 - cachedir@2.3.0: {} + cachedir@2.3.0: + optional: true call-bind@1.0.2: dependencies: @@ -17070,7 +17099,8 @@ snapshots: canvas-confetti@1.9.3: {} - caseless@0.12.0: {} + caseless@0.12.0: + optional: true cbor@9.0.2: dependencies: @@ -17218,6 +17248,7 @@ snapshots: cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 + optional: true cli-highlight@2.1.11: dependencies: @@ -17235,11 +17266,13 @@ snapshots: string-width: 4.2.3 optionalDependencies: '@colors/colors': 1.5.0 + optional: true cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 string-width: 4.2.3 + optional: true cli-width@4.1.0: {} @@ -17297,7 +17330,8 @@ snapshots: colord@2.9.3: {} - colorette@2.0.19: {} + colorette@2.0.19: + optional: true combined-stream@1.0.8: dependencies: @@ -17309,7 +17343,8 @@ snapshots: commander@2.20.3: {} - commander@6.2.1: {} + commander@6.2.1: + optional: true commander@7.2.0: {} @@ -17317,7 +17352,8 @@ snapshots: commander@9.5.0: {} - common-tags@1.8.2: {} + common-tags@1.8.2: + optional: true commondir@1.0.1: {} @@ -17573,6 +17609,7 @@ snapshots: tmp: 0.2.3 untildify: 4.0.0 yauzl: 2.10.0 + optional: true cypress@13.15.0: dependencies: @@ -17618,6 +17655,7 @@ snapshots: tmp: 0.2.3 untildify: 4.0.0 yauzl: 2.10.0 + optional: true dashdash@1.14.1: dependencies: @@ -17929,6 +17967,7 @@ snapshots: enquirer@2.3.6: dependencies: ansi-colors: 4.1.3 + optional: true entities@2.2.0: {} @@ -18408,7 +18447,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.0.2 eslint-visitor-keys: 4.0.0 @@ -18484,7 +18523,8 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter2@6.4.7: {} + eventemitter2@6.4.7: + optional: true eventemitter3@4.0.7: {} @@ -18513,6 +18553,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 + optional: true execa@5.1.1: dependencies: @@ -18673,6 +18714,7 @@ snapshots: '@types/yauzl': 2.10.0 transitivePeerDependencies: - supports-color + optional: true extsprintf@1.3.0: {} @@ -18780,6 +18822,7 @@ snapshots: fd-slicer@1.1.0: dependencies: pend: 1.2.0 + optional: true feed@4.2.2: dependencies: @@ -18793,6 +18836,7 @@ snapshots: figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 + optional: true figures@6.1.0: dependencies: @@ -18933,7 +18977,8 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 - forever-agent@0.6.1: {} + forever-agent@0.6.1: + optional: true form-data-encoder@2.1.4: {} @@ -18979,6 +19024,7 @@ snapshots: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 + optional: true fs-minipass@2.1.0: dependencies: @@ -19067,6 +19113,7 @@ snapshots: getos@3.2.1: dependencies: async: 3.2.4 + optional: true getpass@0.1.7: dependencies: @@ -19124,6 +19171,7 @@ snapshots: global-dirs@3.0.1: dependencies: ini: 2.0.0 + optional: true globals@11.12.0: {} @@ -19348,6 +19396,7 @@ snapshots: assert-plus: 1.0.0 jsprim: 2.0.2 sshpk: 1.18.0 + optional: true http2-wrapper@1.0.3: dependencies: @@ -19375,7 +19424,8 @@ snapshots: transitivePeerDependencies: - supports-color - human-signals@1.1.1: {} + human-signals@1.1.1: + optional: true human-signals@2.1.0: {} @@ -19449,7 +19499,8 @@ snapshots: ini@1.3.8: {} - ini@2.0.0: {} + ini@2.0.0: + optional: true insert-text-at-cursor@0.3.0: {} @@ -19542,6 +19593,7 @@ snapshots: is-ci@3.0.1: dependencies: ci-info: 3.7.1 + optional: true is-core-module@2.13.1: dependencies: @@ -19586,6 +19638,7 @@ snapshots: dependencies: global-dirs: 3.0.1 is-path-inside: 3.0.3 + optional: true is-ip@3.1.0: dependencies: @@ -19666,7 +19719,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-typedarray@1.0.0: {} + is-typedarray@1.0.0: + optional: true is-unicode-supported@0.1.0: {} @@ -19697,7 +19751,8 @@ snapshots: isexe@3.1.1: {} - isstream@0.1.2: {} + isstream@0.1.2: + optional: true istanbul-lib-coverage@3.2.2: {} @@ -20296,6 +20351,7 @@ snapshots: extsprintf: 1.3.0 json-schema: 0.4.0 verror: 1.10.0 + optional: true jsrsasign@11.1.0: {} @@ -20382,6 +20438,7 @@ snapshots: wrap-ansi: 7.0.0 optionalDependencies: enquirer: 2.3.6 + optional: true local-pkg@0.5.0: dependencies: @@ -20406,7 +20463,8 @@ snapshots: lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} + lodash.once@4.1.1: + optional: true lodash.uniq@4.5.0: {} @@ -20423,6 +20481,7 @@ snapshots: cli-cursor: 3.1.0 slice-ansi: 4.0.0 wrap-ansi: 6.2.0 + optional: true longest-streak@3.1.0: {} @@ -21400,7 +21459,8 @@ snapshots: os-utils@0.0.14: {} - ospath@1.2.2: {} + ospath@1.2.2: + optional: true otpauth@9.3.2: dependencies: @@ -21551,9 +21611,11 @@ snapshots: peek-readable@5.2.0: {} - pend@1.2.0: {} + pend@1.2.0: + optional: true - performance-now@2.1.0: {} + performance-now@2.1.0: + optional: true pg-cloudflare@1.1.1: optional: true @@ -21872,7 +21934,8 @@ snapshots: prettier@3.3.3: {} - pretty-bytes@5.6.0: {} + pretty-bytes@5.6.0: + optional: true pretty-format@27.5.1: dependencies: @@ -21950,7 +22013,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.0.0: {} + proxy-from-env@1.0.0: + optional: true proxy-from-env@1.1.0: {} @@ -21962,7 +22026,9 @@ snapshots: pseudomap@1.0.2: {} - psl@1.9.0: {} + psl@1.13.0: + dependencies: + punycode: 2.3.1 pstree.remy@1.1.8: {} @@ -22308,6 +22374,7 @@ snapshots: request-progress@3.0.0: dependencies: throttleit: 1.0.0 + optional: true require-directory@2.1.1: {} @@ -22357,6 +22424,7 @@ snapshots: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 + optional: true ret@0.5.0: {} @@ -22364,7 +22432,8 @@ snapshots: reusify@1.0.4: {} - rfdc@1.3.0: {} + rfdc@1.3.0: + optional: true rfdc@1.4.1: {} @@ -22744,12 +22813,14 @@ snapshots: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + optional: true slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + optional: true slick@1.12.2: {} @@ -22849,6 +22920,7 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 tweetnacl: 0.14.5 + optional: true ssri@10.0.4: dependencies: @@ -23145,7 +23217,8 @@ snapshots: throttle-debounce@5.0.2: {} - throttleit@1.0.0: {} + throttleit@1.0.0: + optional: true through@2.3.8: {} @@ -23197,7 +23270,7 @@ snapshots: tough-cookie@4.1.4: dependencies: - psl: 1.9.0 + psl: 1.13.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 @@ -23306,6 +23379,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 + optional: true tweetnacl@0.14.5: {} @@ -23523,7 +23597,8 @@ snapshots: webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 - untildify@4.0.0: {} + untildify@4.0.0: + optional: true update-browserslist-db@1.0.13(browserslist@4.22.2): dependencies: @@ -23570,7 +23645,8 @@ snapshots: uuid@10.0.0: {} - uuid@8.3.2: {} + uuid@8.3.2: + optional: true uuid@9.0.1: {} @@ -23766,6 +23842,8 @@ snapshots: vue-component-type-helpers@2.0.16: {} + vue-component-type-helpers@2.1.10: {} + vue-component-type-helpers@2.1.6: {} vue-demi@0.14.7(vue@3.5.10(typescript@5.6.2)): @@ -24089,6 +24167,7 @@ snapshots: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + optional: true yocto-queue@0.1.0: {} diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 163fd0b0ae..1f9b6ef4f5 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -397,3 +397,8 @@ _auth: allowed: "Allowed" _announcement: new: "New" +_deck: + _columns: + following: "Following" + +selectFollowRelationship: "Select a follow relationship..." diff --git a/sharkey-service.nix b/sharkey-service.nix new file mode 100644 index 0000000000..e6e004c8d1 --- /dev/null +++ b/sharkey-service.nix @@ -0,0 +1,418 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.sharkey; + settingsFormat = pkgs.formats.yaml { }; + redisType = lib.types.submodule { + freeformType = lib.types.attrsOf settingsFormat.type; + options = { + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "The Redis host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 6379; + description = "The Redis port."; + }; + }; + }; + settings = lib.mkOption { + description = '' + Configuration for Misskey, see + [`example.yml`](https://github.com/sharkey-dev/sharkey/blob/develop/.config/example.yml) + for all supported options. + ''; + type = lib.types.submodule { + freeformType = lib.types.attrsOf settingsFormat.type; + options = { + url = lib.mkOption { + type = lib.types.str; + example = "https://example.tld/"; + description = '' + The final user-facing URL. Do not change after running Misskey for the first time. + + This needs to match up with the configured reverse proxy and is automatically configured when using `services.sharkey.reverseProxy`. + ''; + }; + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "The port your Misskey server should listen on."; + }; + socket = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/path/to/sharkey.sock"; + description = "The UNIX socket your Misskey server should listen on."; + }; + chmodSocket = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "777"; + description = "The file access mode of the UNIX socket."; + }; + db = lib.mkOption { + description = "Database settings."; + type = lib.types.submodule { + options = { + host = lib.mkOption { + type = lib.types.str; + default = "/var/run/postgresql"; + example = "localhost"; + description = "The PostgreSQL host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 5432; + description = "The PostgreSQL port."; + }; + db = lib.mkOption { + type = lib.types.str; + default = "sharkey"; + description = "The database name."; + }; + user = lib.mkOption { + type = lib.types.str; + default = "sharkey"; + description = "The user used for database authentication."; + }; + pass = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The password used for database authentication."; + }; + disableCache = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to disable caching queries."; + }; + extra = lib.mkOption { + type = lib.types.nullOr (lib.types.attrsOf settingsFormat.type); + default = null; + example = { + ssl = true; + }; + description = "Extra connection options."; + }; + }; + }; + default = { }; + }; + redis = lib.mkOption { + type = redisType; + default = { }; + description = "`ioredis` options. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + redisForPubsub = lib.mkOption { + type = lib.types.nullOr redisType; + default = null; + description = "`ioredis` options for pubsub. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + redisForJobQueue = lib.mkOption { + type = lib.types.nullOr redisType; + default = null; + description = "`ioredis` options for the job queue. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + redisForTimelines = lib.mkOption { + type = lib.types.nullOr redisType; + default = null; + description = "`ioredis` options for timelines. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference."; + }; + meilisearch = lib.mkOption { + description = "Meilisearch connection options."; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "The Meilisearch host."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 7700; + description = "The Meilisearch port."; + }; + apiKey = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The Meilisearch API key."; + }; + ssl = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to connect via SSL."; + }; + index = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Meilisearch index to use."; + }; + scope = lib.mkOption { + type = lib.types.enum [ + "local" + "global" + ]; + default = "local"; + description = "The search scope."; + }; + }; + } + ); + default = null; + }; + id = lib.mkOption { + type = lib.types.enum [ + "aid" + "aidx" + "meid" + "ulid" + "objectid" + ]; + default = "aidx"; + description = "The ID generation method to use. Do not change after starting Misskey for the first time."; + }; + }; + }; + }; +in + +{ + options = { + services.sharkey = { + enable = lib.mkEnableOption "sharkey"; + package = lib.mkPackageOption pkgs "sharkey" { }; + inherit settings; + database = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create the PostgreSQL database locally. Sets `services.sharkey.settings.db.{db,host,port,user,pass}`."; + }; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "The path to a file containing the database password. Sets `services.sharkey.settings.db.pass`."; + }; + }; + redis = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create and use a local Redis instance. Sets `services.sharkey.settings.redis.host`."; + }; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "The path to a file containing the Redis password. Sets `services.sharkey.settings.redis.pass`."; + }; + }; + meilisearch = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Create and use a local Meilisearch instance. Sets `services.sharkey.settings.meilisearch.{host,port,ssl}`."; + }; + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "The path to a file containing the Meilisearch API key. Sets `services.sharkey.settings.meilisearch.apiKey`."; + }; + }; + reverseProxy = { + enable = lib.mkEnableOption "a HTTP reverse proxy for Misskey"; + webserver = lib.mkOption { + type = lib.types.attrTag { + nginx = lib.mkOption { + type = lib.types.submodule (import ../web-servers/nginx/vhost-options.nix); + default = { }; + description = '' + Extra configuration for the nginx virtual host of Misskey. + Set to `{ }` to use the default configuration. + ''; + }; + caddy = lib.mkOption { + type = lib.types.submodule ( + import ../web-servers/caddy/vhost-options.nix { cfg = config.services.caddy; } + ); + default = { }; + description = '' + Extra configuration for the caddy virtual host of Misskey. + Set to `{ }` to use the default configuration. + ''; + }; + }; + description = "The webserver to use as the reverse proxy."; + }; + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + The fully qualified domain name to bind to. Sets `services.sharkey.settings.url`. + + This is required when using `services.sharkey.reverseProxy.enable = true`. + ''; + example = "sharkey.example.com"; + default = null; + }; + ssl = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + description = '' + Whether to enable SSL for the reverse proxy. Sets `services.sharkey.settings.url`. + + This is required when using `services.sharkey.reverseProxy.enable = true`. + ''; + example = true; + default = null; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = + cfg.reverseProxy.enable -> ((cfg.reverseProxy.host != null) && (cfg.reverseProxy.ssl != null)); + message = "`services.sharkey.reverseProxy.enable` requires `services.sharkey.reverseProxy.host` and `services.sharkey.reverseProxy.ssl` to be set."; + } + ]; + + services.sharkey.settings = lib.mkMerge [ + (lib.mkIf cfg.database.createLocally { + db = { + db = lib.mkDefault "sharkey"; + # Use unix socket instead of localhost to allow PostgreSQL peer authentication, + # required for `services.postgresql.ensureUsers` + host = lib.mkDefault "/var/run/postgresql"; + port = lib.mkDefault config.services.postgresql.settings.port; + user = lib.mkDefault "sharkey"; + pass = lib.mkDefault null; + }; + }) + (lib.mkIf (cfg.database.passwordFile != null) { db.pass = lib.mkDefault "@DATABASE_PASSWORD@"; }) + (lib.mkIf cfg.redis.createLocally { redis.host = lib.mkDefault "localhost"; }) + (lib.mkIf (cfg.redis.passwordFile != null) { redis.pass = lib.mkDefault "@REDIS_PASSWORD@"; }) + (lib.mkIf cfg.meilisearch.createLocally { + meilisearch = { + host = lib.mkDefault "localhost"; + port = lib.mkDefault config.services.meilisearch.listenPort; + ssl = lib.mkDefault false; + }; + }) + (lib.mkIf (cfg.meilisearch.keyFile != null) { + meilisearch.apiKey = lib.mkDefault "@MEILISEARCH_KEY@"; + }) + (lib.mkIf cfg.reverseProxy.enable { + url = lib.mkDefault "${ + if cfg.reverseProxy.ssl then "https" else "http" + }://${cfg.reverseProxy.host}"; + }) + ]; + + systemd.services.sharkey = { + after = [ + "network-online.target" + "postgresql.service" + ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + MISSKEY_CONFIG_YML = "/run/sharkey/default.yml"; + }; + preStart = + '' + install -m 700 ${settingsFormat.generate "sharkey-config.yml" cfg.settings} /run/sharkey/default.yml + '' + + (lib.optionalString (cfg.database.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@DATABASE_PASSWORD@' "${cfg.database.passwordFile}" /run/sharkey/default.yml + '') + + (lib.optionalString (cfg.redis.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@REDIS_PASSWORD@' "${cfg.redis.passwordFile}" /run/sharkey/default.yml + '') + + (lib.optionalString (cfg.meilisearch.keyFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@MEILISEARCH_KEY@' "${cfg.meilisearch.keyFile}" /run/sharkey/default.yml + ''); + serviceConfig = { + ExecStart = "${cfg.package}/bin/sharkey migrateandstart"; + RuntimeDirectory = "sharkey"; + RuntimeDirectoryMode = "700"; + StateDirectory = "sharkey"; + StateDirectoryMode = "700"; + TimeoutSec = 60; + DynamicUser = true; + User = "sharkey"; + LockPersonality = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectProc = "invisible"; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; + }; + }; + + services.postgresql = lib.mkIf cfg.database.createLocally { + enable = true; + ensureDatabases = [ "sharkey" ]; + ensureUsers = [ + { + name = "sharkey"; + ensureDBOwnership = true; + } + ]; + }; + + services.redis.servers = lib.mkIf cfg.redis.createLocally { + sharkey = { + enable = true; + port = cfg.settings.redis.port; + }; + }; + + services.meilisearch = lib.mkIf cfg.meilisearch.createLocally { enable = true; }; + + services.caddy = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? caddy) { + enable = true; + virtualHosts.${cfg.settings.url} = lib.mkMerge [ + cfg.reverseProxy.webserver.caddy + { + hostName = lib.mkDefault cfg.settings.url; + extraConfig = '' + reverse_proxy localhost:${toString cfg.settings.port} + ''; + } + ]; + }; + + services.nginx = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? nginx) { + enable = true; + virtualHosts.${cfg.reverseProxy.host} = lib.mkMerge [ + cfg.reverseProxy.webserver.nginx + { + locations."/" = { + proxyPass = lib.mkDefault "http://localhost:${toString cfg.settings.port}"; + proxyWebsockets = lib.mkDefault true; + recommendedProxySettings = lib.mkDefault true; + }; + } + (lib.mkIf (cfg.reverseProxy.ssl != null) { forceSSL = lib.mkDefault cfg.reverseProxy.ssl; }) + ]; + }; + }; + + meta = { + maintainers = [ lib.maintainers.feathecutie ]; + }; +} diff --git a/sharkey.nix b/sharkey.nix new file mode 100644 index 0000000000..aca87ca223 --- /dev/null +++ b/sharkey.nix @@ -0,0 +1,124 @@ +{ + stdenv, + lib, + nixosTests, + fetchFromGitHub, + nodejs, + pnpm, + makeWrapper, + python3, + bash, + jemalloc, + ffmpeg-headless, + writeShellScript, + xcbuild, + ... +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "sharkey"; + + version = "2024.10.0"; + + src = fetchGit { + url = "https://activitypub.software/TransFem-org/Sharkey.git"; + rev = "150d949a3ec2b5162e2dfda10c2cc5dddea8c59a"; + submodules = true; + }; + + nativeBuildInputs = [ + nodejs + pnpm.configHook + makeWrapper + python3 + ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ xcbuild.xcrun ]; + + # https://nixos.org/manual/nixpkgs/unstable/#javascript-pnpm + pnpmDeps = pnpm.fetchDeps { + inherit (finalAttrs) pname version src; + hash = "sha256-PpXmNBO4pWj8AG0we4DpPhzfx/18rwDZHi86+esFghM="; + }; + + buildPhase = '' + runHook preBuild + + # https://github.com/NixOS/nixpkgs/pull/296697/files#r1617546739 + ( + cd node_modules/.pnpm/node_modules/v-code-diff + pnpm run postinstall + ) + + # https://github.com/NixOS/nixpkgs/pull/296697/files#r1617595593 + export npm_config_nodedir=${nodejs} + ( + cd node_modules/.pnpm/node_modules/re2 + pnpm run rebuild + ) + ( + cd node_modules/.pnpm/node_modules/sharp + pnpm run install + ) + + pnpm build + + runHook postBuild + ''; + + installPhase = + let + checkEnvVarScript = writeShellScript "sharkey-check-env-var" '' + if [[ -z $MISSKEY_CONFIG_YML ]]; then + echo "MISSKEY_CONFIG_YML must be set to the location of the Misskey config file." + exit 1 + fi + ''; + in + '' + runHook preInstall + + mkdir -p $out/data + cp -r . $out/data + + # Set up symlink for use at runtime + # TODO: Find a better solution for this (potentially patch Misskey to make this configurable?) + # Line that would need to be patched: https://github.com/sharkey-dev/sharkey/blob/9849aab40283cbde2184e74d4795aec8ef8ccba3/packages/backend/src/core/InternalStorageService.ts#L18 + # Otherwise, maybe somehow bindmount a writable directory into /data/files. + ln -s /var/lib/sharkey $out/data/files + + makeWrapper ${pnpm}/bin/pnpm $out/bin/sharkey \ + --run "${checkEnvVarScript} || exit" \ + --chdir $out/data \ + --add-flags run \ + --set-default NODE_ENV production \ + --prefix PATH : ${ + lib.makeBinPath [ + nodejs + pnpm + bash + ] + } \ + --prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ + jemalloc + ffmpeg-headless + stdenv.cc.cc + ] + } + + runHook postInstall + ''; + + passthru = { + inherit (finalAttrs) pnpmDeps; + tests.sharkey = nixosTests.sharkey; + }; + + meta = { + description = "🌎 An interplanetary microblogging platform 🚀"; + homepage = "https://sharkey-hub.net/"; + license = lib.licenses.agpl3Only; + maintainers = [ lib.maintainers.feathecutie ]; + platforms = lib.platforms.unix; + mainProgram = "sharkey"; + }; +})