Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -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
});