|
31 | 31 | from bot.exts.filtering._filter_lists import FilterList, ListType, ListTypeConverter, filter_list_types |
32 | 32 | from bot.exts.filtering._filter_lists.filter_list import AtomicList |
33 | 33 | from bot.exts.filtering._filters.filter import Filter, UniqueFilter |
34 | | -from bot.exts.filtering._image_hash import RhodiumAPIError, get_image_hash, signed_i64_to_hex |
| 34 | +from bot.exts.filtering._image_hash import ( |
| 35 | + HASH_DISTANCE_THRESHOLD, |
| 36 | + RhodiumAPIError, |
| 37 | + get_image_hash, |
| 38 | + signed_i64_to_hex, |
| 39 | +) |
35 | 40 | from bot.exts.filtering._settings import ActionSettings |
36 | 41 | from bot.exts.filtering._settings_types.actions.infraction_and_notification import Infraction |
37 | 42 | from bot.exts.filtering._ui.filter import ( |
@@ -685,11 +690,32 @@ async def f_imagehash(self, ctx: Context) -> None: |
685 | 690 | return |
686 | 691 |
|
687 | 692 | image_hash_hex = signed_i64_to_hex(image_hash) |
688 | | - await ctx.reply( |
689 | | - "Image hash:\n" |
690 | | - f"- hex: `{image_hash_hex}`\n" |
691 | | - f"- signed i64: `{image_hash}`" |
| 693 | + uploaded_hash = int(image_hash_hex, 16) |
| 694 | + closest = self._closest_image_hash_filter(uploaded_hash) |
| 695 | + |
| 696 | + if closest is None: |
| 697 | + embed = Embed(colour=Colour.orange(), title="Rhodium Hash") |
| 698 | + embed.add_field(name="hex", value=f"`{image_hash_hex}`", inline=False) |
| 699 | + embed.add_field(name="closest known", value="❔ no denied `image_hash` filters found", inline=False) |
| 700 | + await ctx.reply(embed=embed) |
| 701 | + return |
| 702 | + |
| 703 | + closest_filter, distance = closest |
| 704 | + captured = distance <= HASH_DISTANCE_THRESHOLD |
| 705 | + status_emoji = "✅" if captured else "🆕" |
| 706 | + colour = Colour.green() if captured else Colour.orange() |
| 707 | + embed = Embed(colour=colour, title="Rhodium Hash") |
| 708 | + embed.add_field(name="hex", value=f"`{image_hash_hex}`", inline=False) |
| 709 | + embed.add_field( |
| 710 | + name="closest known", |
| 711 | + value=( |
| 712 | + f"{status_emoji} `#{closest_filter.id}` (`{closest_filter.content}`)" |
| 713 | + f" - {closest_filter.description or '*No description*'}" |
| 714 | + f" (distance `{distance}`)" |
| 715 | + ), |
| 716 | + inline=False, |
692 | 717 | ) |
| 718 | + await ctx.reply(embed=embed) |
693 | 719 |
|
694 | 720 | @filter.command(name="match") |
695 | 721 | async def f_match( |
@@ -1175,6 +1201,25 @@ def _first_attached_image(message: Message) -> discord.Attachment | None: |
1175 | 1201 | return attachment |
1176 | 1202 | return None |
1177 | 1203 |
|
| 1204 | + def _closest_image_hash_filter(self, uploaded_hash: int) -> tuple[Filter, int] | None: |
| 1205 | + """Return the closest denied image_hash filter and its hamming distance.""" |
| 1206 | + image_hash_list = self.filter_lists.get("image_hash") |
| 1207 | + if not image_hash_list or ListType.DENY not in image_hash_list: |
| 1208 | + return None |
| 1209 | + |
| 1210 | + closest: tuple[Filter, int] | None = None |
| 1211 | + for filter_ in image_hash_list[ListType.DENY].filters.values(): |
| 1212 | + try: |
| 1213 | + candidate_hash = int(filter_.content, 16) |
| 1214 | + except ValueError: |
| 1215 | + continue |
| 1216 | + |
| 1217 | + distance = int.bit_count(uploaded_hash ^ candidate_hash) |
| 1218 | + if closest is None or distance < closest[1]: |
| 1219 | + closest = (filter_, distance) |
| 1220 | + |
| 1221 | + return closest |
| 1222 | + |
1178 | 1223 | async def _add_filter( |
1179 | 1224 | self, |
1180 | 1225 | ctx: Context, |
|
0 commit comments