Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
import { In, Not } from "typeorm";
|
||||
import Ajv from "ajv";
|
||||
import { User } from "../entities/user.js";
|
||||
import config from "../../config/index.js";
|
||||
import { awaitAll } from "../../prelude/await-all.js";
|
||||
import { populateEmojis } from "../../misc/populate-emojis.js";
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from "../../const.js";
|
||||
import { Cache } from "../../misc/cache.js";
|
||||
import { db } from "../../db/postgre.js";
|
||||
import { isActor, getApId } from "../../remote/activitypub/type.js";
|
||||
import DbResolver from "../../remote/activitypub/db-resolver.js";
|
||||
import Resolver from "../../remote/activitypub/resolver.js";
|
||||
import { createPerson } from "../../remote/activitypub/models/person.js";
|
||||
import { AnnouncementReads, Announcements, Blockings, ChannelFollowings, DriveFiles, Followings, FollowRequests, Instances, MessagingMessages, Mutings, RenoteMutings, Notes, NoteUnreads, Notifications, Pages, Plans, UserGroupJoinings, UserNotePinings, UserPlans, UserProfiles, UserSecurityKeys } from "../index.js";
|
||||
import AsyncLock from "async-lock";
|
||||
const userInstanceCache = new Cache("userInstance", 60 * 60 * 3);
|
||||
function isMissingRelationError(err) {
|
||||
const error = err;
|
||||
return error.code === "42P01" || error.driverError?.code === "42P01";
|
||||
}
|
||||
const ajv = new Ajv();
|
||||
const localUsernameSchema = {
|
||||
type: "string",
|
||||
pattern: /^\w{1,20}$/.toString().slice(1, -1)
|
||||
};
|
||||
const passwordSchema = {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
};
|
||||
const nameSchema = {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
};
|
||||
const descriptionSchema = {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 2048
|
||||
};
|
||||
const locationSchema = {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
};
|
||||
const birthdaySchema = {
|
||||
type: "string",
|
||||
pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1)
|
||||
};
|
||||
/**
|
||||
* Returns true if the user is local.
|
||||
*
|
||||
* @param user The user to check.
|
||||
* @returns True if the user is local.
|
||||
*/ function isLocalUser(user) {
|
||||
return user.host == null;
|
||||
}
|
||||
/**
|
||||
* Returns true if the user is remote.
|
||||
*
|
||||
* @param user The user to check.
|
||||
* @returns True if the user is remote.
|
||||
*/ function isRemoteUser(user) {
|
||||
return !isLocalUser(user);
|
||||
}
|
||||
export const UserRepository = db.getRepository(User).extend({
|
||||
localUsernameSchema,
|
||||
passwordSchema,
|
||||
nameSchema,
|
||||
descriptionSchema,
|
||||
locationSchema,
|
||||
birthdaySchema,
|
||||
//#region Validators
|
||||
validateLocalUsername: ajv.compile(localUsernameSchema),
|
||||
validatePassword: ajv.compile(passwordSchema),
|
||||
validateName: ajv.compile(nameSchema),
|
||||
validateDescription: ajv.compile(descriptionSchema),
|
||||
validateLocation: ajv.compile(locationSchema),
|
||||
validateBirthday: ajv.compile(birthdaySchema),
|
||||
//#endregion
|
||||
async getRelation (me, target) {
|
||||
return awaitAll({
|
||||
id: target,
|
||||
isFollowing: Followings.count({
|
||||
where: {
|
||||
followerId: me,
|
||||
followeeId: target
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
isFollowed: Followings.count({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
hasPendingFollowRequestFromYou: FollowRequests.count({
|
||||
where: {
|
||||
followerId: me,
|
||||
followeeId: target
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
hasPendingFollowRequestToYou: FollowRequests.count({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
isBlocking: Blockings.count({
|
||||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
isBlocked: Blockings.count({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
isMuted: Mutings.count({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0),
|
||||
isRenoteMuted: RenoteMutings.count({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target
|
||||
},
|
||||
take: 1
|
||||
}).then((n)=>n > 0)
|
||||
});
|
||||
},
|
||||
async getHasUnreadMessagingMessage (userId) {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId
|
||||
});
|
||||
const joinings = await UserGroupJoinings.findBy({
|
||||
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([
|
||||
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);
|
||||
},
|
||||
async getHasUnreadAnnouncement (userId) {
|
||||
const reads = await AnnouncementReads.findBy({
|
||||
userId: userId
|
||||
});
|
||||
const count = await Announcements.countBy(reads.length > 0 ? {
|
||||
id: Not(In(reads.map((read)=>read.announcementId)))
|
||||
} : {});
|
||||
return count > 0;
|
||||
},
|
||||
async userFromURI (uri) {
|
||||
try {
|
||||
const dbResolver = new DbResolver();
|
||||
let local = await dbResolver.getUserFromApId(uri);
|
||||
if (local) {
|
||||
return local;
|
||||
}
|
||||
// fetching Object once from remote
|
||||
const resolver = new Resolver();
|
||||
const object = await resolver.resolve(uri);
|
||||
// /@user If a URI other than the id is specified,
|
||||
// the URI is determined here
|
||||
if (uri !== object.id) {
|
||||
local = await dbResolver.getUserFromApId(object.id);
|
||||
if (local != null) return local;
|
||||
}
|
||||
return isActor(object) ? await createPerson(getApId(object)) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async getHasUnreadAntenna (userId) {
|
||||
// try {
|
||||
// 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;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
return false; // TODO
|
||||
},
|
||||
async getHasUnreadChannel (userId) {
|
||||
const channels = await ChannelFollowings.findBy({
|
||||
followerId: userId
|
||||
});
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
|
||||
userId: userId,
|
||||
noteChannelId: In(channels.map((x)=>x.followeeId))
|
||||
}) : null;
|
||||
return unread != null;
|
||||
},
|
||||
async getHasUnreadNotification (userId) {
|
||||
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;
|
||||
},
|
||||
async getHasPendingReceivedFollowRequest (userId) {
|
||||
const count = await FollowRequests.countBy({
|
||||
followeeId: userId
|
||||
});
|
||||
return count > 0;
|
||||
},
|
||||
getOnlineStatus (user) {
|
||||
if (user.hideOnlineStatus) return "unknown";
|
||||
if (user.lastActiveDate == null) return "unknown";
|
||||
const elapsed = Date.now() - user.lastActiveDate.getTime();
|
||||
return elapsed < USER_ONLINE_THRESHOLD ? "online" : elapsed < USER_ACTIVE_THRESHOLD ? "active" : "offline";
|
||||
},
|
||||
async getAvatarUrl (user) {
|
||||
if (user.avatar) {
|
||||
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else if (user.avatarId) {
|
||||
if (user.avatarUrl) return DriveFiles.getFinalUrl(user.avatarUrl);
|
||||
const avatar = await DriveFiles.findOneByOrFail({
|
||||
id: user.avatarId
|
||||
});
|
||||
return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user.id);
|
||||
}
|
||||
},
|
||||
getAvatarUrlSync (user) {
|
||||
if (user.avatarId && user.avatarUrl) {
|
||||
return DriveFiles.getFinalUrl(user.avatarUrl);
|
||||
} else if (user.avatar) {
|
||||
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user.id);
|
||||
}
|
||||
},
|
||||
getIdenticonUrl (userId) {
|
||||
return `${config.url}/identicon/${userId}`;
|
||||
},
|
||||
getFreshPackedUserCache () {
|
||||
return {
|
||||
locks: new AsyncLock(),
|
||||
results: []
|
||||
};
|
||||
},
|
||||
async getRandomFollower (targetId) {
|
||||
return await this.createQueryBuilder("u").select(`u.id`).leftJoinAndSelect("following", "f", `f."followerId" = u.id`).where(`f."followeeId" = :id`, {
|
||||
id: targetId
|
||||
}).getOne();
|
||||
},
|
||||
async packCached (src, cache, me, options) {
|
||||
const id = typeof src === "object" ? src.id : src;
|
||||
return cache.locks.acquire(id, async ()=>{
|
||||
const result = cache.results.find((p)=>p.id === id);
|
||||
if (result) return result;
|
||||
return this.pack(src, me, options).then((result)=>{
|
||||
cache.results.push(result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
},
|
||||
async pack (src, me, options) {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
includeSecrets: false,
|
||||
isPrivateMode: false
|
||||
}, options);
|
||||
let user;
|
||||
if (typeof src === "object") {
|
||||
user = src;
|
||||
} else {
|
||||
user = await this.findOneOrFail({
|
||||
where: {
|
||||
id: src
|
||||
}
|
||||
});
|
||||
}
|
||||
const meId = me ? me.id : null;
|
||||
const isMe = meId === user.id;
|
||||
const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
|
||||
const pins = opts.detail ? await UserNotePinings.createQueryBuilder("pin").where("pin.userId = :userId", {
|
||||
userId: user.id
|
||||
}).innerJoinAndSelect("pin.note", "note").orderBy("pin.id", "DESC").getMany() : [];
|
||||
const profile = opts.detail ? await UserProfiles.findOneByOrFail({
|
||||
userId: user.id
|
||||
}) : null;
|
||||
const followingCount = profile == null ? null : profile.ffVisibility === "public" || isMe ? user.followingCount : profile.ffVisibility === "followers" && relation && relation.isFollowing ? user.followingCount : null;
|
||||
const followersCount = profile == null ? null : profile.ffVisibility === "public" || isMe ? user.followersCount : profile.ffVisibility === "followers" && relation && relation.isFollowing ? user.followersCount : null;
|
||||
const falsy = opts.detail ? false : undefined;
|
||||
if (opts.isPrivateMode) {
|
||||
const packed = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
...opts.detail ? {
|
||||
twoFactorEnabled: profile.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile.usePasswordLessLogin,
|
||||
securityKeys: profile.twoFactorEnabled ? UserSecurityKeys.countBy({
|
||||
userId: user.id
|
||||
}).then((result)=>result >= 1) : false
|
||||
} : {}
|
||||
};
|
||||
return await awaitAll(packed);
|
||||
}
|
||||
const packed = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: this.getAvatarUrlSync(user),
|
||||
avatarBlurhash: user.avatarId ? user.avatarBlurhash ?? user.avatar?.blurhash ?? null : null,
|
||||
avatarColor: null,
|
||||
isAdmin: user.isAdmin || falsy,
|
||||
isModerator: user.isModerator || falsy,
|
||||
isVerified: user.isVerified || falsy,
|
||||
minorBadges: user.minorBadges ?? [],
|
||||
plans: UserPlans.find({
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
relations: [
|
||||
"plan"
|
||||
],
|
||||
order: {
|
||||
createdAt: "ASC"
|
||||
}
|
||||
}).then((joins)=>Plans.packMany(joins.map((join)=>join.plan).filter((plan)=>plan != null))).catch((err)=>{
|
||||
if (isMissingRelationError(err)) return [];
|
||||
throw err;
|
||||
}),
|
||||
isBot: user.isBot || falsy,
|
||||
isLocked: user.isLocked,
|
||||
isCat: user.isCat || falsy,
|
||||
speakAsCat: user.speakAsCat || falsy,
|
||||
instance: user.host ? userInstanceCache.fetch(user.host, ()=>Instances.findOneBy({
|
||||
host: user.host
|
||||
}), (v)=>v != null).then((instance)=>instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
iconUrl: instance.iconUrl,
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor
|
||||
} : undefined) : undefined,
|
||||
emojis: populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
driveCapacityOverrideMb: user.driveCapacityOverrideMb,
|
||||
canBite: user.canBite,
|
||||
...opts.detail ? {
|
||||
url: profile.url,
|
||||
uri: user.uri,
|
||||
movedToUri: user.movedToUri ? await this.userFromURI(user.movedToUri) : null,
|
||||
alsoKnownAs: user.alsoKnownAs,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||
bannerUrl: user.bannerId ? DriveFiles.getFinalUrlMaybe(user.bannerUrl) ?? (user.banner ? DriveFiles.getPublicUrl(user.banner, false) : null) : null,
|
||||
bannerBlurhash: user.bannerId ? user.bannerBlurhash ?? user.banner?.blurhash ?? null : null,
|
||||
bannerColor: null,
|
||||
isSilenced: user.isSilenced || falsy,
|
||||
isSuspended: user.isSuspended || falsy,
|
||||
description: profile.description,
|
||||
location: profile.location,
|
||||
birthday: profile.birthday,
|
||||
lang: profile.lang,
|
||||
fields: profile.fields,
|
||||
followersCount: followersCount || 0,
|
||||
followingCount: followingCount || 0,
|
||||
notesCount: user.notesCount,
|
||||
pinnedNoteIds: pins.map((pin)=>pin.noteId),
|
||||
pinnedNotes: Notes.packMany(pins.map((pin)=>pin.note), me, {
|
||||
detail: true
|
||||
}),
|
||||
pinnedPageId: profile.pinnedPageId,
|
||||
pinnedPage: profile.pinnedPageId ? Pages.pack(profile.pinnedPageId, me) : null,
|
||||
publicReactions: profile.publicReactions,
|
||||
allowCalls: profile.allowCalls,
|
||||
symbolFileId: profile.symbolFileId,
|
||||
ffVisibility: profile.ffVisibility,
|
||||
twoFactorEnabled: profile.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile.usePasswordLessLogin,
|
||||
securityKeys: profile.twoFactorEnabled ? UserSecurityKeys.countBy({
|
||||
userId: user.id
|
||||
}).then((result)=>result >= 1) : false,
|
||||
pronouns: profile.pronouns
|
||||
} : {},
|
||||
...opts.detail && isMe ? {
|
||||
avatarId: user.avatarId,
|
||||
bannerId: user.bannerId,
|
||||
injectFeaturedNote: profile.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
alwaysMarkNsfw: profile.alwaysMarkNsfw,
|
||||
carefulBot: profile.carefulBot,
|
||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||
noCrawle: profile.noCrawle,
|
||||
preventAiLearning: profile.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
isDeleted: user.isDeleted,
|
||||
hideOnlineStatus: user.hideOnlineStatus,
|
||||
hasUnreadSpecifiedNotes: NoteUnreads.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
isSpecified: true
|
||||
},
|
||||
take: 1
|
||||
}).then((count)=>count > 0),
|
||||
hasUnreadMentions: NoteUnreads.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
isMentioned: true
|
||||
},
|
||||
take: 1
|
||||
}).then((count)=>count > 0),
|
||||
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
hasUnreadChannel: this.getHasUnreadChannel(user.id),
|
||||
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
integrations: profile.integrations,
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
emailNotificationTypes: profile.emailNotificationTypes
|
||||
} : {},
|
||||
...opts.includeSecrets ? {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
securityKeysList: profile.twoFactorEnabled ? UserSecurityKeys.find({
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
lastUsed: true
|
||||
}
|
||||
}) : []
|
||||
} : {},
|
||||
...relation ? {
|
||||
isFollowing: relation.isFollowing,
|
||||
isFollowed: relation.isFollowed,
|
||||
hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
|
||||
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
|
||||
isBlocking: relation.isBlocking,
|
||||
isBlocked: relation.isBlocked,
|
||||
isMuted: relation.isMuted,
|
||||
isRenoteMuted: relation.isRenoteMuted
|
||||
} : {}
|
||||
};
|
||||
return await awaitAll(packed);
|
||||
},
|
||||
packMany (users, me, options, cache) {
|
||||
return Promise.all(users.map((u)=>this.packCached(u, cache ?? this.getFreshPackedUserCache(), me, options)));
|
||||
},
|
||||
isLocalUser,
|
||||
isRemoteUser
|
||||
});
|
||||
Reference in New Issue
Block a user