resolve merge conflicts

This commit is contained in:
Puniko 2023-01-21 08:39:53 +01:00
commit 5d99ccacfd
175 changed files with 932 additions and 794 deletions

View file

@ -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.)

View file

@ -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
Ive been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Lets talk about that. Ive been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Lets talk about that.

View file

@ -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
View 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.

View file

@ -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}.\

View file

@ -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",

View file

@ -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': [

View 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`);
}
}

View file

@ -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`);
}
}

View file

@ -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",

View file

@ -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})`);

View file

@ -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

View file

@ -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') {

View file

@ -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!;
} }

View file

@ -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);
} }

View file

@ -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,
}); });

View file

@ -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;
} }

View file

@ -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;
}
}
} }

View file

@ -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;
}
}
} }

View file

@ -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 })

View file

@ -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.',

View file

@ -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(

View file

@ -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' {

View file

@ -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();
} }

View file

@ -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);
} }

View file

@ -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();
}); });

View file

@ -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');

View file

@ -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';
/** /**
* 稿 * 稿

View file

@ -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}`;
} }

View file

@ -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';
}; };

View file

@ -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) {

View file

@ -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');

View file

@ -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;

View file

@ -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,

View file

@ -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;
}

View file

@ -16,4 +16,4 @@ export async function perform(actor: CacheableRemoteUser, activity: IObject, res
}); });
} }
} }
}; }

View file

@ -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',

View file

@ -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,

View file

@ -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++) {

View file

@ -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';

View file

@ -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();

View file

@ -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(),
}); });

View file

@ -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],

View file

@ -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) {

View file

@ -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', {});
}
}); });

View file

@ -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');

View file

@ -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),
})); }));
}); });

View file

@ -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');

View file

@ -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);
});

View file

@ -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,
}); });

View file

@ -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();
}); });

View file

@ -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) {

View file

@ -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,

View file

@ -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');

View file

@ -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,
}); });

View file

@ -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) {

View file

@ -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,
}); });

View file

@ -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');
} }

View file

@ -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 {

View file

@ -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][];

View file

@ -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][];

View file

@ -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();

View file

@ -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) {

View file

@ -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();
}); });

View file

@ -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);
}); });

View file

@ -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) {

View file

@ -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(() => {});
})(); })();
}); });

View file

@ -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';

View file

@ -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,
}); });

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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(),

View file

@ -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;
}); });

View file

@ -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');
} }
} }

View file

@ -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) });

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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',
} },
); );
} }

View file

@ -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({

View file

@ -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',
} },
); );
} }

View file

@ -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');

View file

@ -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');

View file

@ -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,

View file

@ -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 })

View file

@ -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({

View file

@ -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 = {

View file

@ -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,

View file

@ -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 });

View file

@ -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;

View file

@ -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({

View file

@ -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;

View file

@ -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

View file

@ -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.');
} }
} }

View file

@ -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({

View file

@ -21,7 +21,7 @@ export const meta = {
v2: { v2: {
method: 'get', method: 'get',
} },
} as const; } as const;
export const paramDef = { export const paramDef = {

View file

@ -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

View file

@ -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']),

View file

@ -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