|
10 | 10 | from operator import attrgetter |
11 | 11 | from typing import Literal, get_type_hints |
12 | 12 |
|
| 13 | +import aiohttp |
13 | 14 | import arrow |
14 | 15 | import discord |
15 | 16 | from async_rediscache import RedisCache |
|
30 | 31 | from bot.exts.filtering._filter_lists import FilterList, ListType, ListTypeConverter, filter_list_types |
31 | 32 | from bot.exts.filtering._filter_lists.filter_list import AtomicList |
32 | 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 |
33 | 35 | from bot.exts.filtering._settings import ActionSettings |
34 | 36 | from bot.exts.filtering._settings_types.actions.infraction_and_notification import Infraction |
35 | 37 | from bot.exts.filtering._ui.filter import ( |
|
64 | 66 | HOURS_BETWEEN_NICKNAME_ALERTS = 1 |
65 | 67 | OFFENSIVE_MSG_DELETE_TIME = datetime.timedelta(days=7) |
66 | 68 | WEEKLY_REPORT_ISO_DAY = 3 # 1=Monday, 7=Sunday |
| 69 | +MAX_IMAGE_HASH_SIZE = 5_000_000 |
67 | 70 |
|
68 | 71 |
|
69 | 72 | def _clean_ban_mentions(mentions: set[str]) -> set[str]: |
@@ -654,6 +657,40 @@ async def setting(self, ctx: Context, setting_name: str | None) -> None: |
654 | 657 | embed.set_author(name=f"Description of the {setting_name} setting") |
655 | 658 | await ctx.send(embed=embed) |
656 | 659 |
|
| 660 | + @filter.command(name="imagehash", aliases=("hash",)) |
| 661 | + async def f_imagehash(self, ctx: Context) -> None: |
| 662 | + """Hash an attached image and return its normalized hexadecimal perceptual hash.""" |
| 663 | + attachment = self._first_attached_image(ctx.message) |
| 664 | + if attachment is None: |
| 665 | + await ctx.reply(":x: Attach an image (or reply to a message with an image attachment).") |
| 666 | + return |
| 667 | + |
| 668 | + if attachment.size > MAX_IMAGE_HASH_SIZE: |
| 669 | + await ctx.reply(":x: The image must be 5 MB or smaller.") |
| 670 | + return |
| 671 | + |
| 672 | + try: |
| 673 | + image_hash = await get_image_hash(attachment.url) |
| 674 | + except aiohttp.ClientError: |
| 675 | + log.exception("Unhandled aiohttp exception while getting image hash") |
| 676 | + await ctx.reply(":x: Failed to reach the image hashing service.") |
| 677 | + return |
| 678 | + except RhodiumAPIError as e: |
| 679 | + log.exception("Rhodium API error: %s", e) |
| 680 | + await ctx.reply(":x: The image hashing service returned an error.") |
| 681 | + return |
| 682 | + except TimeoutError: |
| 683 | + log.exception("Timed out getting image hash") |
| 684 | + await ctx.reply(":x: Timed out while hashing the image.") |
| 685 | + return |
| 686 | + |
| 687 | + 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}`" |
| 692 | + ) |
| 693 | + |
657 | 694 | @filter.command(name="match") |
658 | 695 | async def f_match( |
659 | 696 | self, ctx: Context, no_user: bool | None, message: Message | None, *, string: str | None |
@@ -1121,6 +1158,23 @@ def _get_filter_by_id(self, id_: int) -> tuple[Filter, FilterList, ListType] | N |
1121 | 1158 | return sublist.filters[id_], filter_list, list_type |
1122 | 1159 | return None |
1123 | 1160 |
|
| 1161 | + @staticmethod |
| 1162 | + def _first_attached_image(message: Message) -> discord.Attachment | None: |
| 1163 | + """Return the first image attachment from the invoking or replied-to message.""" |
| 1164 | + attachments = list(message.attachments) |
| 1165 | + |
| 1166 | + if ( |
| 1167 | + not attachments |
| 1168 | + and message.reference |
| 1169 | + and isinstance(message.reference.resolved, Message) |
| 1170 | + ): |
| 1171 | + attachments = list(message.reference.resolved.attachments) |
| 1172 | + |
| 1173 | + for attachment in attachments: |
| 1174 | + if attachment.content_type and attachment.content_type.startswith("image"): |
| 1175 | + return attachment |
| 1176 | + return None |
| 1177 | + |
1124 | 1178 | async def _add_filter( |
1125 | 1179 | self, |
1126 | 1180 | ctx: Context, |
|
0 commit comments