From 412cdad209f567d9654686be507ee34927214866 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Fri, 1 Mar 2024 23:41:55 +0900 Subject: [PATCH] feat: show unlisted posts from following users in antennas --- docs/changelog.md | 1 + .../backend/src/misc/check-hit-antenna.ts | 31 ++++++++++++++----- .../server/api/endpoints/antennas/notes.ts | 3 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 99ece6a663..865ea86050 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,7 @@ Critical security updates are indicated by the :warning: icon. - Introduce new full-text search engine and post search filters - Refactoring +- Show unlisted posts from following users in antennas (similar to [Fedibird](https://github.com/fedibird/mastodon/tree/fedibird) and [kmyblue](https://github.com/kmycode/mastodon), unlisted posts from people you don't follow won't be shown) ## v20240301 diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index 81776ae55e..b93cb459e8 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -2,12 +2,12 @@ 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 type { UserProfile } from "@/models/entities/user-profile.js"; -import { Blockings, UserProfiles } from "@/models/index.js"; +import { Blockings, Followings, UserProfiles } from "@/models/index.js"; import { getFullApAccount } from "@/misc/convert-host.js"; import * as Acct from "@/misc/acct.js"; +import { getWordHardMute } from "@/misc/check-word-mute.js"; import type { Packed } from "@/misc/schema.js"; import { Cache } from "@/misc/cache.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5); const hardMutesCache = new Cache<{ @@ -15,6 +15,7 @@ const hardMutesCache = new Cache<{ mutedWords: UserProfile["mutedWords"]; mutedPatterns: UserProfile["mutedPatterns"]; }>("hardMutes", 60 * 5); +const followingCache = new Cache<User["id"][]>("following", 60 * 5); export async function checkHitAntenna( antenna: Antenna, @@ -22,11 +23,10 @@ export async function checkHitAntenna( noteUser: { id: User["id"]; username: string; host: string | null }, ): Promise<boolean> { if (note.visibility === "specified") return false; - if (note.visibility === "home") return false; - if (!antenna.withReplies && note.replyId != null) return false; if (antenna.withFile) { if (note.fileIds && note.fileIds.length === 0) return false; } + if (!antenna.withReplies && note.replyId != null) return false; if (antenna.src === "users") { const accts = antenna.users.map((x) => { @@ -53,14 +53,19 @@ export async function checkHitAntenna( .map((xs) => xs.filter((x) => x !== "")) .filter((xs) => xs.length > 0); + let text = `${note.text ?? ""} ${note.cw ?? ""}`; + if (note.files != null) + text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`; + text = text.trim(); + 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()), + ? text.includes(keyword) + : text.toLowerCase().includes(keyword.toLowerCase()), ), ); @@ -78,8 +83,8 @@ export async function checkHitAntenna( const matched = excludeKeywords.some((and) => and.every((keyword) => antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()), + ? note.text?.includes(keyword) + : note.text?.toLowerCase().includes(keyword.toLowerCase()), ), ); @@ -94,6 +99,16 @@ export async function checkHitAntenna( ); if (blockings.includes(antenna.userId)) return false; + if (note.visibility === "followers" || note.visibility === "home") { + const following = await followingCache.fetch(antenna.userId, () => + Followings.find({ + where: { followerId: antenna.userId }, + select: ["followeeId"], + }).then((relations) => relations.map((relation) => relation.followeeId)), + ); + if (!following.includes(note.userId)) return false; + } + const mutes = await hardMutesCache.fetch(antenna.userId, () => UserProfiles.findOneByOrFail({ userId: antenna.userId, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 1512cb9281..cbe0318525 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -111,8 +111,7 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect("replyUser.banner", "replyUserBanner") .leftJoinAndSelect("renote.user", "renoteUser") .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .andWhere("note.visibility != 'home'"); + .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user);