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
|
return content
|
||||||
|
|
||||||
|
|
||||||
def apply_mention_policy(content: str, policy: dict, author_id: int, exempt_users: list) -> str:
|
def apply_mention_policy(
|
||||||
"""Apply granular mention policy — blocked mentions are stripped entirely."""
|
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:
|
if author_id in exempt_users:
|
||||||
return content
|
return content
|
||||||
if not policy.get("allow_everyone", False):
|
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):
|
if not policy.get("allow_role_mentions", False):
|
||||||
content = re.sub(r"<@&\d+>\s?", "", content)
|
content = re.sub(r"<@&\d+>\s?", "", content)
|
||||||
if not policy.get("allow_user_mentions", False):
|
if not policy.get("allow_user_mentions", False):
|
||||||
|
# Network-wide: strip ALL user mentions
|
||||||
content = re.sub(r"<@!?\d+>\s?", "", content)
|
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
|
# Collapse any leftover double-spaces from removals
|
||||||
content = re.sub(r" +", " ", content).strip()
|
content = re.sub(r" +", " ", content).strip()
|
||||||
return content
|
return content
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,7 @@ _DEFAULT_NETWORK = {
|
||||||
},
|
},
|
||||||
"server_mention_overrides": {}, # {guild_id_str: {same keys as mention_policy}}
|
"server_mention_overrides": {}, # {guild_id_str: {same keys as mention_policy}}
|
||||||
"mention_exempt_users": [], # user IDs allowed to bypass 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 / Terms of Service acceptance gate
|
||||||
"rules_required": False, # must users accept before talking?
|
"rules_required": False, # must users accept before talking?
|
||||||
"rules_text": "", # the legal ToS text
|
"rules_text": "", # the legal ToS text
|
||||||
|
|
@ -405,19 +406,19 @@ class Wormhole(commands.Cog):
|
||||||
async def _init(self):
|
async def _init(self):
|
||||||
try:
|
try:
|
||||||
await self.bot.wait_until_ready()
|
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:
|
async with self.config.networks() as nets:
|
||||||
for name, data in nets.items():
|
for name, data in nets.items():
|
||||||
mp = data.setdefault("mention_policy", {})
|
mp = data.setdefault("mention_policy", {})
|
||||||
changed = False
|
changed = False
|
||||||
for key in ("allow_user_mentions", "allow_role_mentions", "allow_everyone", "allow_here"):
|
# One-time migration: if no _mention_migrated flag, force all off
|
||||||
if mp.get(key) is not False:
|
if not data.get("_mention_migrated"):
|
||||||
|
for key in ("allow_user_mentions", "allow_role_mentions", "allow_everyone", "allow_here"):
|
||||||
mp[key] = False
|
mp[key] = False
|
||||||
changed = True
|
data["_mention_migrated"] = True
|
||||||
mc = data.get("mention_control", {})
|
|
||||||
if mc and not mc.get("strip_everyone"):
|
|
||||||
mc["strip_everyone"] = True
|
|
||||||
changed = True
|
changed = True
|
||||||
|
# Ensure optout list exists
|
||||||
|
data.setdefault("mention_optout_users", [])
|
||||||
if changed:
|
if changed:
|
||||||
log.info("Wormhole migration: disabled all mentions for network '%s'", name)
|
log.info("Wormhole migration: disabled all mentions for network '%s'", name)
|
||||||
networks = await self.config.networks()
|
networks = await self.config.networks()
|
||||||
|
|
@ -3021,7 +3022,7 @@ class Wormhole(commands.Cog):
|
||||||
mp = d.get("mention_policy", {})
|
mp = d.get("mention_policy", {})
|
||||||
sym = lambda v: "✅" if v else "❌"
|
sym = lambda v: "✅" if v else "❌"
|
||||||
lines = [
|
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"**@role mentions:** {sym(mp.get('allow_role_mentions', False))}",
|
||||||
f"**@everyone:** {sym(mp.get('allow_everyone', False))}",
|
f"**@everyone:** {sym(mp.get('allow_everyone', False))}",
|
||||||
f"**@here:** {sym(mp.get('allow_here', False))}",
|
f"**@here:** {sym(mp.get('allow_here', False))}",
|
||||||
|
|
@ -3029,7 +3030,12 @@ class Wormhole(commands.Cog):
|
||||||
exempt = d.get("mention_exempt_users", [])
|
exempt = d.get("mention_exempt_users", [])
|
||||||
if exempt:
|
if exempt:
|
||||||
names = [str(self.bot.get_user(uid) or uid) for uid in exempt[:10]]
|
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", {})
|
overrides = d.get("server_mention_overrides", {})
|
||||||
if overrides:
|
if overrides:
|
||||||
lines.append(f"\n**Server overrides:** {len(overrides)} server(s)")
|
lines.append(f"\n**Server overrides:** {len(overrides)} server(s)")
|
||||||
|
|
@ -3040,6 +3046,63 @@ class Wormhole(commands.Cog):
|
||||||
lines.append(f" • {gname}: {ov_lines}")
|
lines.append(f" • {gname}: {ov_lines}")
|
||||||
await ctx.send(embed=info_embed("\n".join(lines), title=f"Mention Policy — {name}"))
|
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
|
# 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))
|
server_overrides = nd.get("server_mention_overrides", {}).get(str(message.guild.id))
|
||||||
active_policy = server_overrides if server_overrides else mp
|
active_policy = server_overrides if server_overrides else mp
|
||||||
exempt = nd.get("mention_exempt_users", [])
|
exempt = nd.get("mention_exempt_users", [])
|
||||||
|
optouts = set(nd.get("mention_optout_users", []))
|
||||||
if active_policy:
|
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:
|
else:
|
||||||
mc = nd.get("mention_control", {})
|
mc = nd.get("mention_control", {})
|
||||||
content = sanitise_mentions(message.content or "", mc)
|
content = sanitise_mentions(message.content or "", mc)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue