refactor(backend): User関連のスキーマ/型の指定を強くする (#12808)

* refactor(backend): User関連のスキーマ/型の指定を強くする

* refactor(backend): `pack()`の引数にスキーマを指定するように

* chore: fix ci

* fix: 変更漏れ

* fix ci

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
zyoshoka 2024-01-31 15:45:35 +09:00 committed by GitHub
parent 8aea3603a6
commit 2db5b61616
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 141 additions and 169 deletions

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<'UserDetailed' | 'UserLite'>;
unfollow: Packed<'UserDetailed'>;
meUpdated: Packed<'UserDetailed'>;
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'>;
};
@ -140,8 +140,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

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

@ -324,9 +324,7 @@ export class NoteEntityService implements OnModuleInit {
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
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();
@ -303,33 +295,34 @@ 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);
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
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 followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@ -341,15 +334,15 @@ 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 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,
@ -385,7 +378,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,
@ -443,7 +436,7 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
...(opts.detail && isMe ? {
...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
isModerator: isModerator,
@ -515,19 +508,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)));
}
}