vojo/src/app/pages/client/direct/useDirectInvites.ts

97 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { allInvitesAtom } from '../../../state/room-list/inviteList';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { bannedInRooms, getMemberDisplayName, getStateEvent } from '../../../utils/room';
import { testBadWords } from '../../../plugins/bad-words';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { getMxIdLocalPart } from '../../../utils/matrix';
import { StateEvent } from '../../../../types/matrix/room';
export type DirectInviteEntry = {
room: Room;
roomId: string;
ts: number;
isSpam: boolean;
};
const getInviteTs = (room: Room, myUserId: string): number => {
const me = room.getMember(myUserId);
return me?.events.member?.getTs() ?? 0;
};
const inviteHasBadWords = (room: Room, myUserId: string): boolean => {
const roomName = room.name || '';
const topic =
getStateEvent(room, StateEvent.RoomTopic)?.getContent<{ topic?: string }>()?.topic ?? '';
const me = room.getMember(myUserId);
const memberEvent = me?.events.member;
const senderId = memberEvent?.getSender() ?? '';
const senderName = senderId
? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
: '';
const reasonContent = memberEvent?.getContent();
const reason =
reasonContent && 'reason' in reasonContent && typeof reasonContent.reason === 'string'
? reasonContent.reason
: '';
return (
testBadWords(roomName) ||
testBadWords(topic) ||
testBadWords(senderName) ||
testBadWords(senderId) ||
testBadWords(reason)
);
};
export const useDirectInvites = (): DirectInviteEntry[] => {
const mx = useMatrixClient();
const inviteIds = useAtomValue(allInvitesAtom);
const allRooms = useAtomValue(allRoomsAtom);
const [spamFilterEnabled] = useSetting(settingsAtom, 'inviteSpamFilter');
const myUserId = mx.getSafeUserId();
return useMemo(() => {
// Cache `bannedInRooms` per senderId — a single inviter MXID can be the
// sender of multiple invite rooms, and bannedInRooms iterates every
// joined room. Without the cache we'd be O(invites × joinedRooms) on
// every recompute.
const bannedCache = new Map<string, boolean>();
const isSenderBanned = (senderId: string): boolean => {
const cached = bannedCache.get(senderId);
if (cached !== undefined) return cached;
const banned = bannedInRooms(mx, allRooms, senderId);
bannedCache.set(senderId, banned);
return banned;
};
const out: DirectInviteEntry[] = [];
inviteIds.forEach((roomId) => {
const room = mx.getRoom(roomId);
if (!room) return;
const me = room.getMember(myUserId);
const senderId = me?.events.member?.getSender() ?? '';
// Moderation signal — sender is banned in a room we share. This is a
// server-side moderator's verdict, not a personal preference, so it's
// ALWAYS classified as spam regardless of the user-facing toggle.
const moderationSpam = !!senderId && isSenderBanned(senderId);
// Lexical signal — bad-words match in the room name / topic / sender /
// reason. This is heuristic and noisy, so it's gated on the user-facing
// `inviteSpamFilter` toggle for users who'd rather see everything raw.
const lexicalSpam = spamFilterEnabled && inviteHasBadWords(room, myUserId);
const isSpam = moderationSpam || lexicalSpam;
out.push({
room,
roomId,
ts: getInviteTs(room, myUserId),
isSpam,
});
});
out.sort((a, b) => b.ts - a.ts);
return out;
}, [mx, inviteIds, allRooms, myUserId, spamFilterEnabled]);
};