misskey/src/models/repositories/user.ts
2019-06-15 00:07:41 +09:00

393 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import $ from 'cafy';
import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..';
import { ensure } from '../../prelude/ensure';
import config from '../../config';
import { SchemaType, bool, types } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all';
export type PackedUser = SchemaType<typeof packedUserSchema>;
@EntityRepository(User)
export class UserRepository extends Repository<User> {
public async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOne({
followerId: me,
followeeId: target
}),
Followings.findOne({
followerId: target,
followeeId: me
}),
FollowRequests.findOne({
followerId: me,
followeeId: target
}),
FollowRequests.findOne({
followerId: target,
followeeId: me
}),
Blockings.findOne({
blockerId: me,
blockeeId: target
}),
Blockings.findOne({
blockerId: target,
blockeeId: me
}),
Mutings.findOne({
muterId: me,
muteeId: target
})
]);
return {
id: target,
isFollowing: following1 != null,
hasPendingFollowRequestFromYou: followReq1 != null,
hasPendingFollowRequestToYou: followReq2 != null,
isFollowed: following2 != null,
isBlocking: toBlocking != null,
isBlocked: fromBlocked != null,
isMuted: mute != null
};
}
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
const joinings = await UserGroupJoinings.find({ userId: userId });
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null)));
const [withUser, withGroups] = await Promise.all([
// TODO: ミュートを考慮
MessagingMessages.count({
where: {
recipientId: userId,
isRead: false
},
take: 1
}).then(count => count > 0),
groupQs
]);
return withUser || withGroups.some(x => x);
}
public async pack(
src: User['id'] | User,
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
): Promise<PackedUser> {
const opts = Object.assign({
detail: false,
includeSecrets: false
}, options);
const user = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const meId = me ? typeof me === 'string' ? me : me.id : null;
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await UserNotePinings.find({
where: { userId: user.id },
order: { id: 'DESC' }
}) : [];
const profile = opts.detail ? await UserProfiles.findOne(user.id).then(ensure) : null;
const falsy = opts.detail ? false : undefined;
const packed = {
id: user.id,
name: user.name,
username: user.username,
host: user.host,
avatarUrl: user.avatarUrl ? user.avatarUrl : config.url + '/avatar/' + user.id,
avatarColor: user.avatarColor,
isAdmin: user.isAdmin || falsy,
isBot: user.isBot || falsy,
isCat: user.isCat || falsy,
// カスタム絵文字添付
emojis: user.emojis.length > 0 ? Emojis.find({
where: {
name: In(user.emojis),
host: user.host
},
select: ['name', 'host', 'url', 'aliases']
}) : [],
...(opts.includeHasUnreadNotes ? {
hasUnreadSpecifiedNotes: NoteUnreads.count({
where: { userId: user.id, isSpecified: true },
take: 1
}).then(count => count > 0),
hasUnreadMentions: NoteUnreads.count({
where: { userId: user.id },
take: 1
}).then(count => count > 0),
} : {}),
...(opts.detail ? {
url: profile!.url,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
bannerUrl: user.bannerUrl,
bannerColor: user.bannerColor,
isLocked: user.isLocked,
isModerator: user.isModerator || falsy,
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: pins.map(pin => pin.noteId),
pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
detail: true
}),
twoFactorEnabled: profile!.twoFactorEnabled,
twitter: profile!.twitter ? {
id: profile!.twitterUserId,
screenName: profile!.twitterScreenName
} : null,
github: profile!.github ? {
id: profile!.githubId,
login: profile!.githubLogin
} : null,
discord: profile!.discord ? {
id: profile!.discordId,
username: profile!.discordUsername,
discriminator: profile!.discordDiscriminator
} : null,
} : {}),
...(opts.detail && meId === user.id ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
autoWatch: profile!.autoWatch,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed,
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: Notifications.count({
where: {
notifieeId: user.id,
isRead: false
},
take: 1
}).then(count => count > 0),
pendingReceivedFollowRequestsCount: FollowRequests.count({
followeeId: user.id
}),
} : {}),
...(opts.includeSecrets ? {
clientData: profile!.clientData,
email: profile!.email,
emailVerified: profile!.emailVerified,
} : {}),
...(relation ? {
isFollowing: relation.isFollowing,
isFollowed: relation.isFollowed,
hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
} : {})
};
return await awaitAll(packed);
}
public packMany(
users: (User['id'] | User)[],
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean,
includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
}
) {
return Promise.all(users.map(u => this.pack(u, me, options)));
}
public isLocalUser(user: User): user is ILocalUser {
return user.host == null;
}
public isRemoteUser(user: User): user is IRemoteUser {
return !this.isLocalUser(user);
}
//#region Validators
public validateLocalUsername = $.str.match(/^\w{1,20}$/);
public validateRemoteUsername = $.str.match(/^\w([\w-]*\w)?$/);
public validatePassword = $.str.min(1);
public validateName = $.str.min(1).max(50);
public validateDescription = $.str.min(1).max(500);
public validateLocation = $.str.min(1).max(50);
public validateBirthday = $.str.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/);
//#endregion
}
export const packedUserSchema = {
type: types.object,
nullable: bool.false, optional: bool.false,
properties: {
id: {
type: types.string,
nullable: bool.false, optional: bool.false,
format: 'id',
description: 'The unique identifier for this User.',
example: 'xxxxxxxxxx',
},
username: {
type: types.string,
nullable: bool.false, optional: bool.false,
description: 'The screen name, handle, or alias that this user identifies themselves with.',
example: 'ai'
},
name: {
type: types.string,
nullable: bool.true, optional: bool.false,
description: 'The name of the user, as theyve defined it.',
example: '藍'
},
url: {
type: types.string,
format: 'url',
nullable: bool.true, optional: bool.true,
},
avatarUrl: {
type: types.string,
format: 'url',
nullable: bool.true, optional: bool.false,
},
avatarColor: {
type: types.any,
nullable: bool.true, optional: bool.false,
},
bannerUrl: {
type: types.string,
format: 'url',
nullable: bool.true, optional: bool.true,
},
bannerColor: {
type: types.any,
nullable: bool.true, optional: bool.true,
},
emojis: {
type: types.any,
nullable: bool.true, optional: bool.false,
},
host: {
type: types.string,
nullable: bool.true, optional: bool.false,
example: 'misskey.example.com'
},
description: {
type: types.string,
nullable: bool.true, optional: bool.true,
description: 'The user-defined UTF-8 string describing their account.',
example: 'Hi masters, I am Ai!'
},
birthday: {
type: types.string,
nullable: bool.true, optional: bool.true,
example: '2018-03-12'
},
createdAt: {
type: types.string,
nullable: bool.false, optional: bool.true,
format: 'date-time',
description: 'The date that the user account was created on Misskey.'
},
updatedAt: {
type: types.string,
nullable: bool.true, optional: bool.true,
format: 'date-time',
},
location: {
type: types.string,
nullable: bool.true, optional: bool.true,
},
followersCount: {
type: types.number,
nullable: bool.false, optional: bool.true,
description: 'The number of followers this account currently has.'
},
followingCount: {
type: types.number,
nullable: bool.false, optional: bool.true,
description: 'The number of users this account is following.'
},
notesCount: {
type: types.number,
nullable: bool.false, optional: bool.true,
description: 'The number of Notes (including renotes) issued by the user.'
},
isBot: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
description: 'Whether this account is a bot.'
},
pinnedNoteIds: {
type: types.array,
nullable: bool.false, optional: bool.true,
items: {
type: types.string,
nullable: bool.false, optional: bool.false,
format: 'id',
}
},
pinnedNotes: {
type: types.array,
nullable: bool.false, optional: bool.true,
items: {
type: types.object,
nullable: bool.false, optional: bool.false,
ref: 'Note'
}
},
isCat: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
description: 'Whether this account is a cat.'
},
isAdmin: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
description: 'Whether this account is the admin.'
},
isModerator: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
description: 'Whether this account is a moderator.'
},
isLocked: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
},
hasUnreadSpecifiedNotes: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
},
hasUnreadMentions: {
type: types.boolean,
nullable: bool.false, optional: bool.true,
},
},
};