Skip to content

Commit 5fdef66

Browse files
committed
Update !filter hash command to show nearest known hash
1 parent 0c50327 commit 5fdef66

3 files changed

Lines changed: 54 additions & 8 deletions

File tree

bot/exts/filtering/_filters/image_hash.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
from bot.exts.filtering._filter_context import FilterContext
66
from bot.exts.filtering._filters.filter import Filter
7+
from bot.exts.filtering._image_hash import HASH_DISTANCE_THRESHOLD
78

8-
# Maximum perceptual hash difference for positive predictions.
9-
_THRESHOLD = 4
109
_HEX_RE = re.compile(r"^(?:0x)?([0-9a-fA-F]{1,16})$")
1110

1211

@@ -20,7 +19,7 @@ async def triggered_on(self, ctx: FilterContext) -> bool:
2019
candidate_hash = int(self.content, 16)
2120

2221
for image_hash in ctx.content:
23-
if int.bit_count(image_hash ^ candidate_hash) <= _THRESHOLD:
22+
if int.bit_count(image_hash ^ candidate_hash) <= HASH_DISTANCE_THRESHOLD:
2423
ctx.matches.append(f"{image_hash:016x}")
2524
return True
2625
return False

bot/exts/filtering/_image_hash.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
# Maximum number of seconds to wait for Rhodium API.
66
_TIMEOUT = 5
7+
# Maximum perceptual hash difference for a positive prediction.
8+
HASH_DISTANCE_THRESHOLD = 4
79

810

911
class RhodiumAPIError(Exception):

bot/exts/filtering/filtering.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
from bot.exts.filtering._filter_lists import FilterList, ListType, ListTypeConverter, filter_list_types
3232
from bot.exts.filtering._filter_lists.filter_list import AtomicList
3333
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+
)
3540
from bot.exts.filtering._settings import ActionSettings
3641
from bot.exts.filtering._settings_types.actions.infraction_and_notification import Infraction
3742
from bot.exts.filtering._ui.filter import (
@@ -685,11 +690,32 @@ async def f_imagehash(self, ctx: Context) -> None:
685690
return
686691

687692
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,
692717
)
718+
await ctx.reply(embed=embed)
693719

694720
@filter.command(name="match")
695721
async def f_match(
@@ -1175,6 +1201,25 @@ def _first_attached_image(message: Message) -> discord.Attachment | None:
11751201
return attachment
11761202
return None
11771203

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+
11781223
async def _add_filter(
11791224
self,
11801225
ctx: Context,

0 commit comments

Comments
 (0)