merge: upstream

This commit is contained in:
Marie 2024-02-03 20:19:44 +01:00
commit 11628e4b6a
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
285 changed files with 3413 additions and 1913 deletions

View file

@ -3,6 +3,6 @@ import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config);
const spec = genOpenapiSpec(config, true);
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FixMetaDisableRegistration1706791962000 {
name = 'FixMetaDisableRegistration1706791962000'
async up(queryRunner) {
await queryRunner.query(`alter table meta alter column "disableRegistration" set default true;`);
}
async down(queryRunner) {
await queryRunner.query(`alter table meta alter column "disableRegistration" set default false;`);
}
}

View file

@ -65,9 +65,9 @@
"dependencies": {
"@aws-sdk/client-s3": "3.412.0",
"@aws-sdk/lib-storage": "3.412.0",
"@bull-board/api": "5.10.2",
"@bull-board/fastify": "5.10.2",
"@bull-board/ui": "5.10.2",
"@bull-board/api": "5.14.0",
"@bull-board/fastify": "5.14.0",
"@bull-board/ui": "5.14.0",
"@discordapp/twemoji": "15.0.2",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
@ -84,11 +84,11 @@
"@nestjs/testing": "10.2.10",
"@peertube/http-signature": "1.7.0",
"@transfem-org/sfm-js": "0.24.4",
"@simplewebauthn/server": "9.0.0",
"@simplewebauthn/server": "9.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63",
"@swc/core": "1.3.105",
"@swc/core": "1.3.107",
"@twemoji/parser": "15.0.0",
"accepts": "1.3.8",
"ajv": "8.12.0",
@ -98,7 +98,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.1.4",
"bullmq": "5.1.5",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -117,7 +117,7 @@
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"glob": "10.3.10",
"got": "14.0.0",
"got": "14.1.0",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"http-link-header": "1.1.1",
@ -148,7 +148,7 @@
"otpauth": "9.2.2",
"parse5": "7.1.2",
"pg": "8.11.3",
"pkce-challenge": "4.0.1",
"pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
@ -169,12 +169,12 @@
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.21.23",
"systeminformation": "5.21.24",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.19",
"typeorm": "0.3.20",
"typescript": "5.3.3",
"ulid": "2.3.0",
"uuid": "^9.0.1",
@ -186,7 +186,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0",
"@nestjs/platform-express": "10.3.0",
"@nestjs/platform-express": "10.3.1",
"@simplewebauthn/typescript-types": "8.3.4",
"@swc/jest": "0.2.31",
"@types/accepts": "1.3.7",
@ -205,13 +205,13 @@
"@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.11.5",
"@types/node": "20.11.10",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4",
"@types/oauth2orize": "1.11.3",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.10.9",
"@types/pg": "8.11.0",
"@types/pug": "2.0.10",
"@types/punycode": "2.1.3",
"@types/qrcode": "1.5.5",

View file

@ -76,10 +76,10 @@ type Source = {
deliverJobConcurrency?: number;
inboxJobConcurrency?: number;
relashionshipJobConcurrency?: number;
relationshipJobConcurrency?: number;
deliverJobPerSec?: number;
inboxJobPerSec?: number;
relashionshipJobPerSec?: number;
relationshipJobPerSec?: number;
deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number;
@ -141,10 +141,10 @@ export type Config = {
outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined;
deliverJobConcurrency: number | undefined;
inboxJobConcurrency: number | undefined;
relashionshipJobConcurrency: number | undefined;
relationshipJobConcurrency: number | undefined;
deliverJobPerSec: number | undefined;
inboxJobPerSec: number | undefined;
relashionshipJobPerSec: number | undefined;
relationshipJobPerSec: number | undefined;
deliverJobMaxAttempts: number | undefined;
inboxJobMaxAttempts: number | undefined;
proxyRemoteFiles: boolean | undefined;
@ -257,10 +257,10 @@ export function loadConfig(): Config {
outgoingAddressFamily: config.outgoingAddressFamily,
deliverJobConcurrency: config.deliverJobConcurrency,
inboxJobConcurrency: config.inboxJobConcurrency,
relashionshipJobConcurrency: config.relashionshipJobConcurrency,
relationshipJobConcurrency: config.relationshipJobConcurrency,
deliverJobPerSec: config.deliverJobPerSec,
inboxJobPerSec: config.inboxJobPerSec,
relashionshipJobPerSec: config.relashionshipJobPerSec,
relationshipJobPerSec: config.relationshipJobPerSec,
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,

View file

@ -96,7 +96,7 @@ export class AccountMoveService {
await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true });
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
// Unfollow after 24 hours

View file

@ -54,15 +54,15 @@ export interface MainEventTypes {
reply: Packed<'Note'>;
renote: Packed<'Note'>;
follow: Packed<'UserDetailedNotMe'>;
followed: Packed<'User'>;
unfollow: Packed<'User'>;
meUpdated: Packed<'User'>;
followed: Packed<'UserLite'>;
unfollow: Packed<'UserDetailedNotMe'>;
meUpdated: Packed<'MeDetailed'>;
pageEvent: {
pageId: MiPage['id'];
event: string;
var: any;
userId: MiUser['id'];
user: Packed<'User'>;
user: Packed<'UserDetailed'>;
};
urlUploadFinished: {
marker?: string | null;
@ -92,7 +92,7 @@ export interface MainEventTypes {
};
driveFileCreated: Packed<'DriveFile'>;
readAntenna: MiAntenna;
receiveFollowRequest: Packed<'User'>;
receiveFollowRequest: Packed<'UserLite'>;
announcementCreated: {
announcement: Packed<'Announcement'>;
};
@ -143,8 +143,8 @@ type NoteStreamEventTypes = {
};
export interface UserListEventTypes {
userAdded: Packed<'User'>;
userRemoved: Packed<'User'>;
userAdded: Packed<'UserLite'>;
userRemoved: Packed<'UserLite'>;
}
export interface AntennaEventTypes {

View file

@ -109,13 +109,13 @@ export class UserBlockingService implements OnModuleInit {
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, {
detail: true,
schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
if (this.userEntityService.isLocalUser(follower) && !silent) {
this.userEntityService.pack(followee, follower, {
detail: true,
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);

View file

@ -293,9 +293,9 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isLocalUser(follower) && !silent) {
// Publish follow event
this.userEntityService.pack(followee.id, follower, {
detail: true,
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishMainStream(follower.id, 'follow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) {
@ -360,7 +360,7 @@ export class UserFollowingService implements OnModuleInit {
if (!silent && this.userEntityService.isLocalUser(follower)) {
// Publish unfollow event
this.userEntityService.pack(followee.id, follower, {
detail: true,
schema: 'UserDetailedNotMe',
}).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
@ -500,7 +500,7 @@ export class UserFollowingService implements OnModuleInit {
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(followee.id, followee, {
detail: true,
schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成
@ -548,7 +548,7 @@ export class UserFollowingService implements OnModuleInit {
});
this.userEntityService.pack(followee.id, followee, {
detail: true,
schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
@ -576,7 +576,7 @@ export class UserFollowingService implements OnModuleInit {
}
this.userEntityService.pack(followee.id, followee, {
detail: true,
schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
}
@ -696,7 +696,7 @@ export class UserFollowingService implements OnModuleInit {
@bindThis
private async publishUnfollow(followee: Both, follower: Local): Promise<void> {
const packedFollowee = await this.userEntityService.pack(followee.id, follower, {
detail: true,
schema: 'UserDetailedNotMe',
});
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);

View file

@ -94,6 +94,29 @@ type ToJsonSchema<S> = {
};
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
const unflatten = (str: string, parent: Record<string, any>) => {
const keys = str.split('.');
const key = keys.shift();
const nextKey = keys[0];
if (key == null) return;
if (parent.properties[key] == null) {
parent.properties[key] = nextKey ? {
type: 'object',
properties: {},
required: [],
} : {
type: 'array',
items: {
type: 'number',
},
};
}
if (nextKey) unflatten(keys.join('.'), parent.properties[key] as Record<string, any>);
};
const jsonSchema = {
type: 'object',
properties: {} as Record<string, unknown>,
@ -101,10 +124,7 @@ export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatt
};
for (const k in schema) {
jsonSchema.properties[k] = {
type: 'array',
items: { type: 'number' },
};
unflatten(k, jsonSchema);
}
return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;

View file

@ -38,13 +38,13 @@ export class AbuseUserReportEntityService {
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
detail: true,
schema: 'UserDetailedNotMe',
}),
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
detail: true,
schema: 'UserDetailedNotMe',
}),
assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
detail: true,
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
});

View file

@ -37,7 +37,7 @@ export class BlockingEntityService {
createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId,
blockee: this.userEntityService.pack(blocking.blockeeId, me, {
detail: true,
schema: 'UserDetailedNotMe',
}),
});
}

View file

@ -42,7 +42,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title,
summary: flash.summary,
script: flash.script,

View file

@ -89,10 +89,10 @@ export class FollowingEntityService {
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
detail: true,
schema: 'UserDetailedNotMe',
}) : undefined,
follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
detail: true,
schema: 'UserDetailedNotMe',
}) : undefined,
});
}

View file

@ -37,7 +37,7 @@ export class ModerationLogEntityService {
info: log.info,
userId: log.userId,
user: this.userEntityService.pack(log.user ?? log.userId, null, {
detail: true,
schema: 'UserDetailedNotMe',
}),
});
}

View file

@ -39,7 +39,7 @@ export class MutingEntityService {
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, {
detail: true,
schema: 'UserDetailedNotMe',
}),
});
}

View file

@ -181,7 +181,7 @@ export class NoteEntityService implements OnModuleInit {
return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
expiresAt: poll.expiresAt?.toISOString() ?? null,
choices,
};
}
@ -342,9 +342,7 @@ export class NoteEntityService implements OnModuleInit {
createdAt: this.idService.parse(note.id).date.toISOString(),
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, {
detail: false,
}),
user: this.userEntityService.pack(note.user ?? note.userId, me),
text: text,
cw: note.cw,
visibility: note.visibility,

View file

@ -62,7 +62,7 @@ export class NotificationEntityService implements OnModuleInit {
},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'User'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'>> {
const notification = src;
@ -76,9 +76,7 @@ export class NotificationEntityService implements OnModuleInit {
const userIfNeed = 'notifierId' in notification ? (
hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId }, {
detail: false,
})
: this.userEntityService.pack(notification.notifierId, { id: meId })
) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
@ -131,9 +129,7 @@ export class NotificationEntityService implements OnModuleInit {
const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) },
}) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
detail: false,
});
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
// 既に解決されたフォローリクエストの通知を除外
@ -161,7 +157,7 @@ export class NotificationEntityService implements OnModuleInit {
},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'User'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'>> {
const notification = src;
@ -175,18 +171,14 @@ export class NotificationEntityService implements OnModuleInit {
const userIfNeed = 'notifierId' in notification ? (
hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId }, {
detail: false,
})
: this.userEntityService.pack(notification.notifierId, { id: meId })
) : undefined;
if (notification.type === 'reaction:grouped') {
const reactions = await Promise.all(notification.reactions.map(async reaction => {
const user = hint?.packedUsers != null
? hint.packedUsers.get(reaction.userId)!
: await this.userEntityService.pack(reaction.userId, { id: meId }, {
detail: false,
});
: await this.userEntityService.pack(reaction.userId, { id: meId });
return {
user,
reaction: reaction.reaction,
@ -206,9 +198,7 @@ export class NotificationEntityService implements OnModuleInit {
return packedUser;
}
return this.userEntityService.pack(userId, { id: meId }, {
detail: false,
});
return this.userEntityService.pack(userId, { id: meId });
}));
return await awaitAll({
id: notification.id,
@ -275,9 +265,7 @@ export class NotificationEntityService implements OnModuleInit {
const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) },
}) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
detail: false,
});
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
// 既に解決されたフォローリクエストの通知を除外

View file

@ -90,7 +90,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意
user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,

View file

@ -38,7 +38,7 @@ export class RenoteMutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, {
detail: true,
schema: 'UserDetailedNotMe',
}),
});
}

View file

@ -30,14 +30,6 @@ import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
Detailed extends true ?
ExpectsMe extends true ? Packed<'MeDetailed'> :
ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
Packed<'UserDetailed'> :
Packed<'UserLite'>;
const Ajv = _Ajv.default;
const ajv = new Ajv();
@ -304,17 +296,17 @@ export class UserEntityService implements OnModuleInit {
return `${this.config.url}/users/${userId}`;
}
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
public async pack<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
src: MiUser['id'] | MiUser,
me?: { id: MiUser['id']; } | null | undefined,
options?: {
detail?: D,
schema?: S,
includeSecrets?: boolean,
userProfile?: MiUserProfile,
},
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
): Promise<Packed<S>> {
const opts = Object.assign({
detail: false,
schema: 'UserLite',
includeSecrets: false,
}, options);
@ -346,19 +338,20 @@ export class UserEntityService implements OnModuleInit {
});
}
const isDetailed = opts.schema !== 'UserLite';
const meId = me ? me.id : null;
const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin')
const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
.where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC')
.getMany() : [];
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const mastoapi = !opts.detail ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
const followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@ -370,17 +363,16 @@ export class UserEntityService implements OnModuleInit {
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
const unreadAnnouncements = isMe && opts.detail ?
const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null;
const unreadAnnouncements = isMe && isDetailed ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
createdAt: this.idService.parse(announcement.id).date.toISOString(),
...announcement,
})) : null;
const checkHost = user.host == null ? this.config.host : user.host;
const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
const packed = {
id: user.id,
@ -425,7 +417,7 @@ export class UserEntityService implements OnModuleInit {
displayOrder: r.displayOrder,
}))) : undefined,
...(opts.detail ? {
...(isDetailed ? {
url: profile!.url,
uri: user.uri,
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
@ -453,7 +445,7 @@ export class UserEntityService implements OnModuleInit {
}),
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
publicReactions: profile!.publicReactions,
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
@ -480,7 +472,7 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
...(opts.detail && isMe ? {
...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
backgroundId: user.backgroundId,
@ -554,19 +546,19 @@ export class UserEntityService implements OnModuleInit {
notify: relation.following?.notify ?? 'none',
withReplies: relation.following?.withReplies ?? false,
} : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
} as Promiseable<Packed<S>>;
return await awaitAll(packed);
}
public packMany<D extends boolean = false>(
public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
users: (MiUser['id'] | MiUser)[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
detail?: D,
schema?: S,
includeSecrets?: boolean,
},
): Promise<IsUserDetailed<D>[]> {
): Promise<Packed<S>[]> {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
}

View file

@ -6,7 +6,7 @@
// structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') {
@ -14,7 +14,7 @@ export function deepClone<T extends Cloneable>(x: T): T {
if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>;
for (const [k, v] of Object.entries(x)) {
obj[k] = deepClone(v);
obj[k] = v === undefined ? undefined : deepClone(v);
}
return obj as T;
} else {

View file

@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
import { packedPageSchema } from '@/models/json-schema/page.js';
import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@ -37,7 +37,7 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso
import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
import { packedRoleLiteSchema, packedRoleSchema, packedRolePoliciesSchema } from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
@ -67,6 +67,7 @@ export const refs = {
Hashtag: packedHashtagSchema,
InviteCode: packedInviteCodeSchema,
Page: packedPageSchema,
PageBlock: packedPageBlockSchema,
Channel: packedChannelSchema,
QueueCount: packedQueueCountSchema,
Antenna: packedAntennaSchema,
@ -79,12 +80,16 @@ export const refs = {
Signin: packedSigninSchema,
RoleLite: packedRoleLiteSchema,
Role: packedRoleSchema,
RolePolicies: packedRolePoliciesSchema,
ReversiGameLite: packedReversiGameLiteSchema,
ReversiGameDetailed: packedReversiGameDetailedSchema,
};
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
export type KeyOf<x extends keyof typeof refs> = PropertiesToUnion<typeof refs[x]>;
type PropertiesToUnion<p extends Schema> = p['properties'] extends NonNullable<Obj> ? keyof p['properties'] : never;
type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any';
type StringDefToType<T extends TypeStringef> =
T extends 'null' ? null :
@ -114,6 +119,7 @@ export interface Schema extends OfSchema {
readonly example?: any;
readonly format?: string;
readonly ref?: keyof typeof refs;
readonly selfRef?: boolean;
readonly enum?: ReadonlyArray<string | null>;
readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null;
readonly maxLength?: number;

View file

@ -38,7 +38,7 @@ export class MiAnnouncement {
length: 256, nullable: false,
default: 'info',
})
public icon: string;
public icon: 'info' | 'warning' | 'error' | 'success';
// normal ... お知らせページ掲載
// banner ... お知らせページ掲載 + バナー表示
@ -47,7 +47,7 @@ export class MiAnnouncement {
length: 256, nullable: false,
default: 'normal',
})
public display: string;
public display: 'normal' | 'banner' | 'dialog';
@Column('boolean', {
default: false,

View file

@ -37,10 +37,12 @@ export const packedAnnouncementSchema = {
icon: {
type: 'string',
optional: false, nullable: false,
enum: ['info', 'warning', 'error', 'success'],
},
display: {
type: 'string',
optional: false, nullable: false,
enum: ['dialog', 'normal', 'banner'],
},
needConfirmationToRead: {
type: 'boolean',

View file

@ -25,7 +25,7 @@ export const packedBlockingSchema = {
blockee: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'UserDetailedNotMe',
},
},
} as const;

View file

@ -30,12 +30,12 @@ export const packedFollowingSchema = {
followee: {
type: 'object',
optional: true, nullable: false,
ref: 'UserDetailed',
ref: 'UserDetailedNotMe',
},
follower: {
type: 'object',
optional: true, nullable: false,
ref: 'UserDetailed',
ref: 'UserDetailedNotMe',
},
},
} as const;

View file

@ -30,7 +30,7 @@ export const packedMutingSchema = {
mutee: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'UserDetailedNotMe',
},
},
} as const;

View file

@ -69,6 +69,7 @@ export const packedNoteSchema = {
visibility: {
type: 'string',
optional: false, nullable: false,
enum: ['public', 'home', 'followers', 'specified'],
},
mentions: {
type: 'array',
@ -117,6 +118,48 @@ export const packedNoteSchema = {
poll: {
type: 'object',
optional: true, nullable: true,
properties: {
expiresAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
multiple: {
type: 'boolean',
optional: false, nullable: false,
},
choices: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
isVoted: {
type: 'boolean',
optional: false, nullable: false,
},
text: {
type: 'string',
optional: false, nullable: false,
},
votes: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
},
},
emojis: {
type: 'object',
optional: true, nullable: false,
additionalProperties: {
anyOf: [{
type: 'string',
}],
},
},
channelId: {
type: 'string',
@ -162,9 +205,23 @@ export const packedNoteSchema = {
type: 'string',
optional: false, nullable: true,
},
reactionEmojis: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
anyOf: [{
type: 'string',
}],
},
},
reactions: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
anyOf: [{
type: 'number',
}],
},
},
renoteCount: {
type: 'number',
@ -196,7 +253,7 @@ export const packedNoteSchema = {
},
myReaction: {
type: 'object',
type: 'string',
optional: true, nullable: true,
},
},

View file

@ -5,7 +5,7 @@
import { notificationTypes } from '@/types.js';
export const packedNotificationSchema = {
const baseSchema = {
type: 'object',
properties: {
id: {
@ -23,68 +23,368 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: true, nullable: true,
},
userId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: true, nullable: true,
},
reaction: {
type: 'string',
optional: true, nullable: true,
},
achievement: {
type: 'string',
optional: true, nullable: false,
},
body: {
type: 'string',
optional: true, nullable: true,
},
header: {
type: 'string',
optional: true, nullable: true,
},
icon: {
type: 'string',
optional: true, nullable: true,
},
reactions: {
type: 'array',
optional: true, nullable: true,
items: {
type: 'object',
properties: {
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
reaction: {
type: 'string',
optional: false, nullable: false,
},
},
required: ['user', 'reaction'],
},
} as const;
export const packedNotificationSchema = {
type: 'object',
oneOf: [{
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['note'],
},
},
users: {
type: 'array',
optional: true, nullable: true,
items: {
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['mention'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['reply'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['renote'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['quote'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['reaction'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
reaction: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['pollEnded'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['follow'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['receiveFollowRequest'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['followRequestAccepted'],
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['roleAssigned'],
},
role: {
type: 'object',
ref: 'Role',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['achievementEarned'],
},
achievement: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['app'],
},
body: {
type: 'string',
optional: false, nullable: false,
},
header: {
type: 'string',
optional: false, nullable: false,
},
icon: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['reaction:grouped'],
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
reactions: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
properties: {
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
reaction: {
type: 'string',
optional: false, nullable: false,
},
},
required: ['user', 'reaction'],
},
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['renote:grouped'],
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
users: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['test'],
},
},
}],
} as const;

View file

@ -3,6 +3,108 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
const blockBaseSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
type: {
type: 'string',
optional: false, nullable: false,
},
},
} as const;
const textBlockSchema = {
type: 'object',
properties: {
...blockBaseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['text'],
},
text: {
type: 'string',
optional: false, nullable: false,
},
},
} as const;
const sectionBlockSchema = {
type: 'object',
properties: {
...blockBaseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['section'],
},
title: {
type: 'string',
optional: false, nullable: false,
},
children: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'PageBlock',
selfRef: true,
},
},
},
} as const;
const imageBlockSchema = {
type: 'object',
properties: {
...blockBaseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['image'],
},
fileId: {
type: 'string',
optional: false, nullable: true,
},
},
} as const;
const noteBlockSchema = {
type: 'object',
properties: {
...blockBaseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['note'],
},
detailed: {
type: 'boolean',
optional: false, nullable: false,
},
note: {
type: 'string',
optional: false, nullable: true,
},
},
} as const;
export const packedPageBlockSchema = {
type: 'object',
oneOf: [
textBlockSchema,
sectionBlockSchema,
imageBlockSchema,
noteBlockSchema,
],
} as const;
export const packedPageSchema = {
type: 'object',
properties: {
@ -38,6 +140,7 @@ export const packedPageSchema = {
items: {
type: 'object',
optional: false, nullable: false,
ref: 'PageBlock',
},
},
variables: {

View file

@ -25,7 +25,7 @@ export const packedRenoteMutingSchema = {
mutee: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'UserDetailedNotMe',
},
},
} as const;

View file

@ -1,26 +1,103 @@
const rolePolicyValue = {
export const packedRolePoliciesSchema = {
type: 'object',
optional: false, nullable: false,
properties: {
value: {
oneOf: [
{
type: 'integer',
optional: false, nullable: false,
},
{
type: 'boolean',
optional: false, nullable: false,
},
],
gtlAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
priority: {
ltlAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
canPublicNote: {
type: 'boolean',
optional: false, nullable: false,
},
canInvite: {
type: 'boolean',
optional: false, nullable: false,
},
inviteLimit: {
type: 'integer',
optional: false, nullable: false,
},
useDefault: {
inviteLimitCycle: {
type: 'integer',
optional: false, nullable: false,
},
inviteExpirationTime: {
type: 'integer',
optional: false, nullable: false,
},
canManageCustomEmojis: {
type: 'boolean',
optional: false, nullable: false,
},
canManageAvatarDecorations: {
type: 'boolean',
optional: false, nullable: false,
},
canSearchNotes: {
type: 'boolean',
optional: false, nullable: false,
},
canUseTranslator: {
type: 'boolean',
optional: false, nullable: false,
},
canHideAds: {
type: 'boolean',
optional: false, nullable: false,
},
driveCapacityMb: {
type: 'integer',
optional: false, nullable: false,
},
alwaysMarkNsfw: {
type: 'boolean',
optional: false, nullable: false,
},
pinLimit: {
type: 'integer',
optional: false, nullable: false,
},
antennaLimit: {
type: 'integer',
optional: false, nullable: false,
},
wordMuteLimit: {
type: 'integer',
optional: false, nullable: false,
},
webhookLimit: {
type: 'integer',
optional: false, nullable: false,
},
clipLimit: {
type: 'integer',
optional: false, nullable: false,
},
noteEachClipsLimit: {
type: 'integer',
optional: false, nullable: false,
},
userListLimit: {
type: 'integer',
optional: false, nullable: false,
},
userEachUserListsLimit: {
type: 'integer',
optional: false, nullable: false,
},
rateLimitFactor: {
type: 'integer',
optional: false, nullable: false,
},
avatarDecorationLimit: {
type: 'integer',
optional: false, nullable: false,
},
},
} as const;
@ -121,31 +198,28 @@ export const packedRoleSchema = {
policies: {
type: 'object',
optional: false, nullable: false,
properties: {
pinLimit: rolePolicyValue,
canInvite: rolePolicyValue,
clipLimit: rolePolicyValue,
canHideAds: rolePolicyValue,
inviteLimit: rolePolicyValue,
antennaLimit: rolePolicyValue,
gtlAvailable: rolePolicyValue,
ltlAvailable: rolePolicyValue,
webhookLimit: rolePolicyValue,
canPublicNote: rolePolicyValue,
userListLimit: rolePolicyValue,
wordMuteLimit: rolePolicyValue,
alwaysMarkNsfw: rolePolicyValue,
canSearchNotes: rolePolicyValue,
driveCapacityMb: rolePolicyValue,
rateLimitFactor: rolePolicyValue,
inviteLimitCycle: rolePolicyValue,
noteEachClipsLimit: rolePolicyValue,
inviteExpirationTime: rolePolicyValue,
canManageCustomEmojis: rolePolicyValue,
userEachUserListsLimit: rolePolicyValue,
canManageAvatarDecorations: rolePolicyValue,
canUseTranslator: rolePolicyValue,
avatarDecorationLimit: rolePolicyValue,
additionalProperties: {
anyOf: [{
type: 'object',
properties: {
value: {
oneOf: [
{
type: 'integer',
},
{
type: 'boolean',
},
],
},
priority: {
type: 'integer',
},
useDefault: {
type: 'boolean',
},
},
}],
},
},
usersCount: {

View file

@ -628,104 +628,7 @@ export const packedMeDetailedOnlySchema = {
policies: {
type: 'object',
nullable: false, optional: false,
properties: {
gtlAvailable: {
type: 'boolean',
nullable: false, optional: false,
},
ltlAvailable: {
type: 'boolean',
nullable: false, optional: false,
},
canPublicNote: {
type: 'boolean',
nullable: false, optional: false,
},
canInvite: {
type: 'boolean',
nullable: false, optional: false,
},
inviteLimit: {
type: 'number',
nullable: false, optional: false,
},
inviteLimitCycle: {
type: 'number',
nullable: false, optional: false,
},
inviteExpirationTime: {
type: 'number',
nullable: false, optional: false,
},
canManageCustomEmojis: {
type: 'boolean',
nullable: false, optional: false,
},
canManageAvatarDecorations: {
type: 'boolean',
nullable: false, optional: false,
},
canSearchNotes: {
type: 'boolean',
nullable: false, optional: false,
},
canUseTranslator: {
type: 'boolean',
nullable: false, optional: false,
},
canHideAds: {
type: 'boolean',
nullable: false, optional: false,
},
driveCapacityMb: {
type: 'number',
nullable: false, optional: false,
},
alwaysMarkNsfw: {
type: 'boolean',
nullable: false, optional: false,
},
pinLimit: {
type: 'number',
nullable: false, optional: false,
},
antennaLimit: {
type: 'number',
nullable: false, optional: false,
},
wordMuteLimit: {
type: 'number',
nullable: false, optional: false,
},
webhookLimit: {
type: 'number',
nullable: false, optional: false,
},
clipLimit: {
type: 'number',
nullable: false, optional: false,
},
noteEachClipsLimit: {
type: 'number',
nullable: false, optional: false,
},
userListLimit: {
type: 'number',
nullable: false, optional: false,
},
userEachUserListsLimit: {
type: 'number',
nullable: false, optional: false,
},
rateLimitFactor: {
type: 'number',
nullable: false, optional: false,
},
avatarDecorationLimit: {
type: 'number',
nullable: false, optional: false,
},
},
ref: 'RolePolicies',
},
//#region secrets
email: {
@ -820,13 +723,5 @@ export const packedUserSchema = {
type: 'object',
ref: 'UserDetailed',
},
{
type: 'object',
ref: 'UserDetailedNotMe',
},
{
type: 'object',
ref: 'MeDetailed',
},
],
} as const;

View file

@ -295,9 +295,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
}, {
...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
autorun: false,
concurrency: this.config.relashionshipJobConcurrency ?? 16,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
max: this.config.relashionshipJobPerSec ?? 64,
max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
});

View file

@ -206,7 +206,7 @@ export class ServerService implements OnApplicationShutdown {
});
this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));

View file

@ -157,7 +157,7 @@ export class ApiServerService {
return {
ok: true,
token: token.token,
user: await this.userEntityService.pack(token.userId, null, { detail: true }),
user: await this.userEntityService.pack(token.userId, null, { schema: 'UserDetailedNotMe' }),
};
} else {
return {

View file

@ -260,7 +260,7 @@ export class SignupApiService {
});
const res = await this.userEntityService.pack(account, account, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
});

View file

@ -4,8 +4,7 @@
*/
import { permissions } from 'misskey-js';
import type { Schema } from '@/misc/json-schema.js';
import { RolePolicies } from '@/core/RoleService.js';
import type { KeyOf, Schema } from '@/misc/json-schema.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
@ -802,7 +801,7 @@ interface IEndpointMetaBase {
*/
readonly requireAdmin?: boolean;
readonly requireRolePolicy?: keyof RolePolicies;
readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
/**
*

View file

@ -62,17 +62,17 @@ export const meta = {
reporter: {
type: 'object',
nullable: false, optional: false,
ref: 'User',
ref: 'UserDetailedNotMe',
},
targetUser: {
type: 'object',
nullable: false, optional: false,
ref: 'User',
ref: 'UserDetailedNotMe',
},
assignee: {
type: 'object',
nullable: true, optional: true,
ref: 'User',
ref: 'UserDetailedNotMe',
},
},
},

View file

@ -11,6 +11,7 @@ import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { Packed } from '@/misc/json-schema.js';
export const meta = {
tags: ['admin'],
@ -18,7 +19,7 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
ref: 'User',
ref: 'MeDetailed',
properties: {
token: {
type: 'string',
@ -60,11 +61,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
const res = await this.userEntityService.pack(account, account, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
});
}) as Packed<'MeDetailed'> & { token: string };
(res as any).token = secret;
res.token = secret;
return res;
});

View file

@ -27,7 +27,7 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
ref: 'User',
ref: 'UserDetailedNotMe',
},
} as const;
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
const res = await this.userEntityService.pack(profile.user!, null, {
detail: true,
schema: 'UserDetailedNotMe',
});
return res;

View file

@ -40,7 +40,7 @@ export const meta = {
},
required: ['id', 'createdAt', 'user'],
},
}
},
} as const;
export const paramDef = {
@ -92,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});

View file

@ -50,7 +50,7 @@ export const meta = {
user: {
type: 'object',
optional: false, nullable: false,
ref: 'UserDetailed',
ref: 'UserDetailedNotMe',
},
},
},

View file

@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.getMany();
return await this.userEntityService.packMany(users, me, { detail: true });
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});
}
}

View file

@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (user != null) {
return {
type: 'User',
object: await this.userEntityService.pack(user, me, { detail: true }),
object: await this.userEntityService.pack(user, me, { schema: 'UserDetailedNotMe' }),
};
} else if (note != null) {
try {

View file

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return {
accessToken: accessToken.token,
user: await this.userEntityService.pack(session.userId, null, {
detail: true,
schema: 'UserDetailedNotMe',
}),
};
});

View file

@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.userBlockingService.block(blocker, blockee);
return await this.userEntityService.pack(blockee.id, blocker, {
detail: true,
schema: 'UserDetailedNotMe',
});
});
}

View file

@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.userBlockingService.unblock(blocker, blockee);
return await this.userEntityService.pack(blockee.id, blocker, {
detail: true,
schema: 'UserDetailedNotMe',
});
});
}

View file

@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
relations: ['user'],
});
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
const users = await this.userEntityService.packMany(records.map(r => r.user!), null);
return records.map(r => ({
id: r.id,

View file

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
return await this.userEntityService.packMany(users, me, { detail: true });
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailedNotMe' });
});
}
}

View file

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.limit(ps.limit).getMany();
return await this.userEntityService.packMany(users, me, { detail: true });
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});
}
}

View file

@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
}
return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, {
detail: true,
return await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
schema: 'MeDetailed',
includeSecrets: isSecure,
userProfile,
});

View file

@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));

View file

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));

View file

@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));
});

View file

@ -99,7 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));

View file

@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));
});

View file

@ -69,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
}));

View file

@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
return await this.userEntityService.pack<true, true>(me.id, me, {
detail: true,
return await this.userEntityService.pack(me.id, me, {
schema: 'MeDetailed',
});
});
}

View file

@ -51,8 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
return await this.userEntityService.pack<true, true>(me.id, me, {
detail: true,
return await this.userEntityService.pack(me.id, me, {
schema: 'MeDetailed',
});
});
}

View file

@ -44,7 +44,7 @@ export const meta = {
res: {
type: 'object',
ref: 'UserDetailed',
ref: 'MeDetailed',
},
} as const;
@ -107,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
const iObj = await this.userEntityService.pack(me.id, me, {
detail: true,
schema: 'MeDetailed',
includeSecrets: true,
});

View file

@ -478,8 +478,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
verifiedLinks: [],
});
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
detail: true,
const iObj = await this.userEntityService.pack(user.id, user, {
schema: 'MeDetailed',
includeSecrets: isSecure,
});

View file

@ -314,6 +314,11 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
policies: {
type: 'object',
optional: false, nullable: false,
ref: 'RolePolicies',
},
},
},
} as const;

View file

@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
var: ps.var,
userId: me.id,
user: await this.userEntityService.pack(me.id, { id: page.userId }, {
detail: true,
schema: 'UserDetailed',
}),
});
});

View file

@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(),
})));
return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { detail: true });
return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { schema: 'UserDetailed' });
});
}
}

View file

@ -14,6 +14,32 @@ export const meta = {
requireCredential: false,
res: {
type: 'array',
items: {
type: 'object',
properties: {
createdAt: {
type: 'string',
format: 'date-time',
},
users: {
type: 'number',
},
data: {
type: 'object',
additionalProperties: {
anyOf: [{
type: 'number',
}],
},
},
},
required: [
'createdAt',
'users',
'data',
],
},
},
allowGet: true,

View file

@ -33,11 +33,11 @@ export const meta = {
properties: {
id: {
type: 'string',
format: 'misskey:id'
format: 'misskey:id',
},
user: {
type: 'object',
ref: 'User'
ref: 'UserDetailed',
},
},
required: ['id', 'user'],
@ -94,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
})));
});
}

View file

@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.getMany();
return await this.userEntityService.packMany(users, me, { detail: true });
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});
}
}

View file

@ -122,7 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
user: await this.userEntityService.pack(user, me, { detail: true }),
user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
weight: repliedUsers[user] / peak,
})));

View file

@ -46,7 +46,7 @@ export const meta = {
},
user: {
type: 'object',
ref: 'User',
ref: 'UserLite',
},
withReplies: {
type: 'boolean',

View file

@ -9,6 +9,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -34,6 +37,11 @@ export const meta = {
code: 'REACTIONS_NOT_PUBLIC',
id: '673a7dd2-6924-1093-e0c0-e68456ceae5c',
},
isRemoteUser: {
message: 'Currently unavailable to display reactions of remote users. See https://github.com/misskey-dev/misskey/issues/12964',
code: 'IS_REMOTE_USER',
id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805',
},
},
} as const;
@ -59,14 +67,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private noteReactionEntityService: NoteReactionEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
if (!iAmModerator) {
const user = await this.cacheService.findUserById(ps.userId);
if (this.userEntityService.isRemoteUser(user)) {
throw new ApiError(meta.errors.isRemoteUser);
}
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic);
}
}
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),

View file

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.limit(ps.limit).offset(ps.offset).getMany();
return await this.userEntityService.packMany(users, me, { detail: true });
return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
});
}
}

View file

@ -131,7 +131,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.getMany();
}
return await this.userEntityService.packMany(users, me, { detail: !!ps.detail });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}
}

View file

@ -141,7 +141,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
return await this.userEntityService.packMany(users, me, { detail: ps.detail });
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
});
}
}

View file

@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
detail: true,
schema: 'UserDetailed',
})));
} else {
// Lookup user
@ -146,7 +146,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
return await this.userEntityService.pack(user, me, {
detail: true,
schema: 'UserDetailed',
});
}
});

View file

@ -6,9 +6,9 @@
import type { Config } from '@/config.js';
import endpoints, { IEndpoint } from '../endpoints.js';
import { errors as basicErrors } from './errors.js';
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
import { getSchemas, convertSchemaToOpenApiSchema } from './schemas.js';
export function genOpenapiSpec(config: Config) {
export function genOpenapiSpec(config: Config, includeSelfRef = false) {
const spec = {
openapi: '3.1.0',
@ -30,7 +30,7 @@ export function genOpenapiSpec(config: Config) {
paths: {} as any,
components: {
schemas: schemas,
schemas: getSchemas(includeSelfRef),
securitySchemes: {
bearerAuth: {
@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) {
}
}
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {};
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res', includeSelfRef) : {};
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
}
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };
const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param', false) };
if (endpoint.meta.requireFile) {
schema.properties = {

View file

@ -6,10 +6,10 @@
import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') {
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, nullable, ref, ...res }: any = schema;
const { optional, nullable, ref, selfRef, ...res }: any = schema;
if (schema.type === 'object' && schema.properties) {
if (type === 'res') {
@ -21,20 +21,20 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're
}
for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type);
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type, includeSelfRef);
}
}
if (schema.type === 'array' && schema.items) {
res.items = convertSchemaToOpenApiSchema(schema.items, type);
res.items = convertSchemaToOpenApiSchema(schema.items, type, includeSelfRef);
}
for (const o of ['anyOf', 'oneOf', 'allOf'] as const) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type));
if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type, includeSelfRef));
}
if (type === 'res' && schema.ref) {
if (type === 'res' && schema.ref && (!schema.selfRef || includeSelfRef)) {
const $ref = `#/components/schemas/${schema.ref}`;
if (schema.nullable || schema.optional) {
res.allOf = [{ $ref }];
@ -54,35 +54,37 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're
return res;
}
export const schemas = {
Error: {
type: 'object',
properties: {
error: {
type: 'object',
description: 'An error object.',
properties: {
code: {
type: 'string',
description: 'An error code. Unique within the endpoint.',
},
message: {
type: 'string',
description: 'An error message.',
},
id: {
type: 'string',
format: 'uuid',
description: 'An error ID. This ID is static.',
export function getSchemas(includeSelfRef: boolean) {
return {
Error: {
type: 'object',
properties: {
error: {
type: 'object',
description: 'An error object.',
properties: {
code: {
type: 'string',
description: 'An error code. Unique within the endpoint.',
},
message: {
type: 'string',
description: 'An error message.',
},
id: {
type: 'string',
format: 'uuid',
description: 'An error ID. This ID is static.',
},
},
required: ['code', 'id', 'message'],
},
required: ['code', 'id', 'message'],
},
required: ['error'],
},
required: ['error'],
},
...Object.fromEntries(
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]),
),
};
...Object.fromEntries(
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res', includeSelfRef)]),
),
};
}