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

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.
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
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`
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
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."
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}.\

View file

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

View file

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

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": {
"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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,13 +66,13 @@ 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']);
cleanup();
logger.succ('Imported');
done();
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
{
param: '#/properties/fileIds/items',
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 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({

View file

@ -54,7 +54,7 @@ export default define(meta, paramDef, async (ps, user) => {
{
param: '#/properties/fileIds/items',
reason: 'contains invalid file IDs',
}
},
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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