import type { Antenna } from "@/models/entities/antenna.js"; import type { Note } from "@/models/entities/note.js"; import type { User } from "@/models/entities/user.js"; import { UserListJoinings, UserGroupJoinings, Blockings, } from "@/models/index.js"; import { getFullApAccount } from "./convert-host.js"; import * as Acct from "@/misc/acct.js"; import type { Packed } from "./schema.js"; import { Cache } from "./cache.js"; const blockingCache = new Cache("blocking", 60 * 5); // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている /** * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい */ export async function checkHitAntenna( antenna: Antenna, note: Note | Packed<"Note">, noteUser: { id: User["id"]; username: string; host: string | null }, noteUserFollowers?: User["id"][], antennaUserFollowing?: User["id"][], ): Promise { if (note.visibility === "specified") return false; if (note.visibility === "home") return false; // アンテナ作成者がノート作成者にブロックされていたらスキップ const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then((res) => res.map((x) => x.blockeeId), ), ); if (blockings.some((blocking) => blocking === antenna.userId)) return false; if (note.visibility === "followers") { if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; } if (!antenna.withReplies && note.replyId != null) return false; if (antenna.src === "home") { if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; } else if (antenna.src === "list") { const listUsers = ( await UserListJoinings.findBy({ userListId: antenna.userListId!, }) ).map((x) => x.userId); if (!listUsers.includes(note.userId)) return false; } else if (antenna.src === "group") { const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId!, }); const groupUsers = ( await UserGroupJoinings.findBy({ userGroupId: joining.userGroupId, }) ).map((x) => x.userId); if (!groupUsers.includes(note.userId)) return false; } else if (antenna.src === "users") { const accts = antenna.users.map((x) => { const { username, host } = Acct.parse(x); return getFullApAccount(username, host).toLowerCase(); }); if ( !accts.includes( getFullApAccount(noteUser.username, noteUser.host).toLowerCase(), ) ) return false; } else if (antenna.src === "instances") { const instances = antenna.instances .filter((x) => x !== "") .map((host) => { return host.toLowerCase(); }); if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false; } const keywords = antenna.keywords // Clean up .map((xs) => xs.filter((x) => x !== "")) .filter((xs) => xs.length > 0); if (keywords.length > 0) { if (note.text == null) return false; const matched = keywords.some((and) => and.every((keyword) => antenna.caseSensitive ? note.text!.includes(keyword) : note.text!.toLowerCase().includes(keyword.toLowerCase()), ), ); if (!matched) return false; } const excludeKeywords = antenna.excludeKeywords // Clean up .map((xs) => xs.filter((x) => x !== "")) .filter((xs) => xs.length > 0); if (excludeKeywords.length > 0) { if (note.text == null) return false; const matched = excludeKeywords.some((and) => and.every((keyword) => antenna.caseSensitive ? note.text!.includes(keyword) : note.text!.toLowerCase().includes(keyword.toLowerCase()), ), ); if (matched) return false; } if (antenna.withFile) { if (note.fileIds && note.fileIds.length === 0) return false; } // TODO: eval expression return true; }