import rndstr from "rndstr"; import { Notes, PromoNotes, PromoReads, Users } from "../../../../models/index.js"; import define from "../../define.js"; import { shouldHideEUsersFor } from "../../common/generate-minor-badge-visibility-query.js"; import { readPromo } from "../../common/read-promo.js"; const serviceTags = { video: "videoservice", audio: "audioservice", image: "imageservice", karaoke: "karaokeservice", lua4frozen: "lua4frozen" }; const systemTags = new Set([ "adservice", "videoservice", "audioservice", "imageservice", "karaokeservice", "lua4frozen" ]); function isExplicitAd(note) { return note.tags.includes("explicit") || note.user?.minorBadges?.includes("E") === true; } function canShowExplicit(note, user, hideExplicit) { if (!isExplicitAd(note)) return true; if (hideExplicit) return false; return !(user && shouldHideEUsersFor(user)); } function hasPollOr(note, fn) { return note.hasPoll || fn(); } function hasFileType(note, prefix) { return note.attachedFileTypes.some((type)=>type.startsWith(prefix)); } function matchesService(note, service) { if (service == null) return true; if (!note.tags.includes(serviceTags[service])) return false; switch(service){ case "video": return hasPollOr(note, ()=>hasFileType(note, "video/")); case "audio": return hasPollOr(note, ()=>hasFileType(note, "audio/")); case "image": return hasPollOr(note, ()=>hasFileType(note, "image/")); case "karaoke": return hasFileType(note, "video/") || hasFileType(note, "audio/") && hasFileType(note, "image/"); case "lua4frozen": return hasPollOr(note, ()=>hasFileType(note, "image/")); } } export const meta = { tags: [ "notes" ], requireCredential: false, requireCredentialPrivateMode: true }; export const paramDef = { type: "object", properties: { service: { type: "string", enum: [ "video", "audio", "image", "karaoke", "lua4frozen" ], nullable: true }, tag: { type: "string", nullable: true }, hideExplicit: { type: "boolean", default: false } }, required: [] }; export default define(meta, paramDef, async (ps, user)=>{ const readDay = new Date().toISOString().slice(0, 10); const reads = user ? await PromoReads.findBy({ userId: user.id, readDay }) : []; const readNoteIds = new Set(reads.map((read)=>read.noteId)); const promos = (await PromoNotes.find()).filter((promo)=>promo.expiresAt.getTime() > Date.now() && promo.remainingCredits > 0 && !readNoteIds.has(promo.noteId)); const candidates = []; for (const promo of promos){ const note = await Notes.findOneBy({ id: promo.noteId }); if (!note?.tags.includes("adservice")) continue; note.user = await Users.findOneByOrFail({ id: note.userId }); if (!canShowExplicit(note, user, ps.hideExplicit)) continue; if (!matchesService(note, ps.service ?? null)) continue; candidates.push(note); } if (candidates.length === 0) return null; const requestedTag = ps.tag?.trim().toLowerCase().replace(/^#/, ""); const priority = requestedTag && !systemTags.has(requestedTag) ? candidates.filter((note)=>note.tags.includes(requestedTag)) : []; const pool = priority.length > 0 ? priority : candidates; const note = pool[Math.floor(Math.random() * pool.length)]; const promo = promos.find((promo)=>promo.noteId === note.id); note._prId_ = rndstr("a-z0-9", 8); if (user && promo) { await readPromo(note, promo, user); } return Notes.pack(note, user); });