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
|
||||
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.
|
||||
(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.
|
||||
|
||||
For the current status you should see the following:
|
||||
* The Behavioral Fixes [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/3)
|
||||
* The Technological Upkeep [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/4)
|
||||
* The Features [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/5)
|
||||
* Issues labeled with [behaviour-fix](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=44)
|
||||
* Issues labeled with [upkeep](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=43)
|
||||
* Issues labeled with [feature](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=42)
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -70,6 +70,8 @@ Build foundkey with the following:
|
|||
|
||||
`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:
|
||||
|
||||
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 submodule update --init
|
||||
yarn install
|
||||
# Use build-parallel if your system has 4GB or more RAM and want faster builds
|
||||
NODE_ENV=production yarn build
|
||||
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."
|
||||
output: "Output"
|
||||
updateRemoteUser: "Update remote user information"
|
||||
deleteAllFiles: "Delete all files"
|
||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||
removeAllFollowing: "Unfollow all followed users"
|
||||
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"packages/*"
|
||||
],
|
||||
"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:test": "yarn workspace backend run start:test",
|
||||
"init": "yarn migrate",
|
||||
|
|
|
@ -6,7 +6,11 @@ module.exports = {
|
|||
extends: [
|
||||
'../shared/.eslintrc.js',
|
||||
],
|
||||
plugins: [
|
||||
'foundkey-custom-rules',
|
||||
],
|
||||
rules: {
|
||||
'foundkey-custom-rules/typeorm-prefer-count': 'error',
|
||||
'import/order': ['warn', {
|
||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
'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": {
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"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",
|
||||
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||
"start": "node --experimental-json-modules ./built/index.js",
|
||||
|
@ -113,7 +113,6 @@
|
|||
"unzipper": "0.10.11",
|
||||
"uuid": "8.3.2",
|
||||
"web-push": "3.5.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.8.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
|
@ -164,12 +163,12 @@
|
|||
"@types/tmp": "0.2.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"execa": "6.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
|
|
@ -140,7 +140,7 @@ async function connectDb(): 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;
|
||||
for (const mode of modes.filter(mode => clusterLimits[mode] > 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 lock: (key: string, timeout?: number) => Promise<() => void>
|
||||
= redisClient
|
||||
? promisify(redisLock(redisClient, retryDelay))
|
||||
: async () => () => { };
|
||||
const lock: (key: string, timeout?: number) => Promise<() => void> = promisify(redisLock(redisClient, retryDelay));
|
||||
|
||||
/**
|
||||
* Get AP Object lock
|
||||
|
|
|
@ -22,7 +22,7 @@ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'No
|
|||
if (note.visibility === 'specified') return false;
|
||||
|
||||
// skip if the antenna creator is blocked by the note author
|
||||
const blockings = await blockingCache.fetch(noteUser.id);
|
||||
const blockings = (await blockingCache.fetch(noteUser.id)) ?? [];
|
||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { db } from '@/db/postgre.js';
|
|||
import { Meta } from '@/models/entities/meta.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
|
||||
|
@ -57,5 +57,5 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
|
|||
|
||||
await getMeta();
|
||||
|
||||
return cache;
|
||||
return cache!;
|
||||
}
|
||||
|
|
|
@ -54,7 +54,11 @@ export async function getResponse(args: { url: string, method: string, body?: st
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ export async function toDbReaction(reaction?: string | null, idnReacterHost?: st
|
|||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = await Emojis.findOneBy({
|
||||
const emoji = await Emojis.countBy({
|
||||
host: reacterHost ?? IsNull(),
|
||||
name,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { Note } from '@/models/entities/note.js';
|
||||
|
||||
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,
|
||||
})
|
||||
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,
|
||||
})
|
||||
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 { id } from '../id.js';
|
||||
import { User } from './user.js';
|
||||
import { Note } from './note.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'threadId'], { unique: true })
|
||||
|
|
|
@ -155,7 +155,14 @@ export class User {
|
|||
})
|
||||
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', {
|
||||
default: false,
|
||||
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 });
|
||||
if (file == null) return null;
|
||||
|
||||
return await this.pack(file);
|
||||
return await this.pack(file, opts);
|
||||
},
|
||||
|
||||
async packMany(
|
||||
|
|
|
@ -5,7 +5,6 @@ import config from '@/config/index.js';
|
|||
import { Packed } from '@/misc/schema.js';
|
||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
||||
import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.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> {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
return await db.query(
|
||||
`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')
|
||||
.where('message.groupId = :groupId', { groupId: j.userGroupId })
|
||||
.andWhere('message.userId != :userId', { userId })
|
||||
.andWhere('NOT (:userId = ANY(message.reads))', { userId })
|
||||
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
||||
.getOne().then(x => x != null)));
|
||||
|
||||
const [withUser, withGroups] = await Promise.all([
|
||||
MessagingMessages.count({
|
||||
where: {
|
||||
recipientId: userId,
|
||||
isRead: false,
|
||||
...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
|
||||
},
|
||||
take: 1,
|
||||
}).then(count => count > 0),
|
||||
groupQs,
|
||||
]);
|
||||
|
||||
return withUser || withGroups.some(x => x);
|
||||
SELECT 1
|
||||
FROM "messaging_message"
|
||||
JOIN "user_group_joining"
|
||||
ON "messaging_message"."groupId" = "user_group_joining"."userGroupId"
|
||||
WHERE
|
||||
"user_group_joining"."userId" = $1
|
||||
AND
|
||||
"messaging_message"."userId" != $1
|
||||
AND
|
||||
NOT $1 = ANY("messaging_message"."reads")
|
||||
AND
|
||||
"messaging_message"."createdAt" > "user_group_joining"."createdAt"
|
||||
) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
|
||||
const reads = await AnnouncementReads.findBy({
|
||||
userId,
|
||||
});
|
||||
|
||||
const count = await Announcements.countBy(reads.length > 0 ? {
|
||||
id: Not(In(reads.map(read => read.announcementId))),
|
||||
} : {});
|
||||
|
||||
return count > 0;
|
||||
return await db.query(
|
||||
`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);
|
||||
},
|
||||
|
||||
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
|
||||
|
||||
const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({
|
||||
antennaId: In(myAntennas.map(x => x.id)),
|
||||
read: false,
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
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`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||
const channels = await ChannelFollowings.findBy({ followerId: userId });
|
||||
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
|
||||
userId,
|
||||
noteChannelId: In(channels.map(x => x.followeeId)),
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "note_unread" WHERE "noteChannelId" IN (SELECT "followeeId" FROM "channel_following" WHERE "followerId" = $1)) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.muteeId);
|
||||
|
||||
const count = await Notifications.count({
|
||||
where: {
|
||||
notifieeId: userId,
|
||||
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
|
||||
isRead: false,
|
||||
},
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
return await db.query(
|
||||
`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]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
|
||||
const count = await FollowRequests.countBy({
|
||||
followeeId: userId,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "follow_request" WHERE "followeeId" = $1) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
|
||||
|
|
|
@ -4,20 +4,6 @@ const dateTimeIntervals = {
|
|||
'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 {
|
||||
return a.getTime() === b.getTime();
|
||||
}
|
||||
|
|
|
@ -83,10 +83,8 @@ export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise
|
|||
}
|
||||
}
|
||||
|
||||
// soft指定されている場合は物理削除しない
|
||||
if (job.data.soft) {
|
||||
// nop
|
||||
} else {
|
||||
// No physical deletion if soft is specified.
|
||||
if (!job.data.soft) {
|
||||
await Users.delete(job.data.user.id);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
const emoji = await Emojis.insert({
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
|
@ -66,7 +66,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
}).then(x => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
|
|
@ -2,7 +2,7 @@ import Bull from 'bull';
|
|||
import { In, LessThan } from 'typeorm';
|
||||
import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.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';
|
||||
|
||||
const logger = queueLogger.createSubLogger('check-expired');
|
||||
|
|
|
@ -4,7 +4,7 @@ import { extractDbHost } from '@/misc/convert-host.js';
|
|||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.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 { Users } from '@/models/index.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> {
|
||||
apLogger.info(`Deleting the Actor: ${uri}`);
|
||||
|
@ -17,14 +17,9 @@ export async function deleteActor(actor: CacheableRemoteUser, uri: string): Prom
|
|||
return 'ok: gone';
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
const following = await Followings.findOneBy({
|
||||
const following = await Followings.countBy({
|
||||
followerId: follower.id,
|
||||
followeeId: actor.id,
|
||||
});
|
||||
|
@ -22,5 +22,5 @@ export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<st
|
|||
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';
|
||||
}
|
||||
|
||||
const [req, following] = await Promise.all([
|
||||
FollowRequests.findOneBy({
|
||||
const [requested, following] = await Promise.all([
|
||||
FollowRequests.countBy({
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
}),
|
||||
Followings.findOneBy({
|
||||
Followings.countBy({
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (req) {
|
||||
if (requested) {
|
||||
await cancelFollowRequest(followee, actor);
|
||||
return 'ok: follow request canceled';
|
||||
} else if (following) {
|
||||
|
|
|
@ -24,7 +24,7 @@ import { DbResolver } from '../db-resolver.js';
|
|||
import { apLogger } from '../logger.js';
|
||||
import { resolvePerson } from './person.js';
|
||||
import { resolveImage } from './image.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import { extractApHashtags, extractQuoteUrl } from './tag.js';
|
||||
import { extractPollFromQuestion } from './question.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);
|
||||
if (uri.startsWith(config.url + '/')) {
|
||||
const id = uri.split('/').pop();
|
||||
const talk = await MessagingMessages.findOneBy({ id });
|
||||
const talk = await MessagingMessages.countBy({ id });
|
||||
if (talk) {
|
||||
isTalk = true;
|
||||
return null;
|
||||
|
@ -154,10 +154,10 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
})
|
||||
: 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<{
|
||||
status: 'ok';
|
||||
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 results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||
const uris = unique([quoteUrl, note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||
// check the urls sequentially and abort early to not do unnecessary HTTP requests
|
||||
// picks the first one that works
|
||||
for (const uri in uris) {
|
||||
const res = await tryResolveNote(uri);
|
||||
if (res.status === 'ok') {
|
||||
quote = res.res;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!quote) {
|
||||
if (results.some(x => x.status === 'temperror')) {
|
||||
throw new Error('quote resolve failed');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { URL } from 'node:url';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
|
||||
import config from '@/config/index.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
|
@ -40,7 +40,6 @@ const summaryLength = 2048;
|
|||
* @param uri Fetch target URI
|
||||
*/
|
||||
function validateActor(x: IObject): IActor {
|
||||
|
||||
if (x == 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');
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
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');
|
||||
}
|
||||
|
||||
const expectHost = extractDbHost(uri);
|
||||
const publicKeyIdHost = extractDbHost(x.publicKey.id);
|
||||
if (publicKeyIdHost !== expectHost) {
|
||||
// This is a security critical check to not insert or change an entry of
|
||||
// UserPublickey to point to a local key id.
|
||||
if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) {
|
||||
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.
|
||||
*/
|
||||
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');
|
||||
|
||||
const cached = uriPersonCache.get(uri);
|
||||
if (cached) return cached;
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
// If the URI points to this server, fetch from database.
|
||||
if (uri.startsWith(config.url + '/')) {
|
||||
const id = uri.split('/').pop();
|
||||
const u = await Users.findOneBy({ id });
|
||||
|
@ -130,10 +135,6 @@ export async function fetchPerson(uri: string, resolver: Resolver): Promise<Cach
|
|||
* Personを作成します。
|
||||
*/
|
||||
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 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> {
|
||||
const uri = getApId(value);
|
||||
|
||||
// skip local URIs
|
||||
if (uri.startsWith(config.url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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');
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
const exist = await fetchPerson(uri, resolver);
|
||||
const exist = await fetchPerson(uri);
|
||||
|
||||
if (exist) {
|
||||
return exist;
|
||||
|
|
|
@ -20,10 +20,10 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
|
|||
}
|
||||
|
||||
const choices = question[multiple ? 'anyOf' : 'oneOf']!
|
||||
.map((x, i) => x.name!);
|
||||
.map(x => x.name!);
|
||||
|
||||
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 {
|
||||
choices,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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) {
|
||||
if (tags == null) return [];
|
||||
|
@ -16,3 +16,34 @@ export function extractApHashtagObjects(tags: IObject | IObject[] | null | undef
|
|||
if (tags == null) return [];
|
||||
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',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// Fedibird
|
||||
fedibird: 'http://fedibird.com/ns#',
|
||||
quoteUri: 'fedibird:quoteUri',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
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 });
|
||||
|
||||
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) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
} else {
|
||||
|
@ -111,6 +111,16 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
|||
...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 ? {
|
||||
type: 'Question',
|
||||
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',
|
||||
},
|
||||
_misskey_quote: quote,
|
||||
quoteUrl: quote,
|
||||
quoteUri: quote,
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
cc,
|
||||
|
|
|
@ -30,14 +30,15 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
|||
// don't allow redirects on the inbox
|
||||
redirect: 'error',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AP object with http-signature
|
||||
* @param user http-signature user
|
||||
* @param url URL to fetch
|
||||
*/
|
||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
||||
export async function signedGet(_url: string, user: { id: User['id'] }): Promise<any> {
|
||||
let url = _url;
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
for (let redirects = 0; redirects < 3; redirects++) {
|
||||
|
|
|
@ -45,7 +45,7 @@ export function getOneApId(value: ApObject): string {
|
|||
/**
|
||||
* 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.id === 'string') return value.id;
|
||||
throw new Error('cannot detemine id');
|
||||
|
@ -54,7 +54,7 @@ export function getApId(value: string | IObject): string {
|
|||
/**
|
||||
* 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 (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||
throw new Error('cannot detect type');
|
||||
|
@ -111,7 +111,7 @@ export interface IPost extends IObject {
|
|||
mediaType: string;
|
||||
};
|
||||
_misskey_quote?: string;
|
||||
quoteUrl?: string;
|
||||
quoteUri?: string;
|
||||
_misskey_talk: boolean;
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ export interface IQuestion extends IObject {
|
|||
mediaType: string;
|
||||
};
|
||||
_misskey_quote?: string;
|
||||
quoteUrl?: string;
|
||||
quoteUri?: string;
|
||||
oneOf?: IQuestionChoice[];
|
||||
anyOf?: IQuestionChoice[];
|
||||
endTime?: Date;
|
||||
|
@ -196,24 +196,6 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
|||
typeof object.name === '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 {
|
||||
type: 'Emoji';
|
||||
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 isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
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\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE');
|
||||
.andWhere('NOT note.localOnly');
|
||||
|
||||
const notes = await query.take(limit).getMany();
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ export async function signup(opts: {
|
|||
|
||||
// Start transaction
|
||||
await db.transaction(async transactionalEntityManager => {
|
||||
const exist = await transactionalEntityManager.findOneBy(User, {
|
||||
const exist = await transactionalEntityManager.countBy(User, {
|
||||
usernameLower: username.toLowerCase(),
|
||||
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_updateMeta from './endpoints/admin/update-meta.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___antennas_create from './endpoints/antennas/create.js';
|
||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||
|
@ -363,7 +362,6 @@ const eps = [
|
|||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||
['admin/update-meta', ep___admin_updateMeta],
|
||||
['admin/vacuum', ep___admin_vacuum],
|
||||
['admin/delete-account', ep___admin_deleteAccount],
|
||||
['announcements', ep___announcements],
|
||||
['antennas/create', ep___antennas_create],
|
||||
['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);
|
||||
|
||||
switch (ps.state) {
|
||||
case 'resolved': query.andWhere('report.resolved = TRUE'); break;
|
||||
case 'unresolved': query.andWhere('report.resolved = FALSE'); break;
|
||||
case 'resolved': query.andWhere('report.resolved'); break;
|
||||
case 'unresolved': query.andWhere('NOT report.resolved'); break;
|
||||
}
|
||||
|
||||
switch (ps.reporterOrigin) {
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { Users } from '@/models/index.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { doPostSuspend } from '@/services/suspend-user.js';
|
||||
import { publishUserEvent } from '@/services/stream.js';
|
||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
||||
import { deleteAccount } from '@/services/delete-account.js';
|
||||
import define from '../../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
|
||||
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
|
||||
} as const;
|
||||
|
@ -23,36 +21,19 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const user = await Users.findOneBy({ id: ps.userId });
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId,
|
||||
isDeleted: false,
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
throw new ApiError('NO_SUCH_USER');
|
||||
} else if (user.isAdmin) {
|
||||
throw new ApiError('IS_ADMIN');
|
||||
} else if(user.isModerator) {
|
||||
} else if (user.isModerator) {
|
||||
throw new ApiError('IS_MODERATOR');
|
||||
}
|
||||
|
||||
if (Users.isLocalUser(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', {});
|
||||
}
|
||||
await deleteAccount(user);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 });
|
||||
|
||||
if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT');
|
||||
|
|
|
@ -84,6 +84,6 @@ export default define(meta, paramDef, async (ps) => {
|
|||
title: announcement.title,
|
||||
text: announcement.text,
|
||||
imageUrl: announcement.imageUrl,
|
||||
reads: reads.get(announcement)!,
|
||||
reads: reads.get(announcement),
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 });
|
||||
|
||||
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;
|
||||
|
||||
// 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({
|
||||
userId: ps.userId,
|
||||
});
|
||||
|
|
|
@ -15,6 +15,6 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
export default define(meta, paramDef, async () => {
|
||||
createCleanRemoteFilesJob();
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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);
|
||||
|
||||
if (ps.userId) {
|
||||
|
|
|
@ -163,7 +163,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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({
|
||||
where: [{
|
||||
url: ps.url,
|
||||
|
|
|
@ -37,7 +37,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 });
|
||||
|
||||
if (emoji == null) throw new ApiError('NO_SUCH_EMOJI');
|
||||
|
|
|
@ -18,7 +18,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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({
|
||||
userHost: ps.host,
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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) });
|
||||
|
||||
if (instance == null) {
|
||||
|
|
|
@ -18,7 +18,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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({
|
||||
followerHost: ps.host,
|
||||
});
|
||||
|
|
|
@ -22,10 +22,10 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const instanceExists = await Instances.countBy({ host: toPuny(ps.host) });
|
||||
|
||||
if (instance == null) {
|
||||
if (!instanceExists) {
|
||||
throw new ApiError('NO_SUCH_OBJECT');
|
||||
}
|
||||
|
||||
|
|
|
@ -256,7 +256,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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);
|
||||
|
||||
return {
|
||||
|
|
|
@ -39,7 +39,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 res = [] as [string, number][];
|
||||
|
|
|
@ -39,7 +39,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 res = [] as [string, number][];
|
||||
|
|
|
@ -38,7 +38,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 inboxJobCounts = await inboxQueue.getJobCounts();
|
||||
const dbJobCounts = await dbQueue.getJobCounts();
|
||||
|
|
|
@ -49,7 +49,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
try {
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only');
|
||||
} catch (e) {
|
||||
|
|
|
@ -46,6 +46,6 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
|
|
@ -17,6 +17,6 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
|
|
@ -26,13 +26,11 @@ export const paramDef = {
|
|||
offset: { type: 'integer', default: 0 },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
||||
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 },
|
||||
hostname: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: 'The local host is represented with `null`.',
|
||||
description: "To represent the local host, use `origin: 'local'` instead.",
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
|
@ -43,13 +41,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
const query = Users.createQueryBuilder('user');
|
||||
|
||||
switch (ps.state) {
|
||||
case 'available': query.where('user.isSuspended = FALSE'); break;
|
||||
case 'admin': query.where('user.isAdmin = TRUE'); break;
|
||||
case 'moderator': query.where('user.isModerator = TRUE'); break;
|
||||
case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
|
||||
case 'available': query.where('NOT user.isSuspended'); break;
|
||||
case 'admin': query.where('user.isAdmin'); break;
|
||||
case 'moderator': query.where('user.isModerator'); 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 'silenced': query.where('user.isSilenced = TRUE'); break;
|
||||
case 'suspended': query.where('user.isSuspended = TRUE'); break;
|
||||
case 'silenced': query.where('user.isSilenced'); break;
|
||||
case 'suspended': query.where('user.isSuspended'); break;
|
||||
}
|
||||
|
||||
switch (ps.origin) {
|
||||
|
|
|
@ -50,9 +50,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
|
||||
(async () => {
|
||||
await doPostSuspend(user).catch(e => {});
|
||||
await unFollowAll(user).catch(e => {});
|
||||
await readAllNotify(user).catch(e => {});
|
||||
await doPostSuspend(user).catch(() => {});
|
||||
await unFollowAll(user).catch(() => {});
|
||||
await readAllNotify(user).catch(() => {});
|
||||
})();
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { extractDbHost } from '@/misc/convert-host.js';
|
|||
import { Users, Notes } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.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 { HOUR } from '@/const.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
|
|
|
@ -27,7 +27,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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({
|
||||
token: ps.token,
|
||||
});
|
||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already blocking
|
||||
const exist = await Blockings.findOneBy({
|
||||
const blocked = await Blockings.countBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
|
||||
if (exist != null) throw new ApiError('ALREADY_BLOCKING');
|
||||
if (blocked) throw new ApiError('ALREADY_BLOCKING');
|
||||
|
||||
await create(blocker, blockee);
|
||||
|
||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not blocking
|
||||
const exist = await Blockings.findOneBy({
|
||||
const exist = await Blockings.countBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
|
||||
if (exist == null) throw new ApiError('NOT_BLOCKING');
|
||||
if (!exist) throw new ApiError('NOT_BLOCKING');
|
||||
|
||||
// Delete blocking
|
||||
await deleteBlocking(blocker, blockee);
|
||||
|
|
|
@ -7,6 +7,8 @@ import { ApiError } from '../../error.js';
|
|||
export const meta = {
|
||||
tags: ['channels'],
|
||||
|
||||
description: 'Creates a new channel with the current user as its administrator.',
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
@ -32,14 +34,13 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
let banner = null;
|
||||
if (ps.bannerId != null) {
|
||||
banner = await DriveFiles.findOneBy({
|
||||
const bannerExists = await DriveFiles.countBy({
|
||||
id: ps.bannerId,
|
||||
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({
|
||||
|
@ -47,8 +48,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
description: ps.description || null,
|
||||
bannerId: banner ? banner.id : null,
|
||||
description: ps.description,
|
||||
bannerId: ps.bannerId,
|
||||
} as Channel).then(x => Channels.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await Channels.pack(channel, user);
|
||||
|
|
|
@ -37,12 +37,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw err;
|
||||
});
|
||||
|
||||
const exist = await ClipNotes.findOneBy({
|
||||
const exist = await ClipNotes.countBy({
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
});
|
||||
|
||||
if (exist != null) throw new ApiError('ALREADY_CLIPPED');
|
||||
if (exist) throw new ApiError('ALREADY_CLIPPED');
|
||||
|
||||
await ClipNotes.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -26,10 +26,8 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const file = await DriveFiles.findOneBy({
|
||||
return 0 < await DriveFiles.countBy({
|
||||
md5: ps.md5,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return file != null;
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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');
|
||||
|
||||
switch (ps.sort) {
|
||||
|
@ -69,17 +69,17 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
if (typeof ps.notResponding === 'boolean') {
|
||||
if (ps.notResponding) {
|
||||
query.andWhere('instance.isNotResponding = TRUE');
|
||||
query.andWhere('instance.isNotResponding');
|
||||
} else {
|
||||
query.andWhere('instance.isNotResponding = FALSE');
|
||||
query.andWhere('NOT instance.isNotResponding');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ps.suspended === 'boolean') {
|
||||
if (ps.suspended) {
|
||||
query.andWhere('instance.isSuspended = TRUE');
|
||||
query.andWhere('instance.isSuspended');
|
||||
} else {
|
||||
query.andWhere('instance.isSuspended = FALSE');
|
||||
query.andWhere('NOT instance.isSuspended');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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) });
|
||||
|
||||
|
|
|
@ -49,12 +49,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already following
|
||||
const exist = await Followings.findOneBy({
|
||||
const exist = await Followings.countBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (exist != null) throw new ApiError('ALREADY_FOLLOWING');
|
||||
if (exist) throw new ApiError('ALREADY_FOLLOWING');
|
||||
|
||||
try {
|
||||
await create(follower, followee);
|
||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await Followings.findOneBy({
|
||||
const exist = await Followings.countBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (exist == null) throw new ApiError('NOT_FOLLOWING');
|
||||
if (!exist) throw new ApiError('NOT_FOLLOWING');
|
||||
|
||||
await deleteFollowing(follower, followee);
|
||||
|
||||
|
|
|
@ -48,12 +48,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await Followings.findOneBy({
|
||||
const exist = await Followings.countBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
});
|
||||
|
||||
if (exist == null) throw new ApiError('NOT_FOLLOWING');
|
||||
if (!exist) throw new ApiError('NOT_FOLLOWING');
|
||||
|
||||
await deleteFollowing(follower, followee);
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
{
|
||||
param: '#/properties/fileIds/items',
|
||||
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 already liked
|
||||
const exist = await GalleryLikes.findOneBy({
|
||||
const exist = await GalleryLikes.countBy({
|
||||
postId: post.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist != null) throw new ApiError('ALREADY_LIKED');
|
||||
if (exist) throw new ApiError('ALREADY_LIKED');
|
||||
|
||||
// Create like
|
||||
await GalleryLikes.insert({
|
||||
|
|
|
@ -54,7 +54,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
{
|
||||
param: '#/properties/fileIds/items',
|
||||
reason: 'contains invalid file IDs',
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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');
|
||||
|
||||
if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
|
||||
|
|
|
@ -26,7 +26,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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) });
|
||||
if (hashtag == null) throw new ApiError('NO_SUCH_HASHTAG');
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as speakeasy from 'speakeasy';
|
||||
import { UserProfiles } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import define from '../../../define.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
|
|
@ -72,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
const suspendedQuery = Users.createQueryBuilder('users')
|
||||
.select('users.id')
|
||||
.where('users.isSuspended = TRUE');
|
||||
.where('users.isSuspended');
|
||||
|
||||
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
||||
.andWhere('notification.notifieeId = :meId', { meId: user.id })
|
||||
|
|
|
@ -25,17 +25,17 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// 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
|
||||
const read = await AnnouncementReads.findOneBy({
|
||||
const read = await AnnouncementReads.countBy({
|
||||
announcementId: ps.announcementId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (read != null) return;
|
||||
if (read) return;
|
||||
|
||||
// Create read
|
||||
await AnnouncementReads.insert({
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { comparePassword } from '@/misc/password.js';
|
||||
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.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 generateUserToken from '../../common/generate-native-user-token.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
|
@ -18,9 +18,9 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
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({
|
||||
id: ps.tokenId,
|
||||
userId: user.id,
|
||||
|
|
|
@ -95,12 +95,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP');
|
||||
|
||||
// check joined
|
||||
const joining = await UserGroupJoinings.findOneBy({
|
||||
const joined = await UserGroupJoinings.countBy({
|
||||
userId: user.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)
|
||||
.andWhere('message.groupId = :groupId', { groupId: recipientGroup.id });
|
||||
|
|
|
@ -106,7 +106,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check blocking
|
||||
const block = await Blockings.findOneBy({
|
||||
const block = await Blockings.countBy({
|
||||
blockerId: recipientUser.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');
|
||||
|
||||
// check joined
|
||||
const joining = await UserGroupJoinings.findOneBy({
|
||||
const joined = await UserGroupJoinings.countBy({
|
||||
userId: user.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;
|
||||
|
|
|
@ -235,7 +235,7 @@ export const meta = {
|
|||
},
|
||||
|
||||
v2: {
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -253,7 +253,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// 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 emojis = await Emojis.find({
|
||||
|
|
|
@ -43,12 +43,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await Mutings.findOneBy({
|
||||
const exist = await Mutings.countBy({
|
||||
muterId: muter.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()) {
|
||||
return;
|
||||
|
|
|
@ -35,7 +35,7 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.localOnly = FALSE')
|
||||
.andWhere('NOT note.localOnly')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
|
@ -65,7 +65,7 @@ export default define(meta, paramDef, async (ps) => {
|
|||
}
|
||||
|
||||
if (ps.poll !== undefined) {
|
||||
query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
|
||||
query.andWhere((ps.poll ? '' : 'NOT') + 'note.hasPoll');
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
|
|
@ -169,11 +169,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
// Check blocking
|
||||
if (renote.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
const blocked = await Blockings.countBy({
|
||||
blockerId: renote.userId,
|
||||
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
|
||||
if (reply.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
const blocked = await Blockings.countBy({
|
||||
blockerId: reply.userId,
|
||||
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
|
||||
const exist = await NoteFavorites.findOneBy({
|
||||
const exist = await NoteFavorites.countBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist != null) throw new ApiError('ALREADY_FAVORITED');
|
||||
if (exist) throw new ApiError('ALREADY_FAVORITED');
|
||||
|
||||
// Create favorite
|
||||
await NoteFavorites.insert({
|
||||
|
|
|
@ -21,7 +21,7 @@ export const meta = {
|
|||
|
||||
v2: {
|
||||
method: 'get',
|
||||
}
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
|
@ -98,7 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
if (ps.excludeNsfw) {
|
||||
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
|
||||
|
|
|
@ -47,11 +47,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const block = await Blockings.findOneBy({
|
||||
const blocked = await Blockings.countBy({
|
||||
blockerId: note.userId,
|
||||
blockeeId: user.id,
|
||||
});
|
||||
if (block) throw new ApiError('BLOCKED');
|
||||
if (blocked) throw new ApiError('BLOCKED');
|
||||
}
|
||||
|
||||
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
|
||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
||||
const threadMuted = await NoteThreadMutings.countBy({
|
||||
userId: note.userId,
|
||||
threadId: note.threadId || note.id,
|
||||
mutingNotificationTypes: ArrayOverlap(['pollVote']),
|
||||
|
|
|
@ -51,7 +51,7 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// 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');
|
||||
throw err;
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue