Files
FrozenFriendsYume/packages/backend/built/server/api/mastodon/helpers/misc.js
T
2026-07-26 18:25:37 +09:00

216 lines
9.4 KiB
JavaScript

import config from "../../../../config/index.js";
import { FILE_TYPE_BROWSERSAFE, MAX_NOTE_TEXT_LENGTH } from "../../../../const.js";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { AnnouncementReads, Announcements, Emojis, Instances, Notes, UserProfiles, Users } from "../../../../models/index.js";
import { IsNull } from "typeorm";
import { awaitAll } from "../../../../prelude/await-all.js";
import { UserConverter } from "../converters/user.js";
import { AnnouncementConverter } from "../converters/announcement.js";
import { genId } from "../../../../misc/gen-id.js";
import * as Acct from "../../../../misc/acct.js";
import { UserHelpers } from "./user.js";
import { generateMutedUserQueryForUsers } from "../../common/generate-muted-user-query.js";
import { generateBlockQueryForUsers } from "../../common/generate-block-query.js";
import { uniqBy } from "../../../../prelude/array.js";
import { EmojiConverter } from "../converters/emoji.js";
import { populateEmojis } from "../../../../misc/populate-emojis.js";
import { NoteConverter } from "../converters/note.js";
import { VisibilityConverter } from "../converters/visibility.js";
export class MiscHelpers {
static async getInstance(ctx) {
const userCount = Users.count({
where: {
host: IsNull()
}
});
const noteCount = Notes.count({
where: {
userHost: IsNull()
}
});
const instanceCount = Instances.count({
cache: 3600000
});
const contact = await Users.findOne({
where: {
host: IsNull(),
isAdmin: true,
isDeleted: false,
isSuspended: false
},
order: {
id: "ASC"
}
}).then((p)=>p ? UserConverter.encode(p, ctx) : null);
const meta = await fetchMeta(true);
const res = {
uri: config.domain,
title: meta.name || "FrozenFriendsYume",
short_description: meta.description?.substring(0, 50) || "This is an FrozenFriendsYume instance. It doesn't seem to have a description.",
description: meta.description || "This is an FrozenFriendsYume instance. It doesn't seem to have a description.",
email: meta.maintainerEmail || "",
version: `4.2.1 (compatible; FrozenFriendsYume ${config.version})`,
urls: {
streaming_api: `${config.url.replace(/^http(?=s?:\/\/)/, "ws")}`
},
stats: awaitAll({
user_count: userCount,
status_count: noteCount,
domain_count: instanceCount
}),
max_toot_chars: MAX_NOTE_TEXT_LENGTH,
thumbnail: meta.bannerUrl || "/static-assets/transparent.png",
languages: meta.langs,
registrations: !meta.disableRegistration,
approval_required: meta.disableRegistration,
invites_enabled: meta.disableRegistration,
configuration: {
accounts: {
max_featured_tags: 20
},
statuses: {
supported_mime_types: [
'text/x.misskeymarkdown'
],
max_characters: MAX_NOTE_TEXT_LENGTH,
max_media_attachments: 16,
characters_reserved_per_url: 23
},
media_attachments: {
supported_mime_types: FILE_TYPE_BROWSERSAFE,
image_size_limit: 10485760,
image_matrix_limit: 16777216,
video_size_limit: 41943040,
video_frame_limit: 60,
video_matrix_limit: 2304000
},
polls: {
max_options: 10,
max_characters_per_option: 50,
min_expiration: 50,
max_expiration: 2629746
},
reactions: {
max_reactions: 1,
default_reaction: meta.defaultReaction
}
},
contact_account: contact,
rules: []
};
return awaitAll(res);
}
static async getAnnouncements(includeRead = false, ctx) {
const user = ctx.user;
if (includeRead) {
const [announcements, reads] = await Promise.all([
Announcements.createQueryBuilder("announcement").orderBy({
"announcement.id": "DESC"
}).getMany(),
AnnouncementReads.findBy({
userId: user.id
}).then((p)=>p.map((x)=>x.announcementId))
]);
return Promise.all(announcements.map(async (p)=>AnnouncementConverter.encode(p, reads.includes(p.id))));
}
const sq = AnnouncementReads.createQueryBuilder("reads").select("reads.announcementId").where("reads.userId = :userId");
const query = Announcements.createQueryBuilder("announcement").where(`announcement.id NOT IN (${sq.getQuery()})`).orderBy({
"announcement.id": "DESC"
}).setParameter("userId", user.id);
return query.getMany().then((p)=>Promise.all(p.map(async (x)=>AnnouncementConverter.encode(x, false))));
}
static async dismissAnnouncement(announcement, ctx) {
const user = ctx.user;
const exists = await AnnouncementReads.exist({
where: {
userId: user.id,
announcementId: announcement.id
}
});
if (!exists) {
await AnnouncementReads.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
announcementId: announcement.id
});
}
}
static async getFollowSuggestions(limit, ctx) {
const user = ctx.user;
const results = [];
const pinned = fetchMeta().then((meta)=>Promise.all(meta.pinnedUsers.map((acct)=>Acct.parse(acct)).map((acct)=>Users.findOneBy({
usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull()
}))).then((p)=>p.filter((x)=>!!x)).then((p)=>UserConverter.encodeMany(p, ctx)).then((p)=>p.map((x)=>{
return {
source: "staff",
account: x
};
})));
const query = Users.createQueryBuilder("user").where("user.isExplorable = TRUE").andWhere("user.host IS NULL").orderBy("user.followersCount", "DESC").andWhere("user.updatedAt > :date", {
date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5)
});
generateMutedUserQueryForUsers(query, user);
generateBlockQueryForUsers(query, user);
const global = query.take(limit).getMany().then((p)=>UserConverter.encodeMany(p, ctx)).then((p)=>p.map((x)=>{
return {
source: "global",
account: x
};
}));
results.push(pinned);
results.push(global);
return Promise.all(results).then((p)=>uniqBy(p.flat(), (x)=>x.account.id).slice(0, limit));
}
static async getCustomEmoji() {
return Emojis.find({
where: {
host: IsNull()
},
order: {
category: "ASC",
name: "ASC"
},
cache: {
id: "meta_emojis",
milliseconds: 3600000
}
}).then((dbRes)=>populateEmojis(dbRes.map((p)=>p.name), null).then((p)=>p.map((x)=>EmojiConverter.encode(x)).map((x)=>{
return {
...x,
category: dbRes.find((y)=>y.name === x.shortcode)?.category ?? undefined
};
})));
}
static async getTrendingStatuses(limit = 20, offset = 0, ctx) {
if (limit > 40) limit = 40;
const query = Notes.createQueryBuilder("note").addSelect("note.score").andWhere("note.score > 0").andWhere("note.createdAt > :date", {
date: new Date(Date.now() - 1000 * 60 * 60 * 24)
}).andWhere("note.visibility = 'public'").andWhere("note.userHost IS NULL").orderBy("note.score", "DESC");
return query.skip(offset).take(limit).getMany().then((result)=>NoteConverter.encodeMany(result, ctx));
}
static async getTrendingHashtags(limit = 10, offset = 0) {
if (limit > 20) limit = 20;
return [];
//FIXME: This was already implemented in api/endpoints/hashtags/trend.ts, but the implementation is sketchy at best. Rewrite from scratch.
}
static getPreferences(ctx) {
const user = ctx.user;
const profile = UserProfiles.findOneByOrFail({
userId: user.id
});
const sensitive = profile.then((p)=>p.alwaysMarkNsfw);
const language = profile.then((p)=>p.lang);
const privacy = UserHelpers.getDefaultNoteVisibility(ctx).then((p)=>VisibilityConverter.encode(p));
const res = {
"posting:default:visibility": privacy,
"posting:default:sensitive": sensitive,
"posting:default:language": language,
"reading:expand:media": "default",
"reading:expand:spoilers": false //FIXME: store this on server instead of client
};
return awaitAll(res);
}
}