Skip to content

Commit 59262f2

Browse files
Merge pull request #36 from Jamesllllllllll/codex/requester-blacklist-by-chatter
Add chatter blacklist controls and fix search totals
2 parents c83f06c + a193745 commit 59262f2

File tree

9 files changed

+382
-48
lines changed

9 files changed

+382
-48
lines changed

src/components/song-search-panel.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,11 @@ export function SongSearchPanel(props: {
130130
infoNote?: string;
131131
placeholder?: string;
132132
className?: string;
133+
extraSearchParams?: Record<string, string | number | boolean | undefined>;
133134
resultFilter?: (song: SearchSong) => boolean;
134135
resultState?: (song: SearchSong) => SearchSongResultState;
135136
advancedFiltersContent?: ReactNode;
137+
useTotalForSummary?: boolean;
136138
}) {
137139
const [query, setQuery] = useState("");
138140
const [debouncedQuery, setDebouncedQuery] = useState("");
@@ -194,8 +196,24 @@ export function SongSearchPanel(props: {
194196
}
195197
}
196198

199+
if (props.extraSearchParams) {
200+
for (const [key, value] of Object.entries(props.extraSearchParams)) {
201+
if (value === undefined) {
202+
continue;
203+
}
204+
205+
params.set(key, String(value));
206+
}
207+
}
208+
197209
return params;
198-
}, [debouncedAdvancedFilters, debouncedQuery, field, page]);
210+
}, [
211+
debouncedAdvancedFilters,
212+
debouncedQuery,
213+
field,
214+
page,
215+
props.extraSearchParams,
216+
]);
199217

200218
const hasSearchInput = useMemo(
201219
() =>
@@ -320,6 +338,9 @@ export function SongSearchPanel(props: {
320338
"{count}",
321339
String(catalogTotalQuery.data?.total ?? 0)
322340
);
341+
const summaryCount = props.useTotalForSummary
342+
? (data?.total ?? 0)
343+
: visibleResults.length;
323344
const totalPages = Math.max(
324345
1,
325346
Math.ceil((data?.total ?? 0) / (data?.pageSize ?? 25))
@@ -510,7 +531,7 @@ export function SongSearchPanel(props: {
510531
{!queryTooShort && !error ? (
511532
<div className="search-panel__summary rounded-[24px] border border-(--border) bg-(--panel-soft) px-4 py-3 text-right">
512533
<p className="text-lg font-semibold text-(--text)">
513-
Found {visibleResults.length} songs
534+
Found {summaryCount} songs
514535
</p>
515536
</div>
516537
) : null}

src/lib/db/repositories.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,9 @@ export interface CatalogSearchInput {
749749
tuning?: string[];
750750
parts?: string[];
751751
year?: number[];
752+
excludeSongIds?: number[];
753+
excludeArtistNames?: string[];
754+
excludeCreatorNames?: string[];
752755
page: number;
753756
pageSize: number;
754757
sortBy?:
@@ -1040,12 +1043,47 @@ export async function searchCatalogSongs(
10401043
const advancedCondition = hasAdvancedFilters
10411044
? sql.join(advancedConditions, sql` AND `)
10421045
: null;
1043-
const whereCondition =
1046+
const baseWhereCondition =
10441047
query && advancedCondition
10451048
? sql`(${basicCondition}) AND (${advancedCondition})`
10461049
: query
10471050
? basicCondition
10481051
: (advancedCondition ?? sql`1 = 1`);
1052+
const normalizedExcludedArtists = uniqueCompact(
1053+
(input.excludeArtistNames ?? []).map((name) => normalizeSearchPhrase(name))
1054+
);
1055+
const normalizedExcludedCreators = uniqueCompact(
1056+
(input.excludeCreatorNames ?? []).map((name) => normalizeSearchPhrase(name))
1057+
);
1058+
const excludedSongIds = [...new Set(input.excludeSongIds ?? [])].filter(
1059+
(songId): songId is number => Number.isInteger(songId) && songId > 0
1060+
);
1061+
const blacklistConditions = [
1062+
excludedSongIds.length
1063+
? sql`${catalogSongs.sourceSongId} NOT IN ${sql`(${sql.join(
1064+
excludedSongIds.map((songId) => sql`${songId}`),
1065+
sql`, `
1066+
)})`}`
1067+
: null,
1068+
normalizedExcludedArtists.length
1069+
? sql`lower(coalesce(${catalogSongs.artistName}, '')) NOT IN ${sql`(${sql.join(
1070+
normalizedExcludedArtists.map((name) => sql`${name}`),
1071+
sql`, `
1072+
)})`}`
1073+
: null,
1074+
normalizedExcludedCreators.length
1075+
? sql`lower(coalesce(${catalogSongs.creatorName}, '')) NOT IN ${sql`(${sql.join(
1076+
normalizedExcludedCreators.map((name) => sql`${name}`),
1077+
sql`, `
1078+
)})`}`
1079+
: null,
1080+
].filter(
1081+
(condition): condition is ReturnType<typeof sql> => condition !== null
1082+
);
1083+
const whereCondition =
1084+
blacklistConditions.length > 0
1085+
? sql`(${baseWhereCondition}) AND (${sql.join(blacklistConditions, sql` AND `)})`
1086+
: baseWhereCondition;
10491087

10501088
const titleTokenScore = buildTokenMatchScore(
10511089
catalogSongs.title,
@@ -2030,6 +2068,23 @@ export async function addBlockedUser(
20302068
.onConflictDoNothing();
20312069
}
20322070

2071+
export async function removeBlockedUser(
2072+
env: AppEnv,
2073+
input: {
2074+
channelId: string;
2075+
twitchUserId: string;
2076+
}
2077+
) {
2078+
await getDb(env)
2079+
.delete(blockedUsers)
2080+
.where(
2081+
and(
2082+
eq(blockedUsers.channelId, input.channelId),
2083+
eq(blockedUsers.twitchUserId, input.twitchUserId)
2084+
)
2085+
);
2086+
}
2087+
20332088
export async function addBlacklistedArtist(
20342089
env: AppEnv,
20352090
input: Omit<BlacklistedArtistInsert, "createdAt">

src/lib/eventsub/chat-message.ts

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,21 @@ export async function processEventSubChatMessage(input: {
313313
return { body: "Ignored", status: 202 };
314314
}
315315

316+
if (await deps.isBlockedUser(env, channel.id, event.chatterTwitchUserId)) {
317+
await deps.createRequestLog(env, {
318+
channelId: channel.id,
319+
twitchMessageId: event.messageId,
320+
twitchUserId: event.chatterTwitchUserId,
321+
requesterLogin: event.chatterLogin,
322+
requesterDisplayName: event.chatterDisplayName,
323+
rawMessage: event.rawMessage,
324+
normalizedQuery: parsed.query,
325+
outcome: "blocked",
326+
outcomeReason: "user_blocked",
327+
});
328+
return { body: "Blocked", status: 202 };
329+
}
330+
316331
if (parsed.command === "addvip") {
317332
const targetLogin = parsed.query?.trim();
318333
const canManageVipTokens =
@@ -642,28 +657,6 @@ export async function processEventSubChatMessage(input: {
642657
}
643658
}
644659

645-
if (
646-
await deps.isBlockedUser(env, channel.id, requesterIdentity.twitchUserId)
647-
) {
648-
await deps.createRequestLog(env, {
649-
channelId: channel.id,
650-
twitchMessageId: event.messageId,
651-
twitchUserId: requesterIdentity.twitchUserId,
652-
requesterLogin: requesterIdentity.login,
653-
requesterDisplayName: requesterIdentity.displayName,
654-
rawMessage: event.rawMessage,
655-
normalizedQuery: parsed.query,
656-
outcome: "blocked",
657-
outcomeReason: "user_blocked",
658-
});
659-
await deps.sendChatReply(env, {
660-
channelId: channel.id,
661-
broadcasterUserId: channel.twitchChannelId,
662-
message: `${mention(event.chatterLogin)} you cannot request songs in this channel.`,
663-
});
664-
return { body: "Blocked", status: 202 };
665-
}
666-
667660
const vipTokenBalance = isVipCommand
668661
? await deps.getVipTokenBalance(env, {
669662
channelId: channel.id,

src/lib/validation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ const searchFieldSchema = z.enum([
2323
export const searchInputSchema = z
2424
.object({
2525
query: z.string().trim().max(200).optional(),
26+
channelSlug: z.string().trim().max(100).optional(),
27+
showBlacklisted: z
28+
.preprocess(
29+
(value) =>
30+
value === undefined
31+
? undefined
32+
: value === true || value === "true"
33+
? true
34+
: value === false || value === "false"
35+
? false
36+
: value,
37+
z.boolean().optional()
38+
)
39+
.default(false),
2640
field: searchFieldSchema.default("any"),
2741
title: z.string().trim().max(200).optional(),
2842
artist: z.string().trim().max(200).optional(),
@@ -85,6 +99,10 @@ export const moderationActionSchema = z.discriminatedUnion("action", [
8599
displayName: z.string().min(1).optional(),
86100
reason: z.string().trim().max(300).optional(),
87101
}),
102+
z.object({
103+
action: z.literal("removeBlockedUser"),
104+
twitchUserId: z.string().min(1),
105+
}),
88106
z.object({
89107
action: z.literal("addBlacklistedArtist"),
90108
artistId: z.number().int().positive(),

src/routes/api/dashboard/moderation.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
removeBlacklistedArtist,
1616
removeBlacklistedCharter,
1717
removeBlacklistedSong,
18+
removeBlockedUser,
1819
removeSetlistArtist,
1920
revokeVipToken,
2021
setVipTokenAvailableCount,
@@ -103,6 +104,21 @@ export const Route = createFileRoute("/api/dashboard/moderation")({
103104
payloadJson: JSON.stringify(body),
104105
});
105106
break;
107+
case "removeBlockedUser":
108+
await removeBlockedUser(runtimeEnv, {
109+
channelId: state.channel.id,
110+
twitchUserId: body.twitchUserId,
111+
});
112+
await createAuditLog(runtimeEnv, {
113+
channelId: state.channel.id,
114+
actorUserId: state.channel.ownerUserId,
115+
actorType: "owner",
116+
action: "unblock_user",
117+
entityType: "blocked_user",
118+
entityId: body.twitchUserId,
119+
payloadJson: JSON.stringify(body),
120+
});
121+
break;
106122
case "addBlacklistedArtist":
107123
await addBlacklistedArtist(runtimeEnv, {
108124
channelId: state.channel.id,

src/routes/api/search/route.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { getSessionUserId } from "~/lib/auth/session.server";
55
import {
66
consumeSearchRateLimit,
77
getCachedSearchResult,
8+
getChannelBlacklistByChannelId,
9+
getChannelBySlug,
810
searchCatalogSongs as searchCatalogSongsInDb,
911
upsertCachedSearchResult,
1012
} from "~/lib/db/repositories";
@@ -20,6 +22,8 @@ function normalizeSearchCacheInput(
2022
) {
2123
return {
2224
query: input.query ?? "",
25+
channelSlug: input.channelSlug ?? "",
26+
showBlacklisted: input.showBlacklisted ?? false,
2327
field: input.field,
2428
title: input.title ?? "",
2529
artist: input.artist ?? "",
@@ -62,6 +66,8 @@ export const Route = createFileRoute("/api/search")({
6266

6367
const payload = {
6468
query: url.searchParams.get("query") ?? undefined,
69+
channelSlug: url.searchParams.get("channelSlug") ?? undefined,
70+
showBlacklisted: url.searchParams.get("showBlacklisted") ?? undefined,
6571
field: url.searchParams.get("field") ?? undefined,
6672
title: url.searchParams.get("title") ?? undefined,
6773
artist: url.searchParams.get("artist") ?? undefined,
@@ -137,10 +143,37 @@ export const Route = createFileRoute("/api/search")({
137143
}
138144

139145
try {
140-
const results = await searchCatalogSongsInDb(
141-
runtimeEnv,
142-
normalizedInput
143-
);
146+
let blacklistFilterInput = {};
147+
if (normalizedInput.channelSlug && !normalizedInput.showBlacklisted) {
148+
const channel = await getChannelBySlug(
149+
runtimeEnv,
150+
normalizedInput.channelSlug
151+
);
152+
153+
if (channel) {
154+
const blacklist = await getChannelBlacklistByChannelId(
155+
runtimeEnv,
156+
channel.id
157+
);
158+
159+
blacklistFilterInput = {
160+
excludeSongIds: blacklist.blacklistSongs.map(
161+
(song) => song.songId
162+
),
163+
excludeArtistNames: blacklist.blacklistArtists.map(
164+
(artist) => artist.artistName
165+
),
166+
excludeCreatorNames: blacklist.blacklistCharters.map(
167+
(charter) => charter.charterName
168+
),
169+
};
170+
}
171+
}
172+
173+
const results = await searchCatalogSongsInDb(runtimeEnv, {
174+
...normalizedInput,
175+
...blacklistFilterInput,
176+
});
144177

145178
await upsertCachedSearchResult(runtimeEnv, {
146179
cacheKey,

0 commit comments

Comments
 (0)