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,33 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { AbuseUserReport } from "../entities/abuse-user-report.js";
import { awaitAll } from "../../prelude/await-all.js";
export const AbuseUserReportRepository = db.getRepository(AbuseUserReport).extend({
async pack (src) {
const report = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: report.id,
createdAt: report.createdAt.toISOString(),
comment: report.comment,
resolved: report.resolved,
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
reporter: Users.pack(report.reporter || report.reporterId, null, {
detail: true
}),
targetUser: Users.pack(report.targetUser || report.targetUserId, null, {
detail: true
}),
assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, {
detail: true
}) : null,
forwarded: report.forwarded
});
},
packMany (reports) {
return Promise.all(reports.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,30 @@
import { db } from "../../db/postgre.js";
import { Antenna } from "../entities/antenna.js";
import { UserGroupJoinings } from "../index.js";
export const AntennaRepository = db.getRepository(Antenna).extend({
async pack (src) {
const antenna = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOneBy({
id: antenna.userGroupJoiningId
}) : null;
return {
id: antenna.id,
createdAt: antenna.createdAt.toISOString(),
name: antenna.name,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
src: antenna.src,
userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
users: antenna.users,
instances: antenna.instances,
caseSensitive: antenna.caseSensitive,
notify: antenna.notify,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
hasUnreadNote: false
};
}
});
@@ -0,0 +1,30 @@
import { db } from "../../db/postgre.js";
import { App } from "../entities/app.js";
import { AccessTokens } from "../index.js";
export const AppRepository = db.getRepository(App).extend({
async pack (src, me, options) {
const opts = Object.assign({
detail: false,
includeSecret: false,
includeProfileImageIds: false
}, options);
const app = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: app.id,
name: app.name,
callbackUrl: app.callbackUrl,
permission: app.permission,
...opts.includeSecret ? {
secret: app.secret
} : {},
...me ? {
isAuthorized: await AccessTokens.countBy({
appId: app.id,
userId: me.id
}).then((count)=>count > 0)
} : {}
};
}
});
@@ -0,0 +1,16 @@
import { db } from "../../db/postgre.js";
import { Apps } from "../index.js";
import { AuthSession } from "../entities/auth-session.js";
import { awaitAll } from "../../prelude/await-all.js";
export const AuthSessionRepository = db.getRepository(AuthSession).extend({
async pack (src, me) {
const session = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: session.id,
app: Apps.pack(session.appId, me),
token: session.token
});
}
});
@@ -0,0 +1,106 @@
import { db } from "../../db/postgre.js";
import { Bite } from "../entities/bite.js";
import { Bites, Notes, Users } from "../index.js";
import { awaitAll } from "../../prelude/await-all.js";
import config from "../../config/index.js";
export const BiteRespository = db.getRepository(Bite).extend({
targetType (bite) {
if (bite.targetUserId) return "user";
if (bite.targetBiteId) return "bite";
return "note";
},
async pack (src, me) {
const bite = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: bite.id,
user: Users.pack(bite.user ?? bite.userId, me, {
detail: false
}),
targetType: BiteRespository.targetType(bite),
target: this.packTarget(bite, me),
replied: bite.replied
});
},
async packTarget (bite, me) {
switch(BiteRespository.targetType(bite)){
case "user":
return await Users.pack(bite.targetUser ?? bite.targetUserId, me, {
detail: false
});
case "bite":
return await this.pack(bite.targetBite ?? bite.targetBiteId, me);
case "note":
return await Notes.pack(bite.targetNote ?? bite.targetNoteId, me, {
detail: false
});
}
},
async targetUri (bite) {
switch(BiteRespository.targetType(bite)){
case "user":
{
bite.targetUser = bite.targetUser ?? await Users.findOneOrFail({
where: {
id: bite.targetUserId
}
});
return bite.targetUser.uri ?? `${config.url}/users/${bite.targetUserId}`;
}
case "bite":
{
bite.targetBite = bite.targetBite ?? await Bites.findOneOrFail({
where: {
id: bite.targetBiteId
}
});
return bite.targetBite.uri ?? `${config.url}/bites/${bite.targetBiteId}`;
}
case "note":
{
bite.targetNote = bite.targetNote ?? await Notes.findOneOrFail({
where: {
id: bite.targetNoteId
}
});
return bite.targetNote.uri ?? `${config.url}/notes/${bite.targetBiteId}`;
}
}
},
async targetUserUri (bite) {
switch(BiteRespository.targetType(bite)){
case "user":
if (!bite.targetUser) bite.targetUser = await Users.findOneByOrFail({
id: bite.targetUserId
});
return bite.targetUser.uri ?? `${config.url}/users/${bite.targetUserId}`;
case "bite":
bite.targetBite = bite.targetBite ?? await Bites.findOneOrFail({
where: {
id: bite.targetBiteId
},
relations: [
"user"
]
});
bite.targetBite.user = bite.targetBite.user ?? await Users.findOneByOrFail({
id: bite.targetBite.userId
});
return bite.targetBite.user.uri ?? `${config.url}/users/${bite.targetBite.userId}`;
case "note":
bite.targetNote = bite.targetNote ?? await Notes.findOneOrFail({
where: {
id: bite.targetNoteId
},
relations: [
"user"
]
});
bite.targetNote.user = bite.targetNote.user ?? await Users.findOneByOrFail({
id: bite.targetNote.userId
});
return bite.targetNote.user.uri ?? `${config.url}/users/${bite.targetNote.userId}`;
}
}
});
@@ -0,0 +1,22 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { Blocking } from "../entities/blocking.js";
import { awaitAll } from "../../prelude/await-all.js";
export const BlockingRepository = db.getRepository(Blocking).extend({
async pack (src, me) {
const blocking = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: blocking.id,
createdAt: blocking.createdAt.toISOString(),
blockeeId: blocking.blockeeId,
blockee: Users.pack(blocking.blockeeId, me, {
detail: true
})
});
},
packMany (blockings, me) {
return Promise.all(blockings.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,22 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { CallBlocking } from "../entities/call-blocking.js";
import { awaitAll } from "../../prelude/await-all.js";
export const CallBlockingRepository = db.getRepository(CallBlocking).extend({
async pack (src, me) {
const blocking = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: blocking.id,
createdAt: blocking.createdAt.toISOString(),
blockeeId: blocking.blockeeId,
blockee: Users.pack(blocking.blockeeId, me, {
detail: true
})
});
},
packMany (blockings, me) {
return Promise.all(blockings.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,37 @@
import { db } from "../../db/postgre.js";
import { Channel } from "../entities/channel.js";
import { DriveFiles, ChannelFollowings, NoteUnreads } from "../index.js";
export const ChannelRepository = db.getRepository(Channel).extend({
async pack (src, me) {
const channel = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const meId = me ? me.id : null;
const banner = channel.bannerId ? await DriveFiles.findOneBy({
id: channel.bannerId
}) : null;
const hasUnreadNote = meId ? await NoteUnreads.findOneBy({
noteChannelId: channel.id,
userId: meId
}) != null : undefined;
const following = meId ? await ChannelFollowings.findOneBy({
followerId: meId,
followeeId: channel.id
}) : null;
return {
id: channel.id,
createdAt: channel.createdAt.toISOString(),
lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
name: channel.name,
description: channel.description,
userId: channel.userId,
bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
usersCount: channel.usersCount,
notesCount: channel.notesCount,
...me ? {
isFollowing: following != null,
hasUnreadNote
} : {}
};
}
});
@@ -0,0 +1,23 @@
import { db } from "../../db/postgre.js";
import { Clip } from "../entities/clip.js";
import { Users } from "../index.js";
import { awaitAll } from "../../prelude/await-all.js";
export const ClipRepository = db.getRepository(Clip).extend({
async pack (src) {
const clip = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: clip.id,
createdAt: clip.createdAt.toISOString(),
userId: clip.userId,
user: Users.pack(clip.user || clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic
});
},
packMany (clips) {
return Promise.all(clips.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,181 @@
import { db } from "../../db/postgre.js";
import { DriveFile } from "../entities/drive-file.js";
import { toPuny } from "../../misc/convert-host.js";
import { awaitAll } from "../../prelude/await-all.js";
import config from "../../config/index.js";
import { appendQuery, query } from "../../prelude/url.js";
import { DriveFolders, Users } from "../index.js";
import { deepClone } from "../../misc/clone.js";
import { fetchMetaSync } from "../../misc/fetch-meta.js";
export const DriveFileRepository = db.getRepository(DriveFile).extend({
validateFileName (name) {
return name.trim().length > 0 && name.length <= 200 && name.indexOf("\\") === -1 && name.indexOf("/") === -1 && name.indexOf("..") === -1;
},
getPublicProperties (file) {
if (file.properties.orientation != null) {
const properties = deepClone(file.properties);
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [
properties.height,
properties.width
];
}
properties.orientation = undefined;
return properties;
}
return file.properties;
},
isImage (file) {
return !!file.type && [
"image/png",
"image/apng",
"image/gif",
"image/jpeg",
"image/webp",
"image/svg+xml",
"image/avif"
].includes(file.type);
},
isStreamableMedia (file) {
return file.type.startsWith("video/") || file.type.startsWith("audio/");
},
withStreamQuery (file, url) {
if (url == null || file.allowDownload || !this.isStreamableMedia(file)) return url;
return appendQuery(url, query({
stream: "1"
}));
},
getPublicUrl (file, thumbnail = false) {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
return appendQuery(config.mediaProxy, query({
url: file.uri,
thumbnail: thumbnail ? "1" : undefined
}));
}
if (file.isLink && config.proxyRemoteFiles) {
const url = this.getDatabasePrefetchUrl(file, thumbnail);
if (url != null) return `${config.url}/proxy/${encodeURIComponent(new URL(url).pathname)}?${query({
url: url
})}`;
}
const url = thumbnail ? file.thumbnailUrl || (this.isImage(file) ? file.webpublicUrl || file.url : null) : file.webpublicUrl || file.url;
return thumbnail ? url : this.withStreamQuery(file, url);
},
getDatabasePrefetchUrl (file, thumbnail = false) {
return thumbnail ? file.thumbnailUrl ?? file.webpublicUrl ?? file.url : file.webpublicUrl ?? file.url;
},
getFinalUrl (url) {
if (!config.proxyRemoteFiles) return url;
if (!url.startsWith('https://') && !url.startsWith('http://')) return url;
if (url.startsWith(`${config.url}/files`)) return url;
if (url.startsWith(`${config.url}/static-assets`)) return url;
if (url.startsWith(`${config.url}/identicon`)) return url;
if (url.startsWith(`${config.url}/avatar`)) return url;
const meta = fetchMetaSync();
const baseUrl = meta ? meta.objectStorageBaseUrl ?? `${meta.objectStorageUseSSL ? "https" : "http"}://${meta.objectStorageEndpoint}${meta.objectStoragePort ? `:${meta.objectStoragePort}` : ""}/${meta.objectStorageBucket}` : null;
if (baseUrl !== null && url.startsWith(baseUrl)) return url;
return `${config.url}/proxy/${encodeURIComponent(new URL(url).pathname)}?${query({
url: url
})}`;
},
getFinalUrlMaybe (url) {
if (url == null) return null;
return this.getFinalUrl(url);
},
async calcDriveUsageOf (user) {
const id = typeof user === "object" ? user.id : user;
const { sum } = await this.createQueryBuilder("file").where("file.userId = :id", {
id: id
}).andWhere("file.isLink = FALSE").andWhere("file.isDatabase = FALSE").select("SUM(file.size)", "sum").getRawOne();
return parseInt(sum, 10) || 0;
},
async calcDatabaseUsageOf (user) {
const id = typeof user === "object" ? user.id : user;
const { sum } = await this.createQueryBuilder("file").where("file.userId = :id", {
id: id
}).andWhere("file.isLink = FALSE").andWhere("file.isDatabase = TRUE").select("SUM(file.size)", "sum").getRawOne();
return parseInt(sum, 10) || 0;
},
async calcDriveUsageOfHost (host) {
const { sum } = await this.createQueryBuilder("file").where("file.userHost = :host", {
host: toPuny(host)
}).andWhere("file.isLink = FALSE").andWhere("file.isDatabase = FALSE").select("SUM(file.size)", "sum").getRawOne();
return parseInt(sum, 10) || 0;
},
async calcDriveUsageOfLocal () {
const { sum } = await this.createQueryBuilder("file").where("file.userHost IS NULL").andWhere("file.isLink = FALSE").andWhere("file.isDatabase = FALSE").select("SUM(file.size)", "sum").getRawOne();
return parseInt(sum, 10) || 0;
},
async calcDriveUsageOfRemote () {
const { sum } = await this.createQueryBuilder("file").where("file.userHost IS NOT NULL").andWhere("file.isLink = FALSE").andWhere("file.isDatabase = FALSE").select("SUM(file.size)", "sum").getRawOne();
return parseInt(sum, 10) || 0;
},
async pack (src, options) {
const opts = Object.assign({
detail: false,
self: false
}, options);
const file = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
allowDownload: file.allowDownload,
isDatabase: file.isDatabase,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true
}) : null,
userId: opts.withUser ? file.userId : null,
user: opts.withUser && file.userId ? Users.pack(file.userId) : null
});
},
async packNullable (src, options) {
const opts = Object.assign({
detail: false,
self: false
}, options);
const file = typeof src === "object" ? src : await this.findOneBy({
id: src
});
if (file == null) return null;
return await awaitAll({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
allowDownload: file.allowDownload,
isDatabase: file.isDatabase,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true
}) : null,
userId: opts.withUser ? file.userId : null,
user: opts.withUser && file.userId ? Users.pack(file.userId) : null
});
},
async packMany (files, options) {
const items = await Promise.all(files.map((f)=>this.packNullable(f, options)));
return items.filter((x)=>x != null);
}
});
@@ -0,0 +1,33 @@
import { db } from "../../db/postgre.js";
import { DriveFolders, DriveFiles } from "../index.js";
import { DriveFolder } from "../entities/drive-folder.js";
import { awaitAll } from "../../prelude/await-all.js";
export const DriveFolderRepository = db.getRepository(DriveFolder).extend({
async pack (src, options) {
const opts = Object.assign({
detail: false
}, options);
const folder = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: folder.id,
createdAt: folder.createdAt.toISOString(),
name: folder.name,
parentId: folder.parentId,
...opts.detail ? {
foldersCount: DriveFolders.countBy({
parentId: folder.id
}),
filesCount: DriveFiles.countBy({
folderId: folder.id
}),
...folder.parentId ? {
parent: this.pack(folder.parentId, {
detail: true
})
} : {}
} : {}
});
}
});
@@ -0,0 +1,26 @@
import { db } from "../../db/postgre.js";
import { Emoji } from "../entities/emoji.js";
export const EmojiRepository = db.getRepository(Emoji).extend({
async pack (src) {
const emoji = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のため
url: emoji.publicUrl || emoji.originalUrl,
license: emoji.license,
glyph: emoji.glyph,
glyphUrl: emoji.glyph ? emoji.originalUrl : null,
width: emoji.width,
height: emoji.height
};
},
packMany (emojis) {
return Promise.all(emojis.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,15 @@
import { db } from "../../db/postgre.js";
import { FollowRequest } from "../entities/follow-request.js";
import { Users } from "../index.js";
export const FollowRequestRepository = db.getRepository(FollowRequest).extend({
async pack (src, me) {
const request = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: request.id,
follower: await Users.pack(request.followerId, me),
followee: await Users.pack(request.followeeId, me)
};
}
});
@@ -0,0 +1,39 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { Following } from "../entities/following.js";
import { awaitAll } from "../../prelude/await-all.js";
export const FollowingRepository = db.getRepository(Following).extend({
isLocalFollower (following) {
return following.followerHost == null;
},
isRemoteFollower (following) {
return following.followerHost != null;
},
isLocalFollowee (following) {
return following.followeeHost == null;
},
isRemoteFollowee (following) {
return following.followeeHost != null;
},
async pack (src, me, opts) {
const following = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
if (opts == null) opts = {};
return await awaitAll({
id: following.id,
createdAt: following.createdAt.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
detail: true
}) : undefined,
follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
detail: true
}) : undefined
});
},
packMany (followings, me, opts) {
return Promise.all(followings.map((x)=>this.pack(x, me, opts)));
}
});
@@ -0,0 +1,17 @@
import { db } from "../../db/postgre.js";
import { GalleryLike } from "../entities/gallery-like.js";
import { GalleryPosts } from "../index.js";
export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({
async pack (src, me) {
const like = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: like.id,
post: await GalleryPosts.pack(like.post || like.postId, me)
};
},
packMany (likes, me) {
return Promise.all(likes.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,33 @@
import { db } from "../../db/postgre.js";
import { GalleryPost } from "../entities/gallery-post.js";
import { Users, DriveFiles, GalleryLikes } from "../index.js";
import { awaitAll } from "../../prelude/await-all.js";
export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
async pack (src, me) {
const meId = me ? me.id : null;
const post = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: post.id,
createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
user: Users.pack(post.user || post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
files: DriveFiles.packMany(post.fileIds),
tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive,
likedCount: post.likedCount,
isLiked: meId ? await GalleryLikes.findOneBy({
postId: post.id,
userId: meId
}).then((x)=>x != null) : undefined
});
},
packMany (posts, me) {
return Promise.all(posts.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,18 @@
import { db } from "../../db/postgre.js";
import { Hashtag } from "../entities/hashtag.js";
export const HashtagRepository = db.getRepository(Hashtag).extend({
async pack (src) {
return {
tag: src.name,
mentionedUsersCount: src.mentionedUsersCount,
mentionedLocalUsersCount: src.mentionedLocalUsersCount,
mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
attachedUsersCount: src.attachedUsersCount,
attachedLocalUsersCount: src.attachedLocalUsersCount,
attachedRemoteUsersCount: src.attachedRemoteUsersCount
};
},
packMany (hashtags) {
return Promise.all(hashtags.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,36 @@
import { db } from "../../db/postgre.js";
import { Instance } from "../entities/instance.js";
import { shouldBlockInstance, shouldSilenceInstance } from "../../misc/should-block-instance.js";
export const InstanceRepository = db.getRepository(Instance).extend({
async pack (instance, privileged = true) {
return {
id: instance.id,
caughtAt: instance.caughtAt.toISOString(),
host: instance.host,
usersCount: instance.usersCount,
notesCount: instance.notesCount,
followingCount: instance.followingCount,
followersCount: instance.followersCount,
latestRequestSentAt: instance.latestRequestSentAt ? instance.latestRequestSentAt.toISOString() : null,
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
isNotResponding: instance.isNotResponding,
isSuspended: privileged ? instance.isSuspended : false,
isBlocked: privileged ? await shouldBlockInstance(instance.host) : false,
isSilenced: privileged ? await shouldSilenceInstance(instance.host) : false,
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations,
name: instance.name,
description: instance.description,
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null
};
},
packMany (instances, privileged = true) {
return Promise.all(instances.map((x)=>this.pack(x, privileged)));
}
});
@@ -0,0 +1,29 @@
import { db } from "../../db/postgre.js";
import { MessagingMessage } from "../entities/messaging-message.js";
import { Users, DriveFiles, UserGroups } from "../index.js";
export const MessagingMessageRepository = db.getRepository(MessagingMessage).extend({
async pack (src, me, options) {
const opts = options || {
populateRecipient: true,
populateGroup: true
};
const message = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: message.id,
createdAt: message.createdAt.toISOString(),
text: message.text,
userId: message.userId,
user: await Users.pack(message.user || message.userId, me),
recipientId: message.recipientId,
recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
groupId: message.groupId,
group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
fileId: message.fileId,
file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
isRead: message.isRead,
reads: message.reads
};
}
});
@@ -0,0 +1,24 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { ModerationLog } from "../entities/moderation-log.js";
import { awaitAll } from "../../prelude/await-all.js";
export const ModerationLogRepository = db.getRepository(ModerationLog).extend({
async pack (src) {
const log = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: log.id,
createdAt: log.createdAt.toISOString(),
type: log.type,
info: log.info,
userId: log.userId,
user: Users.pack(log.user || log.userId, null, {
detail: true
})
});
},
packMany (reports) {
return Promise.all(reports.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,23 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { Muting } from "../entities/muting.js";
import { awaitAll } from "../../prelude/await-all.js";
export const MutingRepository = db.getRepository(Muting).extend({
async pack (src, me) {
const muting = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: muting.id,
createdAt: muting.createdAt.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
mutee: Users.pack(muting.muteeId, me, {
detail: true
})
});
},
packMany (mutings, me) {
return Promise.all(mutings.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,22 @@
import { db } from "../../db/postgre.js";
import { NoteFavorite } from "../entities/note-favorite.js";
import { Notes } from "../index.js";
export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({
async pack (src, me) {
const favorite = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: favorite.id,
createdAt: favorite.createdAt.toISOString(),
noteId: favorite.noteId,
// may throw error
note: await Notes.pack(favorite.note || favorite.noteId, me)
};
},
packMany (favorites, me) {
return Promise.allSettled(favorites.map((x)=>this.pack(x, me))).then((promises)=>promises.flatMap((result)=>result.status === "fulfilled" ? [
result.value
] : []));
}
});
@@ -0,0 +1,31 @@
import { db } from "../../db/postgre.js";
import { NoteReaction } from "../entities/note-reaction.js";
import { Notes, Users } from "../index.js";
import { convertLegacyReaction } from "../../misc/reaction-lib.js";
export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
async pack (src, me, options) {
const opts = Object.assign({
withNote: false
}, options);
const reaction = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: reaction.id,
createdAt: reaction.createdAt.toISOString(),
user: await Users.pack(reaction.user ?? reaction.userId, me),
type: convertLegacyReaction(reaction.reaction),
...opts.withNote ? {
// may throw error
note: await Notes.pack(reaction.note ?? reaction.noteId, me)
} : {}
};
},
async packMany (src, me, options) {
const reactions = await Promise.allSettled(src.map((reaction)=>this.pack(reaction, me, options)));
// filter out rejected promises, only keep fulfilled values
return reactions.flatMap((result)=>result.status === "fulfilled" ? [
result.value
] : []);
}
});
@@ -0,0 +1,312 @@
import { In } from "typeorm";
import * as mfm from "mfm-js";
import { Note } from "../entities/note.js";
import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels, Notes, Blockings, UserGroups } from "../index.js";
import { nyaize } from "../../misc/nyaize.js";
import { awaitAll } from "../../prelude/await-all.js";
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from "../../misc/reaction-lib.js";
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from "../../misc/populate-emojis.js";
import { db } from "../../db/postgre.js";
import { IdentifiableError } from "../../misc/identifiable-error.js";
import { isFiltered } from "../../misc/is-filtered.js";
export async function populatePoll(note, meId) {
const poll = await Polls.findOneByOrFail({
noteId: note.id
});
const choices = poll.choices.map((c)=>({
text: c,
votes: poll.votes[poll.choices.indexOf(c)],
isVoted: false
}));
if (meId) {
if (poll.multiple) {
const votes = await PollVotes.findBy({
userId: meId,
noteId: note.id
});
const myChoices = votes.map((v)=>v.choice);
for (const myChoice of myChoices){
choices[myChoice].isVoted = true;
}
} else {
const vote = await PollVotes.findOneBy({
userId: meId,
noteId: note.id
});
if (vote) {
choices[vote.choice].isVoted = true;
}
}
}
return {
multiple: poll.multiple,
expiresAt: poll.expiresAt,
choices
};
}
async function populateMyReaction(note, meId, _hint_) {
if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(note.id);
if (reaction) {
return convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
const reaction = await NoteReactions.findOneBy({
userId: meId,
noteId: note.id
});
if (reaction) {
return convertLegacyReaction(reaction.reaction);
}
return undefined;
}
async function populateIsRenoted(note, meId, _hint_) {
return _hint_?.myRenotes ? _hint_.myRenotes.get(note.id) ? true : undefined : Notes.exist({
where: {
renoteId: note.id,
userId: meId
}
}).then((res)=>res ? true : undefined);
}
export const NoteRepository = db.getRepository(Note).extend({
async isVisibleForMe (note, meId) {
if (meId != null && meId !== note.userId) {
const blocked = await Blockings.count({
where: [
{
blockeeId: meId,
blockerId: note.userId,
groupId: null
},
...note.groupId ? [
{
blockeeId: meId,
groupId: note.groupId
}
] : []
],
take: 1
});
if (blocked !== 0) {
return false;
}
const minorBadgeBlocked = await Users.createQueryBuilder("author").where("author.id = :authorId", {
authorId: note.userId
}).andWhere("'E' = ANY(author.\"minorBadges\")").andWhere(`EXISTS (` + `SELECT 1 FROM "user" viewer ` + `WHERE viewer.id = :meId ` + `AND viewer."isAdmin" = FALSE ` + `AND viewer."isModerator" = FALSE ` + `AND ('K' = ANY(viewer."minorBadges") OR 'T' = ANY(viewer."minorBadges"))` + `)`, {
meId
}).getCount();
if (minorBadgeBlocked !== 0) {
return false;
}
}
// This code must always be synchronized with the checks in generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === "specified") {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else {
// 指定されているかどうか
return note.visibleUserIds.some((id)=>meId === id);
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (note.visibility === "followers") {
if (meId == null) {
return false;
} else if (meId === note.userId) {
return true;
} else if (note.reply && meId === note.reply.userId) {
// 自分の投稿に対するリプライ
return true;
} else if (note.mentions?.some((id)=>meId === id)) {
// 自分へのメンション
return true;
} else {
// フォロワーかどうか
const [following, user] = await Promise.all([
Followings.count({
where: {
followeeId: note.userId,
followerId: meId
},
take: 1
}),
Users.findOneByOrFail({
id: meId
})
]);
/* If we know the following, everyhting is fine.
But if we do not know the following, it might be that both the
author of the note and the author of the like are remote users,
in which case we can never know the following. Instead we have
to assume that the users are following each other.
*/ return following > 0 || note.userHost != null && user.host != null;
}
}
return true;
},
async pack (src, me, options, userCache = Users.getFreshPackedUserCache()) {
const opts = Object.assign({
detail: true
}, options);
const meId = me ? me.id : null;
const note = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const host = note.userHost;
if (!opts.allowAdservice && !note._prId_ && note.tags.includes("adservice") && note.userId !== meId) {
throw new IdentifiableError("9725d0ce-ba28-4dde-95a7-2cbb2c15de24", "No such note.");
}
if (!await this.isVisibleForMe(note, meId)) {
throw new IdentifiableError("9725d0ce-ba28-4dde-95a7-2cbb2c15de24", "No such note.");
}
let text = note.text;
if (note.name && (note.url ?? note.uri)) {
text = `${note.name}\n${(note.text || "").trim()}\n\n${note.url ?? note.uri}`;
}
const channel = note.channelId ? note.channel ? note.channel : await Channels.findOneBy({
id: note.channelId
}) : null;
const reactionEmojiNames = Object.keys(note.reactions).filter((x)=>x?.startsWith(":")).map((x)=>decodeReaction(x).reaction).map((x)=>x.replace(/:/g, ""));
const noteEmoji = populateEmojis(note.emojis.concat(reactionEmojiNames), host);
const reactionEmoji = populateEmojis(reactionEmojiNames, host);
const packed = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
userId: note.userId,
user: Users.packCached(note.user ?? note.userId, userCache, me, {
detail: false
}),
groupId: note.groupId,
group: note.groupId ? UserGroups.pack(note.group ?? note.groupId) : null,
text: text,
cw: note.cw,
visibility: note.visibility,
localOnly: note.localOnly || undefined,
visibleUserIds: note.visibility === "specified" ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
viewCount: note.viewCount,
reactions: convertLegacyReactions(note.reactions),
reactionEmojis: reactionEmoji,
emojis: noteEmoji,
tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds,
files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId,
renoteId: note.renoteId,
channelId: note.channelId || undefined,
channel: channel ? {
id: channel.id,
name: channel.name
} : undefined,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
url: note.url || undefined,
updatedAt: note.updatedAt?.toISOString() || undefined,
poll: note.hasPoll ? populatePoll(note, meId) : undefined,
quoteAuthorization: note.quoteAuthorization || undefined,
canBite: false,
...meId ? {
myReaction: populateMyReaction(note, meId, options?._hint_),
isRenoted: populateIsRenoted(note, meId, options?._hint_),
isFiltered: isFiltered(note, me)
} : {},
...opts.detail ? {
reply: note.replyId ? this.tryPack(note.reply || note.replyId, me, {
detail: false,
_hint_: options?._hint_
}, userCache) : undefined,
renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, {
detail: true,
_hint_: options?._hint_
}, userCache) : undefined
} : {}
});
if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
const tokens = packed.text ? mfm.parse(packed.text) : [];
function nyaizeNode(node) {
if (node.type === "quote") return;
if (node.type === "text") node.props.text = nyaize(node.props.text);
if (node.children) {
for (const child of node.children){
nyaizeNode(child);
}
}
}
for (const node of tokens)nyaizeNode(node);
packed.text = mfm.toString(tokens);
}
if (me) {
if (packed.user.canBite === "anyone") {
packed.canBite = true;
} else if (packed.user.canBite === "followers") {
const isFollowing = await Followings.exist({
where: {
followerId: me.id,
followeeId: packed.userId
},
take: 1
});
packed.canBite = isFollowing;
} else {
packed.canBite = false;
}
}
if (note._prId_) {
packed._prId_ = note._prId_;
}
return packed;
},
async tryPack (src, me, options, userCache = Users.getFreshPackedUserCache()) {
try {
return await this.pack(src, me, options, userCache);
} catch {
return undefined;
}
},
async packMany (notes, me, options, userCache = Users.getFreshPackedUserCache()) {
if (notes.length === 0) return [];
const meId = me ? me.id : null;
const myReactionsMap = new Map();
const myRenotesMap = new Map();
if (meId) {
const renoteIds = notes.filter((n)=>n.renoteId != null).map((n)=>n.renoteId);
const targets = [
...notes.map((n)=>n.id),
...renoteIds
];
const myReactions = await NoteReactions.findBy({
userId: meId,
noteId: In(targets)
});
const myRenotes = await Notes.createQueryBuilder('note').select('note.renoteId').where('note.userId = :meId', {
meId
}).andWhere('note.renoteId IN (:...targets)', {
targets
}).andWhere('note.text IS NULL').andWhere('note.hasPoll = FALSE').andWhere(`note.fileIds = '{}'`).getMany();
for (const target of targets){
myReactionsMap.set(target, myReactions.find((reaction)=>reaction.noteId === target) || null);
myRenotesMap.set(target, !!myRenotes.find((p)=>p.renoteId == target));
}
}
await prefetchEmojis(aggregateNoteEmojis(notes));
const promises = await Promise.allSettled(notes.map((n)=>this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap,
myRenotes: myRenotesMap
}
}, userCache)));
// filter out rejected promises, only keep fulfilled values
return promises.flatMap((result)=>result.status === "fulfilled" ? [
result.value
] : []);
}
});
@@ -0,0 +1,128 @@
import { In } from "typeorm";
import { Notification } from "../entities/notification.js";
import { awaitAll } from "../../prelude/await-all.js";
import { aggregateNoteEmojis, prefetchEmojis } from "../../misc/populate-emojis.js";
import { db } from "../../db/postgre.js";
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions, Bites } from "../index.js";
export const NotificationRepository = db.getRepository(Notification).extend({
async pack (src, options) {
const notification = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({
id: notification.appAccessTokenId
}) : null;
return await awaitAll({
id: notification.id,
createdAt: notification.createdAt.toISOString(),
type: notification.type,
isRead: notification.isRead,
userId: notification.notifierId,
user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
...notification.type === "mention" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
})
} : {},
...notification.type === "reply" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
})
} : {},
...notification.type === "renote" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
})
} : {},
...notification.type === "quote" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
})
} : {},
...notification.type === "reaction" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
}),
reaction: notification.reaction
} : {},
...notification.type === "pollVote" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
}),
choice: notification.choice
} : {},
...notification.type === "pollEnded" ? {
note: Notes.pack(notification.note || notification.noteId, {
id: notification.notifieeId
}, {
detail: true,
_hint_: options._hintForEachNotes_
})
} : {},
...notification.type === "groupInvited" ? {
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId)
} : {},
...notification.type === "app" ? {
body: notification.customBody,
header: notification.customHeader || token?.name,
icon: notification.customIcon || token?.iconUrl
} : {},
...notification.type === "bite" ? {
bite: Bites.pack(notification.bite || notification.biteId, {
id: notification.notifieeId
})
} : {}
});
},
async packMany (notifications, meId) {
if (notifications.length === 0) return [];
const notes = notifications.filter((x)=>x.note != null).map((x)=>x.note);
const noteIds = notes.map((n)=>n.id);
const myReactionsMap = new Map();
const myRenotesMap = new Map();
const renoteIds = notes.filter((n)=>n.renoteId != null).map((n)=>n.renoteId);
const targets = [
...noteIds,
...renoteIds
];
const myReactions = await NoteReactions.findBy({
userId: meId,
noteId: In(targets)
});
const myRenotes = targets.length > 0 ? await Notes.createQueryBuilder('note').select('note.renoteId').where('note.userId = :meId', {
meId
}).andWhere('note.renoteId IN (:...targets)', {
targets
}).getMany() : [];
for (const target of targets){
myReactionsMap.set(target, myReactions.find((reaction)=>reaction.noteId === target) || null);
myRenotesMap.set(target, !!myRenotes.find((p)=>p.renoteId == target));
}
await prefetchEmojis(aggregateNoteEmojis(notes));
const results = await Promise.all(notifications.map((x)=>this.pack(x, {
_hintForEachNotes_: {
myReactions: myReactionsMap,
myRenotes: myRenotesMap
}
}).catch((e)=>null)));
return results.filter((x)=>x != null);
}
});
@@ -0,0 +1,17 @@
import { db } from "../../db/postgre.js";
import { PageLike } from "../entities/page-like.js";
import { Pages } from "../index.js";
export const PageLikeRepository = db.getRepository(PageLike).extend({
async pack (src, me) {
const like = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: like.id,
page: await Pages.pack(like.page || like.pageId, me)
};
},
packMany (likes, me) {
return Promise.all(likes.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,80 @@
import { db } from "../../db/postgre.js";
import { Page } from "../entities/page.js";
import { awaitAll } from "../../prelude/await-all.js";
import { Users, DriveFiles, PageLikes } from "../index.js";
export const PageRepository = db.getRepository(Page).extend({
async pack (src, me) {
const meId = me ? me.id : null;
const page = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const attachedFiles = [];
const collectFile = (xs)=>{
for (const x of xs){
if (x.type === "image") {
attachedFiles.push(DriveFiles.findOneBy({
id: x.fileId,
userId: page.userId
}));
}
if (x.children) {
collectFile(x.children);
}
}
};
collectFile(page.content);
// 後方互換性のため
let migrated = false;
const migrate = (xs)=>{
for (const x of xs){
if (x.type === "input") {
if (x.inputType === "text") {
x.type = "textInput";
}
if (x.inputType === "number") {
x.type = "numberInput";
if (x.default) x.default = parseInt(x.default, 10);
}
migrated = true;
}
if (x.children) {
migrate(x.children);
}
}
};
migrate(page.content);
if (migrated) {
this.update(page.id, {
content: page.content
});
}
return await awaitAll({
id: page.id,
createdAt: page.createdAt.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
user: Users.pack(page.user || page.userId, me),
content: page.content,
variables: page.variables,
title: page.title,
isPublic: page.isPublic,
name: page.name,
summary: page.summary,
hideTitleWhenPinned: page.hideTitleWhenPinned,
alignCenter: page.alignCenter,
font: page.font,
script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x)=>x != null)),
likedCount: page.likedCount,
isLiked: meId ? await PageLikes.findOneBy({
pageId: page.id,
userId: meId
}).then((x)=>x != null) : undefined
});
},
packMany (pages, me) {
return Promise.all(pages.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,22 @@
import { db } from "../../db/postgre.js";
import { Plan } from "../entities/plan.js";
export const PlanRepository = db.getRepository(Plan).extend({
pack (src) {
return Promise.resolve().then(async ()=>{
const plan = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: plan.id,
createdAt: plan.createdAt.toISOString(),
updatedAt: plan.updatedAt?.toISOString() ?? null,
name: plan.name,
icon: plan.icon,
description: plan.description
};
});
},
packMany (plans) {
return Promise.all(plans.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,3 @@
import { db } from "../../db/postgre.js";
import { Relay } from "../entities/relay.js";
export const RelayRepository = db.getRepository(Relay).extend({});
@@ -0,0 +1,22 @@
import { db } from "../../db/postgre.js";
import { RenoteMuting } from "../entities/renote-muting.js";
import { awaitAll } from "../../prelude/await-all.js";
import { Users } from "../index.js";
export const RenoteMutingRepository = db.getRepository(RenoteMuting).extend({
async pack (src, me) {
const muting = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: muting.id,
createdAt: muting.createdAt.toISOString(),
muteeId: muting.muteeId,
mutee: Users.pack(muting.muteeId, me, {
detail: true
})
});
},
packMany (mutings, me) {
return Promise.all(mutings.map((x)=>this.pack(x, me)));
}
});
@@ -0,0 +1,69 @@
import { db } from "../../db/postgre.js";
import { ReversiGame } from "../entities/reversi-game.js";
import { Users } from "../index.js";
import { genId } from "../../misc/gen-id.js";
function createdAtFromId(id) {
const time = parseInt(id.slice(0, 8), 36);
if (Number.isNaN(time)) return new Date().toISOString();
return new Date(time + 946684800000).toISOString();
}
function assertBw(bw) {
return bw === "1" || bw === "2" ? bw : "random";
}
export const ReversiGameRepository = db.getRepository(ReversiGame).extend({
async packDetail (src) {
const game = typeof src === "object" ? src : await this.findOneOrFail({
where: {
id: src
},
relations: {
user1: true,
user2: true
}
});
const user1 = await Users.pack(game.user1 ?? game.user1Id, null, {
detail: false
});
const user2 = await Users.pack(game.user2 ?? game.user2Id, null, {
detail: false
});
return {
id: game.id,
createdAt: createdAtFromId(game.id),
startedAt: game.startedAt?.toISOString() ?? null,
endedAt: game.endedAt?.toISOString() ?? null,
isStarted: game.isStarted,
isEnded: game.isEnded,
form1: game.form1,
form2: game.form2,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1,
user2,
winnerId: game.winnerId,
winner: game.winnerId ? [
user1,
user2
].find((u)=>u.id === game.winnerId) ?? null : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: assertBw(game.bw),
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
timeLimitForEachTurn: game.timeLimitForEachTurn,
noIrregularRules: game.noIrregularRules,
logs: game.logs,
map: game.map
};
},
async packLite (src) {
const detail = await this.packDetail(src);
const { logs, map, form1, form2, user1Ready, user2Ready, ...lite } = detail;
return lite;
},
genId
});
@@ -0,0 +1,57 @@
import { db } from "../../db/postgre.js";
import { genId } from "../../misc/gen-id.js";
import { ShogiGame } from "../entities/shogi-game.js";
import { Users } from "../index.js";
function createdAtFromId(id) {
const time = parseInt(id.slice(0, 8), 36);
if (Number.isNaN(time)) return new Date().toISOString();
return new Date(time + 946684800000).toISOString();
}
export const ShogiGameRepository = db.getRepository(ShogiGame).extend({
async packDetail (src) {
const game = typeof src === "object" ? src : await this.findOneOrFail({
where: {
id: src
},
relations: {
user1: true,
user2: true
}
});
const user1 = await Users.pack(game.user1 ?? game.user1Id, null, {
detail: false
});
const user2 = await Users.pack(game.user2 ?? game.user2Id, null, {
detail: false
});
return {
id: game.id,
createdAt: createdAtFromId(game.id),
startedAt: game.startedAt?.toISOString() ?? null,
endedAt: game.endedAt?.toISOString() ?? null,
isStarted: game.isStarted,
isEnded: game.isEnded,
user1Ready: game.user1Ready,
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1,
user2,
winnerId: game.winnerId,
winner: game.winnerId ? [
user1,
user2
].find((u)=>u.id === game.winnerId) ?? null : null,
surrenderedUserId: game.surrenderedUserId,
sente: game.sente,
sfen: game.sfen,
logs: game.logs
};
},
async packLite (src) {
const detail = await this.packDetail(src);
const { logs, ...lite } = detail;
return lite;
},
genId
});
@@ -0,0 +1,7 @@
import { db } from "../../db/postgre.js";
import { Signin } from "../entities/signin.js";
export const SigninRepository = db.getRepository(Signin).extend({
async pack (src) {
return src;
}
});
@@ -0,0 +1,17 @@
import { db } from "../../db/postgre.js";
import { UserGroupInvitation } from "../entities/user-group-invitation.js";
import { UserGroups } from "../index.js";
export const UserGroupInvitationRepository = db.getRepository(UserGroupInvitation).extend({
async pack (src) {
const invitation = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return {
id: invitation.id,
group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId)
};
},
packMany (invitations) {
return Promise.all(invitations.map((x)=>this.pack(x)));
}
});
@@ -0,0 +1,23 @@
import { db } from "../../db/postgre.js";
import { UserGroup } from "../entities/user-group.js";
import { DriveFiles, UserGroupJoinings } from "../index.js";
export const UserGroupRepository = db.getRepository(UserGroup).extend({
async pack (src) {
const userGroup = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const users = await UserGroupJoinings.findBy({
userGroupId: userGroup.id
});
return {
id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(),
name: userGroup.name,
username: userGroup.username,
ownerId: userGroup.userId,
allowCalls: userGroup.allowCalls,
iconUrl: userGroup.iconFileId ? (await DriveFiles.pack(userGroup.iconFileId)).url : null,
userIds: users.map((x)=>x.userId)
};
}
});
@@ -0,0 +1,20 @@
import { db } from "../../db/postgre.js";
import { UserList } from "../entities/user-list.js";
import { UserListJoinings } from "../index.js";
export const UserListRepository = db.getRepository(UserList).extend({
async pack (src) {
const userList = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
const users = await UserListJoinings.findBy({
userListId: userList.id
});
return {
id: userList.id,
createdAt: userList.createdAt.toISOString(),
name: userList.name,
hideFromHomeTl: userList.hideFromHomeTl,
userIds: users.map((x)=>x.userId)
};
}
});
@@ -0,0 +1,53 @@
import { db } from "../../db/postgre.js";
import { UserProfile } from "../entities/user-profile.js";
import mfm from "mfm-js";
import { extractMentions } from "../../misc/extract-mentions.js";
import { resolveMentionToUserAndProfile } from "../../remote/resolve-user.js";
import { unique } from "../../prelude/array.js";
import config from "../../config/index.js";
import { Mutex, Semaphore } from "async-mutex";
const queue = new Semaphore(5);
export const UserProfileRepository = db.getRepository(UserProfile).extend({
// We must never await this without promiseEarlyReturn, otherwise giant webring-style profile mention trees will cause the queue to stop working
async updateMentions (id, limiter = new RecursionLimiter()) {
const profile = await this.findOneBy({
userId: id
});
if (!profile) return;
const tokens = [];
if (profile.description) tokens.push(...mfm.parse(profile.description));
if (profile.fields.length > 0) tokens.push(...profile.fields.map((p)=>mfm.parse(p.value).concat(mfm.parse(p.name))).flat());
return queue.runExclusive(async ()=>{
const partial = {
mentions: await populateMentions(tokens, profile.userHost, limiter)
};
return UserProfileRepository.update(profile.userId, partial);
});
}
});
async function populateMentions(tokens, objectHost, limiter) {
const mentions = extractMentions(tokens);
const resolved = await Promise.all(mentions.map((m)=>resolveMentionToUserAndProfile(m.username, m.host, objectHost, limiter)));
const remote = resolved.filter((p)=>p && p.data.host !== config.domain && (p.data.host !== null || objectHost !== null)).map((p)=>p);
const res = remote.map((m)=>{
return {
uri: m.user.uri,
url: m.profile?.url ?? undefined,
username: m.data.username,
host: m.data.host
};
});
return unique(res);
}
export class RecursionLimiter {
counter;
mutex = new Mutex();
constructor(count = 10){
this.counter = count;
}
shouldContinue() {
return this.mutex.runExclusive(()=>{
return this.counter-- > 0;
});
}
}
@@ -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
});
@@ -0,0 +1,29 @@
import { db } from "../../db/postgre.js";
import { Users } from "../index.js";
import { VerifiedBadgeRequest } from "../entities/verified-badge-request.js";
import { awaitAll } from "../../prelude/await-all.js";
export const VerifiedBadgeRequestRepository = db.getRepository(VerifiedBadgeRequest).extend({
async pack (src) {
const request = typeof src === "object" ? src : await this.findOneByOrFail({
id: src
});
return await awaitAll({
id: request.id,
createdAt: request.createdAt.toISOString(),
resolvedAt: request.resolvedAt?.toISOString() ?? null,
status: request.status,
comment: request.comment,
userId: request.userId,
resolverId: request.resolverId,
user: Users.pack(request.user || request.userId, null, {
detail: true
}),
resolver: request.resolverId ? Users.pack(request.resolver || request.resolverId, null, {
detail: true
}) : null
});
},
packMany (requests) {
return Promise.all(requests.map((x)=>this.pack(x)));
}
});