Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 0 additions & 133 deletions plugins/platforms/discord/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@

logger = logging.getLogger(__name__)

# Module-level guard for lazily creating each adapter's per-instance reaction-seq
# lock (the lazy-init itself must be race-free across the journal thread pool).
_REACTION_SEQ_INIT_LOCK = threading.Lock()
_DISCORD_MARKDOWN_LINK_LABEL_RE = re.compile(r"([\\\[\]])")
_DISCORD_URL_LABEL_SCHEME_RE = re.compile(r"^https?://", re.IGNORECASE)

Expand Down Expand Up @@ -1731,22 +1728,6 @@ async def connect(self, *, is_reconnect: bool = False) -> bool:
)
intents.voice_states = True

# Reaction journal (opt-in): only request the (non-privileged)
# reactions intent when a journal path is configured. This lets a
# downstream consumer durably capture raw reaction transitions
# (reaction_state / seed_triage). Default off → no behavior change
# and no extra gateway traffic for anyone who hasn't opted in.
# Env-driven like the rest of this adapter: config.yaml
# ``discord.reaction_journal`` is translated to DISCORD_REACTION_JOURNAL
# by _apply_yaml_config (the apply_yaml_config_fn hook).
self._reaction_journal_path = (
os.getenv("DISCORD_REACTION_JOURNAL")
or self.config.extra.get("reaction_journal")
or None
)
if self._reaction_journal_path:
intents.reactions = True

# Resolve proxy (DISCORD_PROXY > generic env vars > macOS system proxy)
from gateway.platforms.base import resolve_proxy_url, proxy_kwargs_for_bot
proxy_url = resolve_proxy_url(platform_env_var="DISCORD_PROXY")
Expand Down Expand Up @@ -1873,20 +1854,6 @@ async def on_voice_state_update(member, before, after):
guild_id,
)

# Reaction journal (opt-in via discord.reaction_journal): append one
# JSON line per RAW reaction transition. Raw events fire regardless of
# message cache, so a reaction on an old/un-cached card is still
# captured. Schema matches the reaction_state core's journal contract:
# {channel_id, message_id, emoji, user_id, action, seq, ts}.
if self._reaction_journal_path:
@self._client.event
async def on_raw_reaction_add(payload):
await adapter_self._emit_reaction_journal(payload, "add")

@self._client.event
async def on_raw_reaction_remove(payload):
await adapter_self._emit_reaction_journal(payload, "remove")

# Register slash commands
if self._slash_commands:
self._register_slash_commands()
Expand Down Expand Up @@ -4037,98 +4004,6 @@ def _reactions_enabled(self) -> bool:
"""Check if message reactions are enabled via config/env."""
return os.getenv("DISCORD_REACTIONS", "true").lower() not in {"false", "0", "no"}

def _next_reaction_seq(self) -> int:
"""Monotonic per-key-safe sequence for journal events. Seeded once from
the existing journal's last seq so a gateway restart never rewinds the
counter (which would make the core reject post-restart events as stale).

Thread-safe: called from a thread pool (via run_in_executor), so the
read-increment-write is guarded by a lock — otherwise two concurrent
burst events could mint the SAME seq and the core would drop one as a
duplicate/stale (silent loss)."""
lock = getattr(self, "_reaction_seq_lock", None)
if lock is None:
# First-call init is itself racy; create the lock under a class-level
# guard so all threads converge on one lock instance.
with _REACTION_SEQ_INIT_LOCK:
lock = getattr(self, "_reaction_seq_lock", None)
if lock is None:
lock = threading.Lock()
self._reaction_seq_lock = lock
with lock:
seq = getattr(self, "_reaction_seq", None)
if seq is None:
seq = 0
path = getattr(self, "_reaction_journal_path", None)
try:
if path and os.path.exists(path):
with open(path, "r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
if not isinstance(obj, dict):
continue # a non-dict line carries no seq
s = int(obj.get("seq", 0))
if s > seq:
seq = s
except (ValueError, TypeError, AttributeError):
# malformed line / non-int seq — skip it, never
# let one bad line abort seeding (which would
# leave _reaction_seq unset, silencing writes).
continue
except OSError:
seq = 0
seq += 1
self._reaction_seq = seq
return seq

async def _emit_reaction_journal(self, payload: Any, action: str) -> None:
"""Async wrapper: offload the blocking journal write to a thread so a
burst of reaction events can never stall the gateway's asyncio loop."""
try:
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None, self._append_reaction_journal, payload, action)
except Exception as e: # noqa: BLE001 - never let it bubble into the loop
logger.debug("[%s] reaction-journal emit soft-fail: %s", self.name, e)

def _append_reaction_journal(self, payload: Any, action: str) -> None:
"""Append one raw reaction transition to the configured journal, in the
reaction_state core's schema. Best-effort: a journal hiccup must never
crash the gateway's event loop (this runs inside a discord.py handler)."""
path = getattr(self, "_reaction_journal_path", None)
if not path:
return
try:
emoji = getattr(payload, "emoji", None)
# Standard emoji -> unicode char; custom -> "name:id" (matches the
# canonical Discord form the core stores verbatim).
emoji_id = getattr(emoji, "id", None)
if emoji_id:
emoji_str = f"{getattr(emoji, 'name', '')}:{emoji_id}"
else:
emoji_str = str(getattr(emoji, "name", emoji) or "")
event = {
"channel_id": str(getattr(payload, "channel_id", "")),
"message_id": str(getattr(payload, "message_id", "")),
"emoji": emoji_str,
"user_id": str(getattr(payload, "user_id", "")),
"action": action,
"seq": self._next_reaction_seq(),
"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
# makedirs once per process (the dir persists after the first write).
if not getattr(self, "_reaction_journal_dir_created", False):
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
self._reaction_journal_dir_created = True
with open(path, "a", encoding="utf-8") as fh:
fh.write(json.dumps(event, ensure_ascii=False) + "\n")
except Exception as e: # noqa: BLE001 - never let a journal error kill the loop
logger.debug("[%s] reaction-journal append soft-fail: %s", self.name, e)

async def on_processing_start(self, event: MessageEvent) -> None:
"""Add an in-progress reaction and record durable handling state."""
message = event.raw_message
Expand Down Expand Up @@ -12027,14 +11902,6 @@ def _apply_yaml_config(yaml_cfg: dict, discord_cfg: dict) -> dict | None:
seeded_extra["ignored_channels"] = str(ic)
if not _skip_env_bridge and not os.getenv("DISCORD_IGNORED_CHANNELS"):
os.environ["DISCORD_IGNORED_CHANNELS"] = str(ic)
# reaction_journal: opt-in path to append raw reaction transitions to, in the
# reaction_state core's journal schema (durable triage state). Empty/unset =
# feature off (no reactions intent requested).
rj = discord_cfg.get("reaction_journal")
if rj is not None:
seeded_extra["reaction_journal"] = str(rj)
if not _skip_env_bridge and not os.getenv("DISCORD_REACTION_JOURNAL"):
os.environ["DISCORD_REACTION_JOURNAL"] = str(rj)
# allowed_channels: if set, bot ONLY responds in these channels (whitelist)
ac = discord_cfg.get("allowed_channels")
if ac is not None:
Expand Down
216 changes: 0 additions & 216 deletions tests/gateway/test_discord_reaction_journal.py

This file was deleted.

Loading