Skip to content

Commit 6edf663

Browse files
committed
Add image_hash filter list
1 parent da2f70e commit 6edf663

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import typing
2+
3+
import aiohttp
4+
from pydis_core.utils.logging import get_logger
5+
6+
from bot.exts.filtering._filter_context import Event, FilterContext
7+
from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType
8+
from bot.exts.filtering._filters.filter import Filter
9+
from bot.exts.filtering._filters.image_hash import ImageHashFilter
10+
from bot.exts.filtering._image_hash import RhodiumAPIError, get_image_hash
11+
from bot.exts.filtering._settings import ActionSettings
12+
13+
if typing.TYPE_CHECKING:
14+
from bot.exts.filtering.filtering import Filtering
15+
16+
log = get_logger(__name__)
17+
18+
_MAX_IMAGE_SIZE = 5_000_000
19+
_U64_MASK = (1 << 64) - 1
20+
21+
22+
class ImageHashesList(FilterList[ImageHashFilter]):
23+
"""A list of perceptual image hashes that should trigger filtering when matched."""
24+
25+
name = "image_hash"
26+
27+
def __init__(self, filtering_cog: Filtering):
28+
super().__init__()
29+
filtering_cog.subscribe(self, Event.MESSAGE)
30+
31+
def get_filter_type(self, content: str) -> type[Filter]:
32+
"""Get a subclass of filter matching the filter list and the filter's content."""
33+
return ImageHashFilter
34+
35+
@property
36+
def filter_types(self) -> set[type[Filter]]:
37+
"""Return the types of filters used by this list."""
38+
return {ImageHashFilter}
39+
40+
async def actions_for(
41+
self, ctx: FilterContext
42+
) -> tuple[ActionSettings | None, list[str], dict[ListType, list[Filter]]]:
43+
"""Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods."""
44+
if not ctx.attachments:
45+
return None, [], {}
46+
47+
image_hashes = []
48+
for attachment in ctx.attachments:
49+
if (
50+
attachment.content_type is None
51+
or not attachment.content_type.startswith("image")
52+
or attachment.size > _MAX_IMAGE_SIZE
53+
):
54+
continue
55+
56+
try:
57+
image_hash = await get_image_hash(attachment.url)
58+
except aiohttp.ClientError:
59+
log.exception("Unhandled aiohttp exception while getting image hash")
60+
continue
61+
except RhodiumAPIError as e:
62+
log.exception("Rhodium API error: %s", e)
63+
continue
64+
except TimeoutError:
65+
log.exception("Timed out getting image hash")
66+
continue
67+
68+
image_hashes.append(image_hash & _U64_MASK)
69+
70+
if not image_hashes:
71+
return None, [], {}
72+
73+
triggers = await self[ListType.DENY].filter_list_result(ctx.replace(content=image_hashes))
74+
if not triggers:
75+
return None, [], {ListType.DENY: triggers}
76+
77+
actions = self[ListType.DENY].merge_actions(triggers)
78+
messages = [
79+
f"{filter_.id} (`{filter_.content}`) - {filter_.description or '*No description*'}"
80+
for filter_ in triggers
81+
]
82+
return actions, messages, {ListType.DENY: triggers}

0 commit comments

Comments
 (0)