feat: per-network mention config + user opt-in/out for pings
- Migration now uses a _mention_migrated flag so it only forces mentions off once. After that, admins can freely toggle per-network settings with 'wh mentions set <network> <type> <true/false>' - New mention_optout_users list per network: users can opt out of being @pinged through the relay even when user mentions are enabled network-wide. Their <@ID> mentions are stripped; everyone else's pass through. Default: opted in. - New commands: • wh mentions optout <network> — stop receiving relay pings • wh mentions optin <network> — start receiving relay pings again • wh mentions mystatus <network> — check your current ping status - apply_mention_policy() now accepts optout_users set and selectively strips only opted-out users' mentions when user mentions are globally allowed - Updated 'wh mentions status' to show opt-out count and correct defaults
This commit is contained in:
parent
8c5b422d83
commit
bbf427284e
2 changed files with 94 additions and 12 deletions
|
|
@ -225,8 +225,19 @@ def sanitise_mentions(content: str, config: dict) -> str:
|
|||
return content
|
||||
|
||||
|
||||
def apply_mention_policy(content: str, policy: dict, author_id: int, exempt_users: list) -> str:
|
||||
"""Apply granular mention policy — blocked mentions are stripped entirely."""
|
||||
def apply_mention_policy(
|
||||
content: str,
|
||||
policy: dict,
|
||||
author_id: int,
|
||||
exempt_users: list,
|
||||
optout_users: Optional[set] = None,
|
||||
) -> str:
|
||||
"""Apply granular mention policy — blocked mentions are stripped entirely.
|
||||
|
||||
*optout_users*: set of user IDs who opted out of being pinged.
|
||||
When user mentions are globally allowed, individual opted-out users'
|
||||
mentions are still stripped.
|
||||
"""
|
||||
if author_id in exempt_users:
|
||||
return content
|
||||
if not policy.get("allow_everyone", False):
|
||||
|
|
@ -236,7 +247,14 @@ def apply_mention_policy(content: str, policy: dict, author_id: int, exempt_user
|
|||
if not policy.get("allow_role_mentions", False):
|
||||
content = re.sub(r"<@&\d+>\s?", "", content)
|
||||
if not policy.get("allow_user_mentions", False):
|
||||
# Network-wide: strip ALL user mentions
|
||||
content = re.sub(r"<@!?\d+>\s?", "", content)
|
||||
elif optout_users:
|
||||
# User mentions allowed network-wide, but strip opted-out users
|
||||
def _strip_optout(m: re.Match) -> str:
|
||||
uid = int(m.group(1))
|
||||
return "" if uid in optout_users else m.group(0)
|
||||
content = re.sub(r"<@!?(\d+)>", _strip_optout, content)
|
||||
# Collapse any leftover double-spaces from removals
|
||||
content = re.sub(r" +", " ", content).strip()
|
||||
return content
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ _DEFAULT_NETWORK = {
|
|||
},
|
||||
"server_mention_overrides": {}, # {guild_id_str: {same keys as mention_policy}}
|
||||
"mention_exempt_users": [], # user IDs allowed to bypass mention policy
|
||||
"mention_optout_users": [], # user IDs who opted out of being @pinged via relay
|
||||
# Rules / Terms of Service acceptance gate
|
||||
"rules_required": False, # must users accept before talking?
|
||||
"rules_text": "", # the legal ToS text
|
||||
|
|
@ -405,19 +406,19 @@ class Wormhole(commands.Cog):
|
|||
async def _init(self):
|
||||
try:
|
||||
await self.bot.wait_until_ready()
|
||||
# ── Migration: force all mentions stripped on existing networks ──
|
||||
# ── Migration v3.4.3: ensure mention_policy defaults + optout list ──
|
||||
async with self.config.networks() as nets:
|
||||
for name, data in nets.items():
|
||||
mp = data.setdefault("mention_policy", {})
|
||||
changed = False
|
||||
for key in ("allow_user_mentions", "allow_role_mentions", "allow_everyone", "allow_here"):
|
||||
if mp.get(key) is not False:
|
||||
# One-time migration: if no _mention_migrated flag, force all off
|
||||
if not data.get("_mention_migrated"):
|
||||
for key in ("allow_user_mentions", "allow_role_mentions", "allow_everyone", "allow_here"):
|
||||
mp[key] = False
|
||||
changed = True
|
||||
mc = data.get("mention_control", {})
|
||||
if mc and not mc.get("strip_everyone"):
|
||||
mc["strip_everyone"] = True
|
||||
data["_mention_migrated"] = True
|
||||
changed = True
|
||||
# Ensure optout list exists
|
||||
data.setdefault("mention_optout_users", [])
|
||||
if changed:
|
||||
log.info("Wormhole migration: disabled all mentions for network '%s'", name)
|
||||
networks = await self.config.networks()
|
||||
|
|
@ -3021,7 +3022,7 @@ class Wormhole(commands.Cog):
|
|||
mp = d.get("mention_policy", {})
|
||||
sym = lambda v: "✅" if v else "❌"
|
||||
lines = [
|
||||
f"**@user mentions:** {sym(mp.get('allow_user_mentions', True))}",
|
||||
f"**@user mentions:** {sym(mp.get('allow_user_mentions', False))}",
|
||||
f"**@role mentions:** {sym(mp.get('allow_role_mentions', False))}",
|
||||
f"**@everyone:** {sym(mp.get('allow_everyone', False))}",
|
||||
f"**@here:** {sym(mp.get('allow_here', False))}",
|
||||
|
|
@ -3029,7 +3030,12 @@ class Wormhole(commands.Cog):
|
|||
exempt = d.get("mention_exempt_users", [])
|
||||
if exempt:
|
||||
names = [str(self.bot.get_user(uid) or uid) for uid in exempt[:10]]
|
||||
lines.append(f"\n**Exempt users:** {', '.join(names)}")
|
||||
lines.append(f"\n**Exempt users (bypass policy):** {', '.join(names)}")
|
||||
optouts = d.get("mention_optout_users", [])
|
||||
if optouts:
|
||||
names = [str(self.bot.get_user(uid) or uid) for uid in optouts[:10]]
|
||||
extra = f" (+{len(optouts)-10} more)" if len(optouts) > 10 else ""
|
||||
lines.append(f"\n**Opted out of pings ({len(optouts)}):** {', '.join(names)}{extra}")
|
||||
overrides = d.get("server_mention_overrides", {})
|
||||
if overrides:
|
||||
lines.append(f"\n**Server overrides:** {len(overrides)} server(s)")
|
||||
|
|
@ -3040,6 +3046,63 @@ class Wormhole(commands.Cog):
|
|||
lines.append(f" • {gname}: {ov_lines}")
|
||||
await ctx.send(embed=info_embed("\n".join(lines), title=f"Mention Policy — {name}"))
|
||||
|
||||
@wh_mentions.command(name="optout")
|
||||
async def wh_mentions_optout(self, ctx, name: str):
|
||||
"""Opt out of being @pinged through this network's relay.
|
||||
|
||||
Your mentions will be stripped from relayed messages so you won't
|
||||
receive cross-server pings. Use `wh mentions optin` to reverse.
|
||||
"""
|
||||
name = name.lower(); d = await self._net(name)
|
||||
if not d: return await ctx.send(embed=err_embed("Network not found."))
|
||||
uid = ctx.author.id
|
||||
async with self.config.networks() as n:
|
||||
optouts = n[name].setdefault("mention_optout_users", [])
|
||||
if uid in optouts:
|
||||
return await ctx.send(embed=info_embed("You're already opted out of pings on this network."))
|
||||
optouts.append(uid)
|
||||
await ctx.send(embed=ok_embed(
|
||||
f"**{ctx.author.display_name}**, you've opted out of relay pings on **{name}**.\n"
|
||||
"Your @mentions will be stripped from relayed messages. Use `wh mentions optin` to reverse."
|
||||
))
|
||||
|
||||
@wh_mentions.command(name="optin")
|
||||
async def wh_mentions_optin(self, ctx, name: str):
|
||||
"""Opt back in to receiving @pings through this network's relay.
|
||||
|
||||
Only works if user mentions are enabled for the network.
|
||||
"""
|
||||
name = name.lower(); d = await self._net(name)
|
||||
if not d: return await ctx.send(embed=err_embed("Network not found."))
|
||||
uid = ctx.author.id
|
||||
async with self.config.networks() as n:
|
||||
optouts = n[name].setdefault("mention_optout_users", [])
|
||||
if uid not in optouts:
|
||||
return await ctx.send(embed=info_embed("You're already opted in to pings on this network."))
|
||||
optouts.remove(uid)
|
||||
await ctx.send(embed=ok_embed(
|
||||
f"**{ctx.author.display_name}**, you've opted back in to relay pings on **{name}**."
|
||||
))
|
||||
|
||||
@wh_mentions.command(name="mystatus")
|
||||
async def wh_mentions_mystatus(self, ctx, name: str):
|
||||
"""Check your personal mention opt-in/out status for a network."""
|
||||
name = name.lower(); d = await self._net(name)
|
||||
if not d: return await ctx.send(embed=err_embed("Network not found."))
|
||||
uid = ctx.author.id
|
||||
mp = d.get("mention_policy", {})
|
||||
optouts = d.get("mention_optout_users", [])
|
||||
exempt = d.get("mention_exempt_users", [])
|
||||
if uid in exempt:
|
||||
status = "🛡️ You are **exempt** — your mentions always pass through."
|
||||
elif not mp.get("allow_user_mentions", False):
|
||||
status = "🔇 All user mentions are **disabled network-wide** — opt-in/out has no effect."
|
||||
elif uid in optouts:
|
||||
status = "🔕 You are **opted out** — your @mentions are stripped from relayed messages."
|
||||
else:
|
||||
status = "🔔 You are **opted in** (default) — your @mentions are relayed normally."
|
||||
await ctx.send(embed=info_embed(status, title=f"Your Ping Status — {name}"))
|
||||
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# PHASE 5 — RULES / TERMS OF SERVICE ACCEPTANCE
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
|
@ -4119,8 +4182,9 @@ class Wormhole(commands.Cog):
|
|||
server_overrides = nd.get("server_mention_overrides", {}).get(str(message.guild.id))
|
||||
active_policy = server_overrides if server_overrides else mp
|
||||
exempt = nd.get("mention_exempt_users", [])
|
||||
optouts = set(nd.get("mention_optout_users", []))
|
||||
if active_policy:
|
||||
content = apply_mention_policy(message.content or "", active_policy, message.author.id, exempt)
|
||||
content = apply_mention_policy(message.content or "", active_policy, message.author.id, exempt, optouts)
|
||||
else:
|
||||
mc = nd.get("mention_control", {})
|
||||
content = sanitise_mentions(message.content or "", mc)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue