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