44
55What this does
66-------------
7- Counts bumps done via the **Disboard bot** in a configured bump channel.
7+ Counts bumps done via a **Bump Reminder** embed in a configured bump channel.
88
99Important technical note
1010------------------------
11- Discord does not expose *another bot's* slash-command interactions to your bot,
12- so we cannot directly "listen to /bump" when Disboard handles it.
13- Instead, we listen for Disboard's **confirmation message** (e.g. "Bump done")
14- in the bump channel and attribute the bump to the mentioned user.
11+ Discord does not expose *another bot/app's* slash-command interactions to your bot.
12+ Instead, we listen for a bump confirmation embed ("Bump Reminder") and attribute
13+ the bump to the user name stored in the embed title.
1514
1615Data is stored in the SQLite database (`botdata.db`).
1716
4241DATA_VERSION = 1
4342DEFAULT_COOLDOWN_SECONDS = 60 # basic anti-spam; adjust as needed
4443
45- # Default Disboard bot user id. If your server uses a different bump bot,
46- # you can change this constant.
47- DISBOARD_BOT_ID = 302050872383242240
48-
49- _MENTION_RE = re .compile (r"<@!?(\d{15,25})>" )
44+ # Minimal signal to identify a "bump" embed.
45+ _BUMP_CMD_RE = re .compile (r"\b/bump\b" , re .IGNORECASE )
5046
5147
5248def _utcnow () -> datetime :
@@ -103,72 +99,65 @@ def __init__(self, bot: commands.Bot):
10399 # In-memory cache to avoid repeatedly parsing ISO strings for cooldown checks.
104100 self ._last_bump_cache : Dict [int , Dict [int , datetime ]] = {}
105101
106- # Prevent double counting when Disboard edits the same message.
102+ # Prevent double counting when the same bump message is edited/reposted .
107103 self ._processed_message_ids : Dict [int , float ] = {}
108104
109- # Remember who invoked /bump most recently per (guild, channel).
110- # Disboard's confirmation embed often does not mention the user.
111- self ._recent_bump_invoker : Dict [Tuple [int , int ], Tuple [int , float ]] = {}
112-
113105 # Ensure DB tables exist (no JSON migration).
114106 self .bot .loop .create_task (self .load_data ())
115107
116108 # ----------------------------
117- # Disboard message parsing
109+ # Bump Reminder embed parsing
118110 # ----------------------------
119111
120- def _is_disboard_message (self , message : discord .Message ) -> bool :
121- return message .author is not None and message .author .id == DISBOARD_BOT_ID
112+ def _looks_like_bump_reminder_embed (self , embed : discord .Embed ) -> bool :
113+ """Return True if an embed looks like a bump confirmation."""
114+ # The screenshot shows a field like "Command ran: /bump".
115+ for f in embed .fields or []:
116+ name = (f .name or "" ).strip ()
117+ value = (f .value or "" ).strip ()
118+ if _BUMP_CMD_RE .search (name ) or _BUMP_CMD_RE .search (value ):
119+ return True
122120
123- def _message_text_blob ( self , message : discord . Message ) -> str :
121+ # Fallback: search embed text blob.
124122 parts : List [str ] = []
125- if message .content :
126- parts .append (message .content )
127-
123+ if embed .title :
124+ parts .append (str (embed .title ))
125+ if embed .description :
126+ parts .append (str (embed .description ))
127+ for f in embed .fields or []:
128+ if f .name :
129+ parts .append (str (f .name ))
130+ if f .value :
131+ parts .append (str (f .value ))
132+ return _BUMP_CMD_RE .search ("\n " .join (parts ) or "" ) is not None
133+
134+ def _extract_bumper_name_from_embeds (self , message : discord .Message ) -> Optional [str ]:
135+ """Return bumper username from the bump embed title."""
128136 for emb in message .embeds or []:
129- if emb .title :
130- parts .append (str (emb .title ))
131- if emb .description :
132- parts .append (str (emb .description ))
133- for f in emb .fields or []:
134- if f .name :
135- parts .append (str (f .name ))
136- if f .value :
137- parts .append (str (f .value ))
138-
139- return "\n " .join (parts )
140-
141- def _looks_like_bump_success (self , message : discord .Message ) -> bool :
142- blob = self ._message_text_blob (message ).lower ()
143- # Common Disboard phrases.
144- return (
145- "bump done" in blob
146- or "bumped" in blob and "done" in blob
147- or "successful" in blob and "bump" in blob
148- )
137+ if not emb or not emb .title :
138+ continue
139+ if not self ._looks_like_bump_reminder_embed (emb ):
140+ continue
141+ name = str (emb .title ).strip ()
142+ if name :
143+ return name
144+ return None
149145
150- async def _extract_bumper_user (self , message : discord .Message ) -> Optional [discord .abc .User ]:
151- # Prefer real resolved mentions.
152- for m in message .mentions or []:
153- if not m .bot :
146+ def _resolve_member_by_name (self , guild : discord .Guild , name : str ) -> Optional [discord .Member ]:
147+ """Resolve a guild member by display name / username (best-effort)."""
148+ # discord.py helper: matches nick / name / name#discrim.
149+ try :
150+ m = guild .get_member_named (name )
151+ if m is not None :
154152 return m
153+ except Exception :
154+ pass
155155
156- # Fall back to parsing mention tags in text.
157- blob = self ._message_text_blob (message )
158- m = _MENTION_RE .search (blob )
159- if not m :
160- return None
161-
162- user_id = int (m .group (1 ))
163- if message .guild :
164- member = message .guild .get_member (user_id )
165- if member :
156+ needle = name .casefold ()
157+ for member in guild .members :
158+ if member .display_name .casefold () == needle or member .name .casefold () == needle :
166159 return member
167-
168- try :
169- return await self .bot .fetch_user (user_id )
170- except Exception :
171- return None
160+ return None
172161
173162 def _cleanup_processed_cache (self ) -> None :
174163 # Keep ~10 minutes of ids; enough to cover edits/reposts.
@@ -177,62 +166,7 @@ def _cleanup_processed_cache(self) -> None:
177166 for mid in stale :
178167 self ._processed_message_ids .pop (mid , None )
179168
180- # Keep ~2 minutes of recent invokers.
181- inv_cutoff = time .monotonic () - 120
182- stale_keys = [k for k , (_ , ts ) in self ._recent_bump_invoker .items () if ts < inv_cutoff ]
183- for k in stale_keys :
184- self ._recent_bump_invoker .pop (k , None )
185-
186- def _record_bump_invocation (self , message : discord .Message ) -> None :
187- """Record a visible '/bump' invocation message so we can attribute Disboard's confirmation."""
188- if message .guild is None :
189- return
190-
191- # This is the "<user> used /bump" system message shown in Discord.
192- # In discord.py it comes through as MessageType.chat_input_command with a MessageInteraction.
193- if message .type != discord .MessageType .chat_input_command :
194- return
195-
196- # discord.py 2.4+: message.interaction_metadata (preferred)
197- # Older versions: message.interaction (deprecated)
198- meta = getattr (message , "interaction_metadata" , None )
199- mi = meta if meta is not None else getattr (message , "interaction" , None )
200- if mi is None :
201- return
202-
203- name = getattr (mi , "name" , None )
204- user = getattr (mi , "user" , None )
205- if name != "bump" or user is None :
206- return
207-
208- # Only store humans.
209- if getattr (user , "bot" , False ):
210- return
211-
212- key = (message .guild .id , message .channel .id )
213- self ._recent_bump_invoker [key ] = (int (user .id ), time .monotonic ())
214-
215- async def _get_recent_invoker (self , guild : discord .Guild , channel_id : int ) -> Optional [discord .abc .User ]:
216- key = (guild .id , channel_id )
217- rec = self ._recent_bump_invoker .get (key )
218- if not rec :
219- return None
220-
221- user_id , ts = rec
222- # Disboard replies quickly; allow a generous window.
223- if time .monotonic () - ts > 45 :
224- return None
225-
226- m = guild .get_member (user_id )
227- if m :
228- return m
229-
230- try :
231- return await self .bot .fetch_user (user_id )
232- except Exception :
233- return None
234-
235- async def _handle_possible_disboard_bump (self , message : discord .Message ) -> None :
169+ async def _handle_possible_bump_reminder_bump (self , message : discord .Message ) -> None :
236170 if message .guild is None :
237171 return
238172
@@ -245,10 +179,13 @@ async def _handle_possible_disboard_bump(self, message: discord.Message) -> None
245179 if message .channel .id != int (bump_channel_id ):
246180 return
247181
248- if not self ._is_disboard_message (message ):
182+ bumper_name = self ._extract_bumper_name_from_embeds (message )
183+ if not bumper_name :
249184 return
250185
251- if not self ._looks_like_bump_success (message ):
186+ bumper_member = self ._resolve_member_by_name (message .guild , bumper_name )
187+ if bumper_member is None :
188+ # If we can't resolve the member, do not guess.
252189 return
253190
254191 # Deduplicate message id (Disboard often edits the same message).
@@ -257,16 +194,19 @@ async def _handle_possible_disboard_bump(self, message: discord.Message) -> None
257194 return
258195 self ._processed_message_ids [message .id ] = time .monotonic ()
259196
260- bumper = await self ._extract_bumper_user (message )
261- if bumper is None :
262- # Disboard embed often doesn't mention the user; fall back to the
263- # last '/bump' invoker message in the channel.
264- bumper = await self ._get_recent_invoker (message .guild , message .channel .id )
265- if bumper is None :
266- return
197+ # Count the bump (+1) and thank the user.
198+ await self .update_bump_count (
199+ message .guild ,
200+ bumper_member ,
201+ now = message .created_at or _utcnow (),
202+ amount = 1 ,
203+ bypass_cooldown = True ,
204+ )
267205
268- # Count the bump. Disboard enforces ~2h cooldown, so we bypass our own.
269- await self .update_bump_count (message .guild , bumper , now = message .created_at or _utcnow (), amount = 1 , bypass_cooldown = True )
206+ try :
207+ await message .channel .send (f"Thanks { bumper_member .mention } for bump" )
208+ except Exception :
209+ pass
270210
271211 # ----------------------------
272212 # Event listeners
@@ -278,22 +218,19 @@ async def on_message(self, message: discord.Message) -> None:
278218 if message .author and message .author .id == getattr (self .bot .user , "id" , None ):
279219 return
280220 try :
281- # Track who invoked /bump (system message).
282- self ._record_bump_invocation (message )
283- await self ._handle_possible_disboard_bump (message )
221+ await self ._handle_possible_bump_reminder_bump (message )
284222 except Exception :
285- logger .exception ("Failed handling possible Disboard bump message" )
223+ logger .exception ("Failed handling possible bump reminder message" )
286224
287225 @commands .Cog .listener ()
288226 async def on_message_edit (self , before : discord .Message , after : discord .Message ) -> None :
289- # Disboard frequently edits the confirmation message; handle edits too.
227+ # Some apps may edit the confirmation message; handle edits too.
290228 if after .author and after .author .id == getattr (self .bot .user , "id" , None ):
291229 return
292230 try :
293- self ._record_bump_invocation (after )
294- await self ._handle_possible_disboard_bump (after )
231+ await self ._handle_possible_bump_reminder_bump (after )
295232 except Exception :
296- logger .exception ("Failed handling edited Disboard bump message" )
233+ logger .exception ("Failed handling edited bump reminder message" )
297234
298235 # ----------------------------
299236 # Persistence helpers
0 commit comments