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); } }