import rndstr from "rndstr"; import { PromoReads, PromoNotes, Notes, Users } from "../../../models/index.js"; import { shouldHideEUsersFor } from "./generate-minor-badge-visibility-query.js"; import { readPromo } from "./read-promo.js"; const systemTags = new Set([ "adservice", "videoservice", "audioservice", "imageservice", "karaokeservice", "lua4frozen" ]); function isExplicitAd(note) { return note.tags.includes("explicit") || note.user?.minorBadges?.includes("E") === true; } export async function injectPromo(timeline, user, preferredTag) { // TODO: readやexpireフィルタはクエリ側でやる const readDay = new Date().toISOString().slice(0, 10); const reads = user ? await PromoReads.findBy({ userId: user.id, readDay }) : []; let promos = await PromoNotes.find(); promos = promos.filter((n)=>n.expiresAt.getTime() > Date.now()); promos = promos.filter((n)=>n.remainingCredits > 0); promos = promos.filter((n)=>!reads.map((r)=>r.noteId).includes(n.noteId)); if (promos.length === 0) return; 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 (user && shouldHideEUsersFor(user) && isExplicitAd(note)) continue; candidates.push(note); } if (candidates.length === 0) return; const normalizedPreferredTag = preferredTag?.trim().toLowerCase().replace(/^#/, ""); const priority = normalizedPreferredTag && !systemTags.has(normalizedPreferredTag) ? candidates.filter((note)=>note.tags.includes(normalizedPreferredTag)) : []; const pool = priority.length > 0 ? priority : candidates; // Pick random promo 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); } // Inject promo timeline.splice(Math.min(3, timeline.length), 0, note); }