forked from FoundKeyGang/FoundKey
resolve merge conflicts
This commit is contained in:
commit
5d99ccacfd
175 changed files with 932 additions and 794 deletions
2
COPYING
2
COPYING
|
@ -1,6 +1,6 @@
|
||||||
Unless otherwise stated this repository is
|
Unless otherwise stated this repository is
|
||||||
Copyright © 2014-2022 syuilo and contributors
|
Copyright © 2014-2022 syuilo and contributors
|
||||||
Copyright © 2022 FoundKey contributors
|
Copyright © 2022-2023 FoundKey contributors
|
||||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||||
(You may be able to run `git shortlog -se` to see a full list of authors.)
|
(You may be able to run `git shortlog -se` to see a full list of authors.)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ Note: this document is historical.
|
||||||
Everything starting with the next section is the original "idea" document that led to the foundation of FoundKey.
|
Everything starting with the next section is the original "idea" document that led to the foundation of FoundKey.
|
||||||
|
|
||||||
For the current status you should see the following:
|
For the current status you should see the following:
|
||||||
* The Behavioral Fixes [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/3)
|
* Issues labeled with [behaviour-fix](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=44)
|
||||||
* The Technological Upkeep [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/4)
|
* Issues labeled with [upkeep](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=43)
|
||||||
* The Features [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/5)
|
* Issues labeled with [feature](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=42)
|
||||||
|
|
||||||
## Misskey Goals
|
## Misskey Goals
|
||||||
I’ve been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Let’s talk about that.
|
I’ve been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Let’s talk about that.
|
||||||
|
|
|
@ -70,6 +70,8 @@ Build foundkey with the following:
|
||||||
|
|
||||||
`NODE_ENV=production yarn build`
|
`NODE_ENV=production yarn build`
|
||||||
|
|
||||||
|
If your system has at least 4GB of RAM, run `NODE_ENV=production yarn build-parallel` to speed up build times.
|
||||||
|
|
||||||
If you're still encountering errors about some modules, use node-gyp:
|
If you're still encountering errors about some modules, use node-gyp:
|
||||||
|
|
||||||
1. `npx node-gyp configure`
|
1. `npx node-gyp configure`
|
||||||
|
@ -182,6 +184,7 @@ Use git to pull in the latest changes and rerun the build and migration commands
|
||||||
git pull
|
git pull
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
yarn install
|
yarn install
|
||||||
|
# Use build-parallel if your system has 4GB or more RAM and want faster builds
|
||||||
NODE_ENV=production yarn build
|
NODE_ENV=production yarn build
|
||||||
yarn migrate
|
yarn migrate
|
||||||
```
|
```
|
||||||
|
|
103
docs/moderation.md
Normal file
103
docs/moderation.md
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# User moderation
|
||||||
|
|
||||||
|
A lot of the user moderation activities can be found on the `user-info` page. You can reach this page by going to a users profile page, open the three dot menu, select "About" and navigating to the "Moderation" section of the page that opens.
|
||||||
|
With the necessary privileges, this page will allow you to:
|
||||||
|
- Toggle whether a user is a moderator (administrators on local users only)
|
||||||
|
- Reset the users password (local users only)
|
||||||
|
- Delete a user (administrators only)
|
||||||
|
- Delete all files of a user
|
||||||
|
For remote users, cached files (if any) will be deleted.
|
||||||
|
- Silence a user
|
||||||
|
This disallows a user from making a note with `public` visibility.
|
||||||
|
If necessary the visibility of incoming notes or locally created notes will be lowered.
|
||||||
|
- Suspend a user
|
||||||
|
This will drop any incoming activities of this actor and hide them from public view on this instance.
|
||||||
|
|
||||||
|
# Administrator
|
||||||
|
|
||||||
|
When an instance is first set up, the initial user to be created will be made an administrator by default.
|
||||||
|
This means that typically the instance owner is the administrator.
|
||||||
|
It is also possible to have multiple administrators, however making a user an administrator is not implemented in the client.
|
||||||
|
To make a user an administrator, you will need access to the database.
|
||||||
|
This is intended for security reasons of
|
||||||
|
1. not exposing this very dangerous functionality via the API
|
||||||
|
2. making sure someone that has shell access to the server anyway "approves" this.
|
||||||
|
|
||||||
|
To make a user an administrator, you will first need the user's ID.
|
||||||
|
To get it you can go to the user's profile page, open the three dot menu, select "About" and copy the ID displayed there.
|
||||||
|
Then, go to the database and run the following query, replacing `<ID>` with the ID gotten above.
|
||||||
|
```sql
|
||||||
|
UPDATE "user" SET "isAdmin" = true WHERE "id" = '<ID>';
|
||||||
|
```
|
||||||
|
|
||||||
|
The user that was made administrator may need to reload their client to see the changes take effect.
|
||||||
|
|
||||||
|
To demote a user, you can do a similar operation, but instead with `... SET "isAdmin" = false ...`.
|
||||||
|
|
||||||
|
## Immunity
|
||||||
|
|
||||||
|
- Cannot be reported by local users.
|
||||||
|
- Cannot have their password reset.
|
||||||
|
To see how you can reset an administrator password, see below.
|
||||||
|
- Cannot have their account deleted.
|
||||||
|
- Cannot be suspended.
|
||||||
|
- Cannot be silenced.
|
||||||
|
- Cannot have their account details viewed by moderators.
|
||||||
|
- Cannot be made moderators.
|
||||||
|
|
||||||
|
## Abilities
|
||||||
|
|
||||||
|
- Create or delete user accounts.
|
||||||
|
- Add or remove moderators.
|
||||||
|
- View and change instance configuration (e.g. Translation API keys).
|
||||||
|
- View all followers and followees.
|
||||||
|
|
||||||
|
Administrators also have the same ability as moderators.
|
||||||
|
Note of course that people with access to the server and/or database access can do basically anything without restrictions (including breaking the instance).
|
||||||
|
|
||||||
|
## Resetting an administrators password
|
||||||
|
|
||||||
|
Administrators are blocked from the paths of resetting the password by moderators or administrators.
|
||||||
|
However, if your server has email configured you should be able to use the "Forgot password" link on the normal signin dialog.
|
||||||
|
|
||||||
|
If you did not set up email, you will need to kick of this process instead through modifying the database yourself.
|
||||||
|
You will need the user ID whose password should be reset, indicated in the following as `<USERID>`;
|
||||||
|
as well as a random string (a UUID would be recommended) indicated as `<TOKEN>`.
|
||||||
|
|
||||||
|
Replacing the two terms above, run the following SQL query:
|
||||||
|
```sql
|
||||||
|
INSERT INTO "password_reset_request" VALUES ('0000000000', now(), '<TOKEN>', '<USERID>');
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, navigate to `/reset-password/<TOKEN>` on your instance to finish the password reset process.
|
||||||
|
After that you should be able to sign in with the new password you just set.
|
||||||
|
|
||||||
|
# Moderator
|
||||||
|
|
||||||
|
A moderator has fewer privileges than an administrator.
|
||||||
|
They can also be more easily added or removed by an adminstrator.
|
||||||
|
Having moderators may be a good idea to help with user moderation.
|
||||||
|
|
||||||
|
## Immunity
|
||||||
|
|
||||||
|
- Cannot be reported by local users.
|
||||||
|
- Cannot be suspended.
|
||||||
|
|
||||||
|
## Abilities
|
||||||
|
|
||||||
|
- Suspend users.
|
||||||
|
- Add, list and remove relays.
|
||||||
|
- View queue, database and server information.
|
||||||
|
- Create, edit, delete, export and import local custom emoji.
|
||||||
|
- View global, social and local timelines even if disabled by administrators.
|
||||||
|
- Show, update and delete any users files and file metadata.
|
||||||
|
Managing emoji is described in [a separate file](emoji.md).
|
||||||
|
- Delete any users notes.
|
||||||
|
- Create an invitation.
|
||||||
|
This allows users to register an account even if (public) registrations are closed using an invite code.
|
||||||
|
- View users' account details.
|
||||||
|
- Suspend and unsuspend users.
|
||||||
|
- Silence and unsilence users.
|
||||||
|
- Handle reports.
|
||||||
|
- Create, update and delete announcements.
|
||||||
|
- View the moderation log.
|
|
@ -501,6 +501,7 @@ scratchpadDescription: "The Scratchpad provides an environment for AiScript expe
|
||||||
\ in it."
|
\ in it."
|
||||||
output: "Output"
|
output: "Output"
|
||||||
updateRemoteUser: "Update remote user information"
|
updateRemoteUser: "Update remote user information"
|
||||||
|
deleteAllFiles: "Delete all files"
|
||||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||||
removeAllFollowing: "Unfollow all followed users"
|
removeAllFollowing: "Unfollow all followed users"
|
||||||
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn workspaces foreach --parallel --topological run build && yarn run gulp",
|
"build": "yarn workspaces foreach --topological run build && yarn run gulp",
|
||||||
|
"build-parallel": "yarn workspaces foreach --parallel --topological run build && yarn run gulp",
|
||||||
"start": "yarn workspace backend run start",
|
"start": "yarn workspace backend run start",
|
||||||
"start:test": "yarn workspace backend run start:test",
|
"start:test": "yarn workspace backend run start:test",
|
||||||
"init": "yarn migrate",
|
"init": "yarn migrate",
|
||||||
|
|
|
@ -6,7 +6,11 @@ module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'../shared/.eslintrc.js',
|
'../shared/.eslintrc.js',
|
||||||
],
|
],
|
||||||
|
plugins: [
|
||||||
|
'foundkey-custom-rules',
|
||||||
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
'foundkey-custom-rules/typeorm-prefer-count': 'error',
|
||||||
'import/order': ['warn', {
|
'import/order': ['warn', {
|
||||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||||
'pathGroups': [
|
'pathGroups': [
|
||||||
|
|
21
packages/backend/migration/1672607891750-remove-reversi.js
Normal file
21
packages/backend/migration/1672607891750-remove-reversi.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export class removeReversi1672607891750 {
|
||||||
|
name = 'removeReversi1672607891750';
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP TABLE "reversi_matching"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "reversi_game"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt")`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
export class removeUserGroupInvite1672991292018 {
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_group_invite"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||||
"watch": "node watch.mjs",
|
"watch": "node watch.mjs",
|
||||||
"lint": "tsc --noEmit && eslint src --ext .ts",
|
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
||||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||||
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||||
"start": "node --experimental-json-modules ./built/index.js",
|
"start": "node --experimental-json-modules ./built/index.js",
|
||||||
|
@ -113,7 +113,6 @@
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.11",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"websocket": "1.0.34",
|
|
||||||
"ws": "8.8.0",
|
"ws": "8.8.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
|
@ -164,12 +163,12 @@
|
||||||
"@types/tmp": "0.2.3",
|
"@types/tmp": "0.2.3",
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||||
"@typescript-eslint/parser": "^5.46.1",
|
"@typescript-eslint/parser": "^5.46.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.29.0",
|
||||||
|
"eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
|
|
@ -140,7 +140,7 @@ async function connectDb(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function spawnWorkers(clusterLimits: Required<Config['clusterLimits']>): Promise<void> {
|
async function spawnWorkers(clusterLimits: Required<Config['clusterLimits']>): Promise<void> {
|
||||||
const modes = ['web', 'queue'];
|
const modes = ['web' as const, 'queue' as const];
|
||||||
const cpus = os.cpus().length;
|
const cpus = os.cpus().length;
|
||||||
for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) {
|
for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) {
|
||||||
bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`);
|
bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`);
|
||||||
|
|
|
@ -8,10 +8,7 @@ import { SECOND } from '@/const.js';
|
||||||
*/
|
*/
|
||||||
const retryDelay = 100;
|
const retryDelay = 100;
|
||||||
|
|
||||||
const lock: (key: string, timeout?: number) => Promise<() => void>
|
const lock: (key: string, timeout?: number) => Promise<() => void> = promisify(redisLock(redisClient, retryDelay));
|
||||||
= redisClient
|
|
||||||
? promisify(redisLock(redisClient, retryDelay))
|
|
||||||
: async () => () => { };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get AP Object lock
|
* Get AP Object lock
|
||||||
|
|
|
@ -22,7 +22,7 @@ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'No
|
||||||
if (note.visibility === 'specified') return false;
|
if (note.visibility === 'specified') return false;
|
||||||
|
|
||||||
// skip if the antenna creator is blocked by the note author
|
// 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)) ?? [];
|
||||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||||
|
|
||||||
if (note.visibility === 'followers') {
|
if (note.visibility === 'followers') {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { db } from '@/db/postgre.js';
|
||||||
import { Meta } from '@/models/entities/meta.js';
|
import { Meta } from '@/models/entities/meta.js';
|
||||||
import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js';
|
import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js';
|
||||||
|
|
||||||
let cache: Meta;
|
let cache: Meta | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the primitive database operation to set the server configuration
|
* Performs the primitive database operation to set the server configuration
|
||||||
|
@ -57,5 +57,5 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||||
|
|
||||||
await getMeta();
|
await getMeta();
|
||||||
|
|
||||||
return cache;
|
return cache!;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,11 @@ export async function getResponse(args: { url: string, method: string, body?: st
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (
|
||||||
|
!res.ok
|
||||||
|
&&
|
||||||
|
// intended redirect is not an error
|
||||||
|
!(args.redirect != 'follow' && res.status >= 300 && res.status < 400)) {
|
||||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ export async function toDbReaction(reaction?: string | null, idnReacterHost?: st
|
||||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const name = custom[1];
|
const name = custom[1];
|
||||||
const emoji = await Emojis.findOneBy({
|
const emoji = await Emojis.countBy({
|
||||||
host: reacterHost ?? IsNull(),
|
host: reacterHost ?? IsNull(),
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
|
|
||||||
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
|
export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } {
|
||||||
return note.renoteId != null && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !note.hasPoll;
|
return note.renoteId != null
|
||||||
|
&& note.text == null
|
||||||
|
&& (
|
||||||
|
note.fileIds == null
|
||||||
|
|| note.fileIds.length === 0
|
||||||
|
)
|
||||||
|
&& !note.hasPoll;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,12 +32,4 @@ export class Announcement {
|
||||||
length: 1024, nullable: true,
|
length: 1024, nullable: true,
|
||||||
})
|
})
|
||||||
public imageUrl: string | null;
|
public imageUrl: string | null;
|
||||||
|
|
||||||
constructor(data: Partial<Announcement>) {
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(data)) {
|
|
||||||
(this as any)[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,4 @@ export class AttestationChallenge {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
public registrationChallenge: boolean;
|
public registrationChallenge: boolean;
|
||||||
|
|
||||||
constructor(data: Partial<AttestationChallenge>) {
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(data)) {
|
|
||||||
(this as any)[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
||||||
import { noteNotificationTypes } from 'foundkey-js';
|
import { noteNotificationTypes } from 'foundkey-js';
|
||||||
import { id } from '../id.js';
|
import { id } from '../id.js';
|
||||||
import { User } from './user.js';
|
import { User } from './user.js';
|
||||||
import { Note } from './note.js';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['userId', 'threadId'], { unique: true })
|
@Index(['userId', 'threadId'], { unique: true })
|
||||||
|
|
|
@ -155,7 +155,14 @@ export class User {
|
||||||
})
|
})
|
||||||
public isExplorable: boolean;
|
public isExplorable: boolean;
|
||||||
|
|
||||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
// for local users:
|
||||||
|
// Indicates a deletion in progress.
|
||||||
|
// A hard delete of the record will follow after the deletion finishes.
|
||||||
|
//
|
||||||
|
// for remote users:
|
||||||
|
// Indicates the user was deleted by an admin.
|
||||||
|
// The users' data is not deleted from the database to keep them from reappearing.
|
||||||
|
// A hard delete of the record may follow if we receive a matching Delete activity.
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether the User is deleted.',
|
comment: 'Whether the User is deleted.',
|
||||||
|
|
|
@ -126,7 +126,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||||
const file = typeof src === 'object' ? src : await this.findOneBy({ id: src });
|
const file = typeof src === 'object' ? src : await this.findOneBy({ id: src });
|
||||||
if (file == null) return null;
|
if (file == null) return null;
|
||||||
|
|
||||||
return await this.pack(file);
|
return await this.pack(file, opts);
|
||||||
},
|
},
|
||||||
|
|
||||||
async packMany(
|
async packMany(
|
||||||
|
|
|
@ -5,7 +5,6 @@ import config from '@/config/index.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
||||||
import { populateEmojis } from '@/misc/populate-emojis.js';
|
import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { Cache } from '@/misc/cache.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
|
@ -126,92 +125,73 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
||||||
const mute = await Mutings.findBy({
|
return await db.query(
|
||||||
muterId: userId,
|
`SELECT EXISTS (
|
||||||
});
|
SELECT 1
|
||||||
|
FROM "messaging_message"
|
||||||
|
WHERE
|
||||||
|
"recipientId" = $1
|
||||||
|
AND
|
||||||
|
NOT "isRead"
|
||||||
|
AND
|
||||||
|
"userId" NOT IN (
|
||||||
|
SELECT "muteeId"
|
||||||
|
FROM "muting"
|
||||||
|
WHERE "muterId" = $1
|
||||||
|
)
|
||||||
|
|
||||||
const joinings = await UserGroupJoinings.findBy({ userId });
|
UNION
|
||||||
|
|
||||||
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
|
SELECT 1
|
||||||
.where('message.groupId = :groupId', { groupId: j.userGroupId })
|
FROM "messaging_message"
|
||||||
.andWhere('message.userId != :userId', { userId })
|
JOIN "user_group_joining"
|
||||||
.andWhere('NOT (:userId = ANY(message.reads))', { userId })
|
ON "messaging_message"."groupId" = "user_group_joining"."userGroupId"
|
||||||
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
WHERE
|
||||||
.getOne().then(x => x != null)));
|
"user_group_joining"."userId" = $1
|
||||||
|
AND
|
||||||
const [withUser, withGroups] = await Promise.all([
|
"messaging_message"."userId" != $1
|
||||||
MessagingMessages.count({
|
AND
|
||||||
where: {
|
NOT $1 = ANY("messaging_message"."reads")
|
||||||
recipientId: userId,
|
AND
|
||||||
isRead: false,
|
"messaging_message"."createdAt" > "user_group_joining"."createdAt"
|
||||||
...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
|
) AS exists`,
|
||||||
},
|
[userId]
|
||||||
take: 1,
|
).then(res => res[0].exists);
|
||||||
}).then(count => count > 0),
|
|
||||||
groupQs,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return withUser || withGroups.some(x => x);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
|
async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
|
||||||
const reads = await AnnouncementReads.findBy({
|
return await db.query(
|
||||||
userId,
|
`SELECT EXISTS (SELECT 1 FROM "announcement" WHERE "id" NOT IN (SELECT "announcementId" FROM "announcement_read" WHERE "userId" = $1)) AS exists`,
|
||||||
});
|
[userId]
|
||||||
|
).then(res => res[0].exists);
|
||||||
const count = await Announcements.countBy(reads.length > 0 ? {
|
|
||||||
id: Not(In(reads.map(read => read.announcementId))),
|
|
||||||
} : {});
|
|
||||||
|
|
||||||
return count > 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||||
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
|
return await db.query(
|
||||||
|
`SELECT EXISTS (SELECT 1 FROM "antenna_note" WHERE NOT "read" AND "antennaId" IN (SELECT "id" FROM "antenna" WHERE "userId" = $1)) AS exists`,
|
||||||
const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({
|
[userId]
|
||||||
antennaId: In(myAntennas.map(x => x.id)),
|
).then(res => res[0].exists);
|
||||||
read: false,
|
|
||||||
}) : null;
|
|
||||||
|
|
||||||
return unread != null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||||
const channels = await ChannelFollowings.findBy({ followerId: userId });
|
return await db.query(
|
||||||
|
`SELECT EXISTS (SELECT 1 FROM "note_unread" WHERE "noteChannelId" IN (SELECT "followeeId" FROM "channel_following" WHERE "followerId" = $1)) AS exists`,
|
||||||
const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
|
[userId]
|
||||||
userId,
|
).then(res => res[0].exists);
|
||||||
noteChannelId: In(channels.map(x => x.followeeId)),
|
|
||||||
}) : null;
|
|
||||||
|
|
||||||
return unread != null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||||
const mute = await Mutings.findBy({
|
return await db.query(
|
||||||
muterId: userId,
|
`SELECT EXISTS (SELECT 1 FROM "notification" WHERE NOT "isRead" AND "notifieeId" = $1 AND "notifierId" NOT IN (SELECT "muteeId" FROM "muting" WHERE "muterId" = $1)) AS exists`,
|
||||||
});
|
[userId]
|
||||||
const mutedUserIds = mute.map(m => m.muteeId);
|
).then(res => res[0].exists);
|
||||||
|
|
||||||
const count = await Notifications.count({
|
|
||||||
where: {
|
|
||||||
notifieeId: userId,
|
|
||||||
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
|
|
||||||
isRead: false,
|
|
||||||
},
|
|
||||||
take: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return count > 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
|
async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
|
||||||
const count = await FollowRequests.countBy({
|
return await db.query(
|
||||||
followeeId: userId,
|
`SELECT EXISTS (SELECT 1 FROM "follow_request" WHERE "followeeId" = $1) AS exists`,
|
||||||
});
|
[userId]
|
||||||
|
).then(res => res[0].exists);
|
||||||
return count > 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
|
getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
|
||||||
|
|
|
@ -4,20 +4,6 @@ const dateTimeIntervals = {
|
||||||
'ms': 1,
|
'ms': 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function dateUTC(time: number[]): Date {
|
|
||||||
const d = time.length === 2 ? Date.UTC(time[0], time[1])
|
|
||||||
: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
|
|
||||||
: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
|
|
||||||
: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
|
|
||||||
: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
|
|
||||||
: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!d) throw new Error('wrong number of arguments');
|
|
||||||
|
|
||||||
return new Date(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTimeSame(a: Date, b: Date): boolean {
|
export function isTimeSame(a: Date, b: Date): boolean {
|
||||||
return a.getTime() === b.getTime();
|
return a.getTime() === b.getTime();
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,10 +83,8 @@ export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// soft指定されている場合は物理削除しない
|
// No physical deletion if soft is specified.
|
||||||
if (job.data.soft) {
|
if (!job.data.soft) {
|
||||||
// nop
|
|
||||||
} else {
|
|
||||||
await Users.delete(job.data.user.id);
|
await Users.delete(job.data.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
||||||
name: emojiInfo.name,
|
name: emojiInfo.name,
|
||||||
});
|
});
|
||||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||||
const emoji = await Emojis.insert({
|
await Emojis.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: emojiInfo.name,
|
name: emojiInfo.name,
|
||||||
|
@ -66,13 +66,13 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
||||||
originalUrl: driveFile.url,
|
originalUrl: driveFile.url,
|
||||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
type: driveFile.webpublicType ?? driveFile.type,
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
}).then(x => Emojis.findOneByOrFail(x.identifiers[0]));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.queryResultCache!.remove(['meta_emojis']);
|
await db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
logger.succ('Imported');
|
logger.succ('Imported');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Bull from 'bull';
|
||||||
import { In, LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js';
|
import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
import { MINUTE, DAY, MONTH } from '@/const.js';
|
import { MINUTE, MONTH } from '@/const.js';
|
||||||
import { queueLogger } from '@/queue/logger.js';
|
import { queueLogger } from '@/queue/logger.js';
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger('check-expired');
|
const logger = queueLogger.createSubLogger('check-expired');
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { extractDbHost } from '@/misc/convert-host.js';
|
||||||
import { StatusError } from '@/misc/fetch.js';
|
import { StatusError } from '@/misc/fetch.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||||
import { createNote, fetchNote } from '@/remote/activitypub/models/note.js';
|
import { createNote, fetchNote } from '@/remote/activitypub/models/note.js';
|
||||||
import { getApId, IObject, ICreate } from '@/remote/activitypub/type.js';
|
import { getApId, IObject } from '@/remote/activitypub/type.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 投稿作成アクティビティを捌きます
|
* 投稿作成アクティビティを捌きます
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
import { apLogger } from '@/remote/activitypub/logger.js';
|
import { apLogger } from '@/remote/activitypub/logger.js';
|
||||||
|
import { deleteAccount } from '@/services/delete-account.js';
|
||||||
|
|
||||||
export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
||||||
apLogger.info(`Deleting the Actor: ${uri}`);
|
apLogger.info(`Deleting the Actor: ${uri}`);
|
||||||
|
@ -17,14 +17,9 @@ export async function deleteActor(actor: CacheableRemoteUser, uri: string): Prom
|
||||||
return 'ok: gone';
|
return 'ok: gone';
|
||||||
}
|
}
|
||||||
if (user.isDeleted) {
|
if (user.isDeleted) {
|
||||||
apLogger.info('skip: already deleted');
|
// the actual deletion already happened by an admin, just delete the record
|
||||||
|
await Users.delete(actor.id);
|
||||||
|
} else {
|
||||||
|
await deleteAccount(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = await createDeleteAccountJob(actor);
|
|
||||||
|
|
||||||
await Users.update(actor.id, {
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return `ok: queued ${job.name} ${job.id}`;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<st
|
||||||
return 'skip: follower not found';
|
return 'skip: follower not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
const following = await Followings.findOneBy({
|
const following = await Followings.countBy({
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: actor.id,
|
followeeId: actor.id,
|
||||||
});
|
});
|
||||||
|
@ -22,5 +22,5 @@ export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<st
|
||||||
return 'ok: unfollowed';
|
return 'ok: unfollowed';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'skip: フォローされていない';
|
return 'skip: not followed';
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,18 +17,18 @@ export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<st
|
||||||
return 'skip: the unfollowed user is not local';
|
return 'skip: the unfollowed user is not local';
|
||||||
}
|
}
|
||||||
|
|
||||||
const [req, following] = await Promise.all([
|
const [requested, following] = await Promise.all([
|
||||||
FollowRequests.findOneBy({
|
FollowRequests.countBy({
|
||||||
followerId: actor.id,
|
followerId: actor.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
}),
|
}),
|
||||||
Followings.findOneBy({
|
Followings.countBy({
|
||||||
followerId: actor.id,
|
followerId: actor.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (req) {
|
if (requested) {
|
||||||
await cancelFollowRequest(followee, actor);
|
await cancelFollowRequest(followee, actor);
|
||||||
return 'ok: follow request canceled';
|
return 'ok: follow request canceled';
|
||||||
} else if (following) {
|
} else if (following) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { DbResolver } from '../db-resolver.js';
|
||||||
import { apLogger } from '../logger.js';
|
import { apLogger } from '../logger.js';
|
||||||
import { resolvePerson } from './person.js';
|
import { resolvePerson } from './person.js';
|
||||||
import { resolveImage } from './image.js';
|
import { resolveImage } from './image.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags, extractQuoteUrl } from './tag.js';
|
||||||
import { extractPollFromQuestion } from './question.js';
|
import { extractPollFromQuestion } from './question.js';
|
||||||
import { extractApMentions } from './mention.js';
|
import { extractApMentions } from './mention.js';
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
||||||
const uri = getApId(note.inReplyTo);
|
const uri = getApId(note.inReplyTo);
|
||||||
if (uri.startsWith(config.url + '/')) {
|
if (uri.startsWith(config.url + '/')) {
|
||||||
const id = uri.split('/').pop();
|
const id = uri.split('/').pop();
|
||||||
const talk = await MessagingMessages.findOneBy({ id });
|
const talk = await MessagingMessages.countBy({ id });
|
||||||
if (talk) {
|
if (talk) {
|
||||||
isTalk = true;
|
isTalk = true;
|
||||||
return null;
|
return null;
|
||||||
|
@ -154,10 +154,10 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 引用
|
|
||||||
let quote: Note | undefined | null;
|
let quote: Note | undefined | null;
|
||||||
|
const quoteUrl = extractQuoteUrl(note.tag);
|
||||||
|
|
||||||
if (note._misskey_quote || note.quoteUrl) {
|
if (quoteUrl || note._misskey_quote || note.quoteUri) {
|
||||||
const tryResolveNote = async (uri: string): Promise<{
|
const tryResolveNote = async (uri: string): Promise<{
|
||||||
status: 'ok';
|
status: 'ok';
|
||||||
res: Note | null;
|
res: Note | null;
|
||||||
|
@ -184,10 +184,16 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
const uris = unique([quoteUrl, note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||||
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
|
// check the urls sequentially and abort early to not do unnecessary HTTP requests
|
||||||
|
// picks the first one that works
|
||||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
for (const uri in uris) {
|
||||||
|
const res = await tryResolveNote(uri);
|
||||||
|
if (res.status === 'ok') {
|
||||||
|
quote = res.res;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
if (results.some(x => x.status === 'temperror')) {
|
if (results.some(x => x.status === 'temperror')) {
|
||||||
throw new Error('quote resolve failed');
|
throw new Error('quote resolve failed');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { URL } from 'node:url';
|
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
|
import { Not, IsNull } from 'typeorm';
|
||||||
|
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||||
|
@ -40,7 +40,6 @@ const summaryLength = 2048;
|
||||||
* @param uri Fetch target URI
|
* @param uri Fetch target URI
|
||||||
*/
|
*/
|
||||||
function validateActor(x: IObject): IActor {
|
function validateActor(x: IObject): IActor {
|
||||||
|
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
throw new Error('invalid Actor: object is null');
|
throw new Error('invalid Actor: object is null');
|
||||||
}
|
}
|
||||||
|
@ -56,6 +55,12 @@ function validateActor(x: IObject): IActor {
|
||||||
throw new Error('invalid Actor: wrong id');
|
throw new Error('invalid Actor: wrong id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This check is security critical.
|
||||||
|
// Without this check, an entry could be inserted into UserPublickey for a local user.
|
||||||
|
if (extractDbHost(uri) === extractDbHost(config.url)) {
|
||||||
|
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||||
|
}
|
||||||
|
|
||||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||||
throw new Error('invalid Actor: wrong inbox');
|
throw new Error('invalid Actor: wrong inbox');
|
||||||
}
|
}
|
||||||
|
@ -85,9 +90,9 @@ function validateActor(x: IObject): IActor {
|
||||||
throw new Error('invalid Actor: publicKey.id is not a string');
|
throw new Error('invalid Actor: publicKey.id is not a string');
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectHost = extractDbHost(uri);
|
// This is a security critical check to not insert or change an entry of
|
||||||
const publicKeyIdHost = extractDbHost(x.publicKey.id);
|
// UserPublickey to point to a local key id.
|
||||||
if (publicKeyIdHost !== expectHost) {
|
if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) {
|
||||||
throw new Error('invalid Actor: publicKey.id has different host');
|
throw new Error('invalid Actor: publicKey.id has different host');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,13 +105,13 @@ function validateActor(x: IObject): IActor {
|
||||||
*
|
*
|
||||||
* If the target Person is registered in FoundKey, it is returned.
|
* If the target Person is registered in FoundKey, it is returned.
|
||||||
*/
|
*/
|
||||||
export async function fetchPerson(uri: string, resolver: Resolver): Promise<CacheableUser | null> {
|
export async function fetchPerson(uri: string): Promise<CacheableUser | null> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
const cached = uriPersonCache.get(uri);
|
const cached = uriPersonCache.get(uri);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// If the URI points to this server, fetch from database.
|
||||||
if (uri.startsWith(config.url + '/')) {
|
if (uri.startsWith(config.url + '/')) {
|
||||||
const id = uri.split('/').pop();
|
const id = uri.split('/').pop();
|
||||||
const u = await Users.findOneBy({ id });
|
const u = await Users.findOneBy({ id });
|
||||||
|
@ -130,10 +135,6 @@ export async function fetchPerson(uri: string, resolver: Resolver): Promise<Cach
|
||||||
* Personを作成します。
|
* Personを作成します。
|
||||||
*/
|
*/
|
||||||
export async function createPerson(value: string | IObject, resolver: Resolver): Promise<User> {
|
export async function createPerson(value: string | IObject, resolver: Resolver): Promise<User> {
|
||||||
if (getApId(value).startsWith(config.url)) {
|
|
||||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
|
||||||
}
|
|
||||||
|
|
||||||
const object = await resolver.resolve(value) as any;
|
const object = await resolver.resolve(value) as any;
|
||||||
|
|
||||||
const person = validateActor(object);
|
const person = validateActor(object);
|
||||||
|
@ -277,13 +278,8 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
||||||
export async function updatePerson(value: IObject | string, resolver: Resolver): Promise<void> {
|
export async function updatePerson(value: IObject | string, resolver: Resolver): Promise<void> {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
|
|
||||||
// skip local URIs
|
|
||||||
if (uri.startsWith(config.url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do we already know this user?
|
// do we already know this user?
|
||||||
const exist = await Users.findOneBy({ uri }) as IRemoteUser;
|
const exist = await Users.findOneBy({ uri, host: Not(IsNull()) }) as IRemoteUser;
|
||||||
|
|
||||||
if (exist == null) {
|
if (exist == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -384,7 +380,7 @@ export async function resolvePerson(uri: string, resolver: Resolver): Promise<Ca
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await fetchPerson(uri, resolver);
|
const exist = await fetchPerson(uri);
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
return exist;
|
return exist;
|
||||||
|
|
|
@ -20,10 +20,10 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
const choices = question[multiple ? 'anyOf' : 'oneOf']!
|
const choices = question[multiple ? 'anyOf' : 'oneOf']!
|
||||||
.map((x, i) => x.name!);
|
.map(x => x.name!);
|
||||||
|
|
||||||
const votes = question[multiple ? 'anyOf' : 'oneOf']!
|
const votes = question[multiple ? 'anyOf' : 'oneOf']!
|
||||||
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
|
.map(x => x.replies && x.replies.totalItems || x._misskey_votes || 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
choices,
|
choices,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { toArray } from '@/prelude/array.js';
|
import { toArray } from '@/prelude/array.js';
|
||||||
import { IObject, isHashtag, IApHashtag } from '../type.js';
|
import { IObject, isHashtag, IApHashtag, isLink, ILink } from '../type.js';
|
||||||
|
|
||||||
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
|
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
|
||||||
if (tags == null) return [];
|
if (tags == null) return [];
|
||||||
|
@ -16,3 +16,34 @@ export function extractApHashtagObjects(tags: IObject | IObject[] | null | undef
|
||||||
if (tags == null) return [];
|
if (tags == null) return [];
|
||||||
return toArray(tags).filter(isHashtag);
|
return toArray(tags).filter(isHashtag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// implements FEP-e232: Object Links (2022-12-23 version)
|
||||||
|
export function extractQuoteUrl(tags: IObject | IObject[] | null | undefined): string | null {
|
||||||
|
if (tags == null) return null;
|
||||||
|
|
||||||
|
// filter out correct links
|
||||||
|
let quotes: ILink[] = toArray(tags)
|
||||||
|
.filter(isLink)
|
||||||
|
.filter(link =>
|
||||||
|
[
|
||||||
|
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
'application/activity+json'
|
||||||
|
].includes(link.mediaType?.toLowerCase())
|
||||||
|
)
|
||||||
|
.filter(link =>
|
||||||
|
toArray(link.rel)
|
||||||
|
.some(rel =>
|
||||||
|
[
|
||||||
|
'https://misskey-hub.net/ns#_misskey_quote',
|
||||||
|
'http://fedibird.com/ns#quoteUri',
|
||||||
|
'https://www.w3.org/ns/activitystreams#quoteUrl',
|
||||||
|
].includes(rel)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quotes.length === 0) return null;
|
||||||
|
|
||||||
|
// Deduplicate by href.
|
||||||
|
// If there is more than one quote, we just pick the first/a random one.
|
||||||
|
quotes.filter((x, i, arr) => arr.findIndex(y => x.href === y.href) === i)[0].href;
|
||||||
|
}
|
||||||
|
|
|
@ -16,4 +16,4 @@ export async function perform(actor: CacheableRemoteUser, activity: IObject, res
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
|
@ -21,12 +21,14 @@ export const renderActivity = (x: any): IActivity | null => {
|
||||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||||
sensitive: 'as:sensitive',
|
sensitive: 'as:sensitive',
|
||||||
Hashtag: 'as:Hashtag',
|
Hashtag: 'as:Hashtag',
|
||||||
quoteUrl: 'as:quoteUrl',
|
|
||||||
// Mastodon
|
// Mastodon
|
||||||
toot: 'http://joinmastodon.org/ns#',
|
toot: 'http://joinmastodon.org/ns#',
|
||||||
Emoji: 'toot:Emoji',
|
Emoji: 'toot:Emoji',
|
||||||
featured: 'toot:featured',
|
featured: 'toot:featured',
|
||||||
discoverable: 'toot:discoverable',
|
discoverable: 'toot:discoverable',
|
||||||
|
// Fedibird
|
||||||
|
fedibird: 'http://fedibird.com/ns#',
|
||||||
|
quoteUri: 'fedibird:quoteUri',
|
||||||
// schema
|
// schema
|
||||||
schema: 'http://schema.org#',
|
schema: 'http://schema.org#',
|
||||||
PropertyValue: 'schema:PropertyValue',
|
PropertyValue: 'schema:PropertyValue',
|
||||||
|
|
|
@ -25,9 +25,9 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||||
inReplyToNote = await Notes.findOneBy({ id: note.replyId });
|
inReplyToNote = await Notes.findOneBy({ id: note.replyId });
|
||||||
|
|
||||||
if (inReplyToNote != null) {
|
if (inReplyToNote != null) {
|
||||||
const inReplyToUser = await Users.findOneBy({ id: inReplyToNote.userId });
|
const inReplyToUserExists = await Users.countBy({ id: inReplyToNote.userId });
|
||||||
|
|
||||||
if (inReplyToUser != null) {
|
if (inReplyToUserExists) {
|
||||||
if (inReplyToNote.uri) {
|
if (inReplyToNote.uri) {
|
||||||
inReplyTo = inReplyToNote.uri;
|
inReplyTo = inReplyToNote.uri;
|
||||||
} else {
|
} else {
|
||||||
|
@ -111,6 +111,16 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||||
...apemojis,
|
...apemojis,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
tag.push({
|
||||||
|
type: 'Link',
|
||||||
|
mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
href: quote,
|
||||||
|
name: `RE: ${quote}`,
|
||||||
|
rel: 'https://misskey-hub.net/ns#_misskey_quote',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const asPoll = poll ? {
|
const asPoll = poll ? {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
content: await toHtml(text, note.mentions),
|
content: await toHtml(text, note.mentions),
|
||||||
|
@ -141,7 +151,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||||
mediaType: 'text/x.misskeymarkdown',
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
},
|
},
|
||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUri: quote,
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
|
|
|
@ -30,14 +30,15 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
||||||
// don't allow redirects on the inbox
|
// don't allow redirects on the inbox
|
||||||
redirect: 'error',
|
redirect: 'error',
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get AP object with http-signature
|
* Get AP object with http-signature
|
||||||
* @param user http-signature user
|
* @param user http-signature user
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
export async function signedGet(_url: string, user: { id: User['id'] }): Promise<any> {
|
||||||
|
let url = _url;
|
||||||
const keypair = await getUserKeypair(user.id);
|
const keypair = await getUserKeypair(user.id);
|
||||||
|
|
||||||
for (let redirects = 0; redirects < 3; redirects++) {
|
for (let redirects = 0; redirects < 3; redirects++) {
|
||||||
|
|
|
@ -45,7 +45,7 @@ export function getOneApId(value: ApObject): string {
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object id
|
* Get ActivityStreams Object id
|
||||||
*/
|
*/
|
||||||
export function getApId(value: string | IObject): string {
|
export function getApId(value: string | Object): string {
|
||||||
if (typeof value === 'string') return value;
|
if (typeof value === 'string') return value;
|
||||||
if (typeof value.id === 'string') return value.id;
|
if (typeof value.id === 'string') return value.id;
|
||||||
throw new Error('cannot detemine id');
|
throw new Error('cannot detemine id');
|
||||||
|
@ -54,7 +54,7 @@ export function getApId(value: string | IObject): string {
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object type
|
* Get ActivityStreams Object type
|
||||||
*/
|
*/
|
||||||
export function getApType(value: IObject): string {
|
export function getApType(value: Object): string {
|
||||||
if (typeof value.type === 'string') return value.type;
|
if (typeof value.type === 'string') return value.type;
|
||||||
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||||
throw new Error('cannot detect type');
|
throw new Error('cannot detect type');
|
||||||
|
@ -111,7 +111,7 @@ export interface IPost extends IObject {
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
};
|
};
|
||||||
_misskey_quote?: string;
|
_misskey_quote?: string;
|
||||||
quoteUrl?: string;
|
quoteUri?: string;
|
||||||
_misskey_talk: boolean;
|
_misskey_talk: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ export interface IQuestion extends IObject {
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
};
|
};
|
||||||
_misskey_quote?: string;
|
_misskey_quote?: string;
|
||||||
quoteUrl?: string;
|
quoteUri?: string;
|
||||||
oneOf?: IQuestionChoice[];
|
oneOf?: IQuestionChoice[];
|
||||||
anyOf?: IQuestionChoice[];
|
anyOf?: IQuestionChoice[];
|
||||||
endTime?: Date;
|
endTime?: Date;
|
||||||
|
@ -196,24 +196,6 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||||
typeof object.name === 'string' &&
|
typeof object.name === 'string' &&
|
||||||
typeof (object as any).value === 'string';
|
typeof (object as any).value === 'string';
|
||||||
|
|
||||||
export interface IApMention extends IObject {
|
|
||||||
type: 'Mention';
|
|
||||||
href: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isMention = (object: IObject): object is IApMention =>
|
|
||||||
getApType(object) === 'Mention' &&
|
|
||||||
typeof object.href === 'string';
|
|
||||||
|
|
||||||
export interface IApHashtag extends IObject {
|
|
||||||
type: 'Hashtag';
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isHashtag = (object: IObject): object is IApHashtag =>
|
|
||||||
getApType(object) === 'Hashtag' &&
|
|
||||||
typeof object.name === 'string';
|
|
||||||
|
|
||||||
export interface IApEmoji extends IObject {
|
export interface IApEmoji extends IObject {
|
||||||
type: 'Emoji';
|
type: 'Emoji';
|
||||||
updated: Date;
|
updated: Date;
|
||||||
|
@ -293,3 +275,34 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
|
||||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||||
|
|
||||||
|
export interface ILink {
|
||||||
|
href: string;
|
||||||
|
rel?: string | string[];
|
||||||
|
mediaType?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApMention extends ILink {
|
||||||
|
type: 'Mention';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApHashtag extends ILink {
|
||||||
|
type: 'Hashtag';
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLink = (object: Record<string, any>): object is ILink =>
|
||||||
|
typeof object.href === 'string'
|
||||||
|
&& (
|
||||||
|
object.rel == undefined
|
||||||
|
|| typeof object.rel === 'string'
|
||||||
|
|| (Array.isArray(object.rel) && object.rel.every(x => typeof x === 'string'))
|
||||||
|
)
|
||||||
|
&& (object.mediaType == undefined || typeof object.mediaType === 'string');
|
||||||
|
export const isMention = (object: Record<string, any>): object is IApMention =>
|
||||||
|
getApType(object) === 'Mention' && isLink(object);
|
||||||
|
export const isHashtag = (object: Record<string, any>): object is IApHashtag =>
|
||||||
|
getApType(object) === 'Hashtag'
|
||||||
|
&& isLink(object)
|
||||||
|
&& typeof object.name === 'string';
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
.where('note.visibility = \'public\'')
|
.where('note.visibility = \'public\'')
|
||||||
.orWhere('note.visibility = \'home\'');
|
.orWhere('note.visibility = \'home\'');
|
||||||
}))
|
}))
|
||||||
.andWhere('note.localOnly = FALSE');
|
.andWhere('NOT note.localOnly');
|
||||||
|
|
||||||
const notes = await query.take(limit).getMany();
|
const notes = await query.take(limit).getMany();
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ export async function signup(opts: {
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await db.transaction(async transactionalEntityManager => {
|
await db.transaction(async transactionalEntityManager => {
|
||||||
const exist = await transactionalEntityManager.findOneBy(User, {
|
const exist = await transactionalEntityManager.countBy(User, {
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,7 +54,6 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
||||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
||||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||||
|
@ -363,7 +362,6 @@ const eps = [
|
||||||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||||
['admin/update-meta', ep___admin_updateMeta],
|
['admin/update-meta', ep___admin_updateMeta],
|
||||||
['admin/vacuum', ep___admin_vacuum],
|
['admin/vacuum', ep___admin_vacuum],
|
||||||
['admin/delete-account', ep___admin_deleteAccount],
|
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
['antennas/delete', ep___antennas_delete],
|
['antennas/delete', ep___antennas_delete],
|
||||||
|
|
|
@ -93,8 +93,8 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
|
const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
|
||||||
|
|
||||||
switch (ps.state) {
|
switch (ps.state) {
|
||||||
case 'resolved': query.andWhere('report.resolved = TRUE'); break;
|
case 'resolved': query.andWhere('report.resolved'); break;
|
||||||
case 'unresolved': query.andWhere('report.resolved = FALSE'); break;
|
case 'unresolved': query.andWhere('NOT report.resolved'); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (ps.reporterOrigin) {
|
switch (ps.reporterOrigin) {
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { doPostSuspend } from '@/services/suspend-user.js';
|
import { deleteAccount } from '@/services/delete-account.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
|
||||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireAdmin: true,
|
||||||
|
|
||||||
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
|
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -23,36 +21,19 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({
|
||||||
|
id: ps.userId,
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new ApiError('NO_SUCH_USER');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
} else if (user.isAdmin) {
|
} else if (user.isAdmin) {
|
||||||
throw new ApiError('IS_ADMIN');
|
throw new ApiError('IS_ADMIN');
|
||||||
} else if(user.isModerator) {
|
} else if (user.isModerator) {
|
||||||
throw new ApiError('IS_MODERATOR');
|
throw new ApiError('IS_MODERATOR');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Users.isLocalUser(user)) {
|
await deleteAccount(user);
|
||||||
// 物理削除する前にDelete activityを送信する
|
|
||||||
await doPostSuspend(user).catch(e => {});
|
|
||||||
|
|
||||||
createDeleteAccountJob(user, {
|
|
||||||
soft: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createDeleteAccountJob(user, {
|
|
||||||
soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await Users.update(user.id, {
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Users.isLocalUser(user)) {
|
|
||||||
// Terminate streaming
|
|
||||||
publishUserEvent(user.id, 'terminate', {});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const announcement = await Announcements.findOneBy({ id: ps.id });
|
const announcement = await Announcements.findOneBy({ id: ps.id });
|
||||||
|
|
||||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
||||||
|
|
|
@ -84,6 +84,6 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
title: announcement.title,
|
title: announcement.title,
|
||||||
text: announcement.text,
|
text: announcement.text,
|
||||||
imageUrl: announcement.imageUrl,
|
imageUrl: announcement.imageUrl,
|
||||||
reads: reads.get(announcement)!,
|
reads: reads.get(announcement),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const announcement = await Announcements.findOneBy({ id: ps.id });
|
const announcement = await Announcements.findOneBy({ id: ps.id });
|
||||||
|
|
||||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { Users } from '@/models/index.js';
|
|
||||||
import { deleteAccount } from '@/services/delete-account.js';
|
|
||||||
import define from '../../define.js';
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
tags: ['admin'],
|
|
||||||
|
|
||||||
requireCredential: true,
|
|
||||||
requireAdmin: true,
|
|
||||||
|
|
||||||
res: {
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: ['userId'],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
|
||||||
export default define(meta, paramDef, async (ps) => {
|
|
||||||
const user = await Users.findOneByOrFail({ id: ps.userId });
|
|
||||||
if (user.isDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteAccount(user);
|
|
||||||
});
|
|
|
@ -18,7 +18,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const files = await DriveFiles.findBy({
|
const files = await DriveFiles.findBy({
|
||||||
userId: ps.userId,
|
userId: ps.userId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,6 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async () => {
|
||||||
createCleanRemoteFilesJob();
|
createCleanRemoteFilesJob();
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
|
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
|
||||||
|
|
||||||
if (ps.userId) {
|
if (ps.userId) {
|
||||||
|
|
|
@ -163,7 +163,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const file = ps.fileId ? await DriveFiles.findOneBy({ id: ps.fileId }) : await DriveFiles.findOne({
|
const file = ps.fileId ? await DriveFiles.findOneBy({ id: ps.fileId }) : await DriveFiles.findOne({
|
||||||
where: [{
|
where: [{
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const emoji = await Emojis.findOneBy({ id: ps.emojiId });
|
const emoji = await Emojis.findOneBy({ id: ps.emojiId });
|
||||||
|
|
||||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const files = await DriveFiles.findBy({
|
const files = await DriveFiles.findBy({
|
||||||
userHost: ps.host,
|
userHost: ps.host,
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
||||||
|
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const followings = await Followings.findBy({
|
const followings = await Followings.findBy({
|
||||||
followerHost: ps.host,
|
followerHost: ps.host,
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,10 +22,10 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
const instanceExists = await Instances.countBy({ host: toPuny(ps.host) });
|
||||||
|
|
||||||
if (instance == null) {
|
if (!instanceExists) {
|
||||||
throw new ApiError('NO_SUCH_OBJECT');
|
throw new ApiError('NO_SUCH_OBJECT');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -256,7 +256,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async () => {
|
||||||
const instance = await fetchMeta(true);
|
const instance = await fetchMeta(true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async () => {
|
||||||
const jobs = await deliverQueue.getJobs(['delayed']);
|
const jobs = await deliverQueue.getJobs(['delayed']);
|
||||||
|
|
||||||
const res = [] as [string, number][];
|
const res = [] as [string, number][];
|
||||||
|
|
|
@ -39,7 +39,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async () => {
|
||||||
const jobs = await inboxQueue.getJobs(['delayed']);
|
const jobs = await inboxQueue.getJobs(['delayed']);
|
||||||
|
|
||||||
const res = [] as [string, number][];
|
const res = [] as [string, number][];
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async () => {
|
||||||
const deliverJobCounts = await deliverQueue.getJobCounts();
|
const deliverJobCounts = await deliverQueue.getJobCounts();
|
||||||
const inboxJobCounts = await inboxQueue.getJobCounts();
|
const inboxJobCounts = await inboxQueue.getJobCounts();
|
||||||
const dbJobCounts = await dbQueue.getJobCounts();
|
const dbJobCounts = await dbQueue.getJobCounts();
|
||||||
|
|
|
@ -49,7 +49,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
try {
|
try {
|
||||||
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only');
|
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -46,6 +46,6 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async () => {
|
||||||
return await listRelay();
|
return await listRelay();
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,6 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
return await removeRelay(ps.inbox);
|
return await removeRelay(ps.inbox);
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,13 +26,11 @@ export const paramDef = {
|
||||||
offset: { type: 'integer', default: 0 },
|
offset: { type: 'integer', default: 0 },
|
||||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
||||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
|
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
|
||||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
|
||||||
username: { type: 'string', nullable: true, default: null },
|
username: { type: 'string', nullable: true, default: null },
|
||||||
hostname: {
|
hostname: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true,
|
description: "To represent the local host, use `origin: 'local'` instead.",
|
||||||
default: null,
|
|
||||||
description: 'The local host is represented with `null`.',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
|
@ -43,13 +41,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const query = Users.createQueryBuilder('user');
|
const query = Users.createQueryBuilder('user');
|
||||||
|
|
||||||
switch (ps.state) {
|
switch (ps.state) {
|
||||||
case 'available': query.where('user.isSuspended = FALSE'); break;
|
case 'available': query.where('NOT user.isSuspended'); break;
|
||||||
case 'admin': query.where('user.isAdmin = TRUE'); break;
|
case 'admin': query.where('user.isAdmin'); break;
|
||||||
case 'moderator': query.where('user.isModerator = TRUE'); break;
|
case 'moderator': query.where('user.isModerator'); break;
|
||||||
case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
|
case 'adminOrModerator': query.where('user.isAdmin OR user.isModerator'); break;
|
||||||
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break;
|
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break;
|
||||||
case 'silenced': query.where('user.isSilenced = TRUE'); break;
|
case 'silenced': query.where('user.isSilenced'); break;
|
||||||
case 'suspended': query.where('user.isSuspended = TRUE'); break;
|
case 'suspended': query.where('user.isSuspended'); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (ps.origin) {
|
switch (ps.origin) {
|
||||||
|
|
|
@ -50,9 +50,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await doPostSuspend(user).catch(e => {});
|
await doPostSuspend(user).catch(() => {});
|
||||||
await unFollowAll(user).catch(e => {});
|
await unFollowAll(user).catch(() => {});
|
||||||
await readAllNotify(user).catch(e => {});
|
await readAllNotify(user).catch(() => {});
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { extractDbHost } from '@/misc/convert-host.js';
|
||||||
import { Users, Notes } from '@/models/index.js';
|
import { Users, Notes } from '@/models/index.js';
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||||
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
|
import { isActor, isPost } from '@/remote/activitypub/type.js';
|
||||||
import { SchemaType } from '@/misc/schema.js';
|
import { SchemaType } from '@/misc/schema.js';
|
||||||
import { HOUR } from '@/const.js';
|
import { HOUR } from '@/const.js';
|
||||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const result = await AuthSessions.delete({
|
const result = await AuthSessions.delete({
|
||||||
token: ps.token,
|
token: ps.token,
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already blocking
|
// Check if already blocking
|
||||||
const exist = await Blockings.findOneBy({
|
const blocked = await Blockings.countBy({
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_BLOCKING');
|
if (blocked) throw new ApiError('ALREADY_BLOCKING');
|
||||||
|
|
||||||
await create(blocker, blockee);
|
await create(blocker, blockee);
|
||||||
|
|
||||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not blocking
|
// Check not blocking
|
||||||
const exist = await Blockings.findOneBy({
|
const exist = await Blockings.countBy({
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) throw new ApiError('NOT_BLOCKING');
|
if (!exist) throw new ApiError('NOT_BLOCKING');
|
||||||
|
|
||||||
// Delete blocking
|
// Delete blocking
|
||||||
await deleteBlocking(blocker, blockee);
|
await deleteBlocking(blocker, blockee);
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { ApiError } from '../../error.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['channels'],
|
tags: ['channels'],
|
||||||
|
|
||||||
|
description: 'Creates a new channel with the current user as its administrator.',
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
@ -32,14 +34,13 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
let banner = null;
|
|
||||||
if (ps.bannerId != null) {
|
if (ps.bannerId != null) {
|
||||||
banner = await DriveFiles.findOneBy({
|
const bannerExists = await DriveFiles.countBy({
|
||||||
id: ps.bannerId,
|
id: ps.bannerId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (banner == null) throw new ApiError('NO_SUCH_FILE');
|
if (!bannerExists) throw new ApiError('NO_SUCH_FILE');
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = await Channels.insert({
|
const channel = await Channels.insert({
|
||||||
|
@ -47,8 +48,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
description: ps.description || null,
|
description: ps.description,
|
||||||
bannerId: banner ? banner.id : null,
|
bannerId: ps.bannerId,
|
||||||
} as Channel).then(x => Channels.findOneByOrFail(x.identifiers[0]));
|
} as Channel).then(x => Channels.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
return await Channels.pack(channel, user);
|
return await Channels.pack(channel, user);
|
||||||
|
|
|
@ -37,12 +37,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await ClipNotes.findOneBy({
|
const exist = await ClipNotes.countBy({
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_CLIPPED');
|
if (exist) throw new ApiError('ALREADY_CLIPPED');
|
||||||
|
|
||||||
await ClipNotes.insert({
|
await ClipNotes.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
|
|
@ -26,10 +26,8 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const file = await DriveFiles.findOneBy({
|
return 0 < await DriveFiles.countBy({
|
||||||
md5: ps.md5,
|
md5: ps.md5,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return file != null;
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const query = Instances.createQueryBuilder('instance');
|
const query = Instances.createQueryBuilder('instance');
|
||||||
|
|
||||||
switch (ps.sort) {
|
switch (ps.sort) {
|
||||||
|
@ -69,17 +69,17 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
|
||||||
if (typeof ps.notResponding === 'boolean') {
|
if (typeof ps.notResponding === 'boolean') {
|
||||||
if (ps.notResponding) {
|
if (ps.notResponding) {
|
||||||
query.andWhere('instance.isNotResponding = TRUE');
|
query.andWhere('instance.isNotResponding');
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('instance.isNotResponding = FALSE');
|
query.andWhere('NOT instance.isNotResponding');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ps.suspended === 'boolean') {
|
if (typeof ps.suspended === 'boolean') {
|
||||||
if (ps.suspended) {
|
if (ps.suspended) {
|
||||||
query.andWhere('instance.isSuspended = TRUE');
|
query.andWhere('instance.isSuspended');
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('instance.isSuspended = FALSE');
|
query.andWhere('NOT instance.isSuspended');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const instance = await Instances
|
const instance = await Instances
|
||||||
.findOneBy({ host: toPuny(ps.host) });
|
.findOneBy({ host: toPuny(ps.host) });
|
||||||
|
|
||||||
|
|
|
@ -49,12 +49,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
const exist = await Followings.findOneBy({
|
const exist = await Followings.countBy({
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_FOLLOWING');
|
if (exist) throw new ApiError('ALREADY_FOLLOWING');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await create(follower, followee);
|
await create(follower, followee);
|
||||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not following
|
// Check not following
|
||||||
const exist = await Followings.findOneBy({
|
const exist = await Followings.countBy({
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) throw new ApiError('NOT_FOLLOWING');
|
if (!exist) throw new ApiError('NOT_FOLLOWING');
|
||||||
|
|
||||||
await deleteFollowing(follower, followee);
|
await deleteFollowing(follower, followee);
|
||||||
|
|
||||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not following
|
// Check not following
|
||||||
const exist = await Followings.findOneBy({
|
const exist = await Followings.countBy({
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) throw new ApiError('NOT_FOLLOWING');
|
if (!exist) throw new ApiError('NOT_FOLLOWING');
|
||||||
|
|
||||||
await deleteFollowing(follower, followee);
|
await deleteFollowing(follower, followee);
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
{
|
{
|
||||||
param: '#/properties/fileIds/items',
|
param: '#/properties/fileIds/items',
|
||||||
reason: 'contains invalid file IDs',
|
reason: 'contains invalid file IDs',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,12 +27,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (post == null) throw new ApiError('NO_SUCH_POST');
|
if (post == null) throw new ApiError('NO_SUCH_POST');
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await GalleryLikes.findOneBy({
|
const exist = await GalleryLikes.countBy({
|
||||||
postId: post.id,
|
postId: post.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_LIKED');
|
if (exist) throw new ApiError('ALREADY_LIKED');
|
||||||
|
|
||||||
// Create like
|
// Create like
|
||||||
await GalleryLikes.insert({
|
await GalleryLikes.insert({
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
{
|
{
|
||||||
param: '#/properties/fileIds/items',
|
param: '#/properties/fileIds/items',
|
||||||
reason: 'contains invalid file IDs',
|
reason: 'contains invalid file IDs',
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const query = Hashtags.createQueryBuilder('tag');
|
const query = Hashtags.createQueryBuilder('tag');
|
||||||
|
|
||||||
if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
|
if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const hashtag = await Hashtags.findOneBy({ name: normalizeForSearch(ps.tag) });
|
const hashtag = await Hashtags.findOneBy({ name: normalizeForSearch(ps.tag) });
|
||||||
if (hashtag == null) throw new ApiError('NO_SUCH_HASHTAG');
|
if (hashtag == null) throw new ApiError('NO_SUCH_HASHTAG');
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as speakeasy from 'speakeasy';
|
import * as speakeasy from 'speakeasy';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
import define from '../../../define.js';
|
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
const suspendedQuery = Users.createQueryBuilder('users')
|
const suspendedQuery = Users.createQueryBuilder('users')
|
||||||
.select('users.id')
|
.select('users.id')
|
||||||
.where('users.isSuspended = TRUE');
|
.where('users.isSuspended');
|
||||||
|
|
||||||
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('notification.notifieeId = :meId', { meId: user.id })
|
.andWhere('notification.notifieeId = :meId', { meId: user.id })
|
||||||
|
|
|
@ -25,17 +25,17 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
// Check if announcement exists
|
// Check if announcement exists
|
||||||
const announcement = await Announcements.findOneBy({ id: ps.announcementId });
|
const exists = await Announcements.countBy({ id: ps.announcementId });
|
||||||
|
|
||||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
if (!exists) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
||||||
|
|
||||||
// Check if already read
|
// Check if already read
|
||||||
const read = await AnnouncementReads.findOneBy({
|
const read = await AnnouncementReads.countBy({
|
||||||
announcementId: ps.announcementId,
|
announcementId: ps.announcementId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (read != null) return;
|
if (read) return;
|
||||||
|
|
||||||
// Create read
|
// Create read
|
||||||
await AnnouncementReads.insert({
|
await AnnouncementReads.insert({
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { comparePassword } from '@/misc/password.js';
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
|
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||||
import { Users, UserProfiles } from '@/models/index.js';
|
import { Users, UserProfiles } from '@/models/index.js';
|
||||||
import generateUserToken from '../../common/generate-native-user-token.js';
|
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import generateUserToken from '../../common/generate-native-user-token.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
|
|
@ -18,9 +18,9 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const token = await AccessTokens.findOneBy({ id: ps.tokenId });
|
const exists = await AccessTokens.countBy({ id: ps.tokenId });
|
||||||
|
|
||||||
if (token) {
|
if (exists) {
|
||||||
await AccessTokens.delete({
|
await AccessTokens.delete({
|
||||||
id: ps.tokenId,
|
id: ps.tokenId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -95,12 +95,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP');
|
if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP');
|
||||||
|
|
||||||
// check joined
|
// check joined
|
||||||
const joining = await UserGroupJoinings.findOneBy({
|
const joined = await UserGroupJoinings.countBy({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userGroupId: recipientGroup.id,
|
userGroupId: recipientGroup.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (joining == null) throw new ApiError('ACCESS_DENIED', 'You have to join a group to read messages in it.');
|
if (!joined) throw new ApiError('ACCESS_DENIED', 'You have to join a group to read messages in it.');
|
||||||
|
|
||||||
const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('message.groupId = :groupId', { groupId: recipientGroup.id });
|
.andWhere('message.groupId = :groupId', { groupId: recipientGroup.id });
|
||||||
|
|
|
@ -106,7 +106,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
const block = await Blockings.findOneBy({
|
const block = await Blockings.countBy({
|
||||||
blockerId: recipientUser.id,
|
blockerId: recipientUser.id,
|
||||||
blockeeId: user.id,
|
blockeeId: user.id,
|
||||||
});
|
});
|
||||||
|
@ -118,12 +118,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP');
|
if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP');
|
||||||
|
|
||||||
// check joined
|
// check joined
|
||||||
const joining = await UserGroupJoinings.findOneBy({
|
const joined = await UserGroupJoinings.countBy({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userGroupId: recipientGroup.id,
|
userGroupId: recipientGroup.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (joining == null) throw new ApiError('ACCESS_DENIED', 'You have to join a group to send a message in it.');
|
if (!joined) throw new ApiError('ACCESS_DENIED', 'You have to join a group to send a message in it.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = null;
|
let file = null;
|
||||||
|
|
|
@ -235,7 +235,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
v2: {
|
v2: {
|
||||||
method: 'get'
|
method: 'get',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async () => {
|
||||||
const instance = await fetchMeta(true);
|
const instance = await fetchMeta(true);
|
||||||
|
|
||||||
const emojis = await Emojis.find({
|
const emojis = await Emojis.find({
|
||||||
|
|
|
@ -43,12 +43,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already muting
|
// Check if already muting
|
||||||
const exist = await Mutings.findOneBy({
|
const exist = await Mutings.countBy({
|
||||||
muterId: muter.id,
|
muterId: muter.id,
|
||||||
muteeId: mutee.id,
|
muteeId: mutee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_MUTING');
|
if (exist) throw new ApiError('ALREADY_MUTING');
|
||||||
|
|
||||||
if (ps.expiresAt && ps.expiresAt <= Date.now()) {
|
if (ps.expiresAt && ps.expiresAt <= Date.now()) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('note.visibility = \'public\'')
|
.andWhere('note.visibility = \'public\'')
|
||||||
.andWhere('note.localOnly = FALSE')
|
.andWhere('NOT note.localOnly')
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
@ -65,7 +65,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.poll !== undefined) {
|
if (ps.poll !== undefined) {
|
||||||
query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
|
query.andWhere((ps.poll ? '' : 'NOT') + 'note.hasPoll');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
@ -169,11 +169,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (renote.userId !== user.id) {
|
if (renote.userId !== user.id) {
|
||||||
const block = await Blockings.findOneBy({
|
const blocked = await Blockings.countBy({
|
||||||
blockerId: renote.userId,
|
blockerId: renote.userId,
|
||||||
blockeeId: user.id,
|
blockeeId: user.id,
|
||||||
});
|
});
|
||||||
if (block) throw new ApiError('BLOCKED', 'Blocked by author of note to be renoted.');
|
if (blocked) throw new ApiError('BLOCKED', 'Blocked by author of note to be renoted.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,11 +194,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (reply.userId !== user.id) {
|
if (reply.userId !== user.id) {
|
||||||
const block = await Blockings.findOneBy({
|
const blocked = await Blockings.countBy({
|
||||||
blockerId: reply.userId,
|
blockerId: reply.userId,
|
||||||
blockeeId: user.id,
|
blockeeId: user.id,
|
||||||
});
|
});
|
||||||
if (block) throw new ApiError('BLOCKED', 'Blocked by author of replied to note.');
|
if (blocked) throw new ApiError('BLOCKED', 'Blocked by author of replied to note.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// if already favorited
|
// if already favorited
|
||||||
const exist = await NoteFavorites.findOneBy({
|
const exist = await NoteFavorites.countBy({
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) throw new ApiError('ALREADY_FAVORITED');
|
if (exist) throw new ApiError('ALREADY_FAVORITED');
|
||||||
|
|
||||||
// Create favorite
|
// Create favorite
|
||||||
await NoteFavorites.insert({
|
await NoteFavorites.insert({
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const meta = {
|
||||||
|
|
||||||
v2: {
|
v2: {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
}
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -98,7 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
if (ps.excludeNsfw) {
|
if (ps.excludeNsfw) {
|
||||||
query.andWhere('note.cw IS NULL');
|
query.andWhere('note.cw IS NULL');
|
||||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive")');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -47,11 +47,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const block = await Blockings.findOneBy({
|
const blocked = await Blockings.countBy({
|
||||||
blockerId: note.userId,
|
blockerId: note.userId,
|
||||||
blockeeId: user.id,
|
blockeeId: user.id,
|
||||||
});
|
});
|
||||||
if (block) throw new ApiError('BLOCKED');
|
if (blocked) throw new ApiError('BLOCKED');
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||||
|
@ -99,7 +99,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// check if this thread and notification type is muted
|
// check if this thread and notification type is muted
|
||||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
const threadMuted = await NoteThreadMutings.countBy({
|
||||||
userId: note.userId,
|
userId: note.userId,
|
||||||
threadId: note.threadId || note.id,
|
threadId: note.threadId || note.id,
|
||||||
mutingNotificationTypes: ArrayOverlap(['pollVote']),
|
mutingNotificationTypes: ArrayOverlap(['pollVote']),
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
// check note visibility
|
// check note visibility
|
||||||
const note = await getNote(ps.noteId, user).catch(err => {
|
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('NO_SUCH_NOTE');
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue