"""Discord ingestion via a discord.py bot. Scope is enforced by Discord itself, not by this code: a bot can only see guilds it has been invited to, and cannot read personal DMs at all. There is deliberately no selfbot path here — that is an explicit ToS violation with real ban risk (docs/project-plan.md Phase 12 step 4). The bot needs the Message Content privileged intent enabled at https://discord.com/developers/applications -> your app -> Bot -> Privileged Gateway Intents, otherwise every message body arrives empty. Bots have no read-state on Discord, so reading history acknowledges nothing. """ import asyncio import logging import os from datetime import datetime, timedelta, timezone LOG = logging.getLogger(__name__) MAX_BODY_CHARS = 2000 async def _fetch_async(lookback_hours, token, max_per_channel, connect_timeout): import discord since = datetime.now(timezone.utc) - timedelta(hours=lookback_hours) messages = [] intents = discord.Intents.default() intents.message_content = True intents.guilds = True client = discord.Client(intents=intents) @client.event async def on_ready(): try: for guild in client.guilds: for channel in guild.text_channels: permissions = channel.permissions_for(guild.me) if not (permissions.read_messages and permissions.read_message_history): continue try: async for message in channel.history(after=since, limit=max_per_channel): if not message.content: continue messages.append( { "source": "discord", "guild": guild.name, "channel": channel.name, "from": message.author.display_name, "timestamp": message.created_at.isoformat(), "body": message.content[:MAX_BODY_CHARS], } ) except Exception: LOG.warning( "discord: could not read #%s in %s, skipping", channel.name, guild.name, exc_info=True, ) finally: await client.close() await asyncio.wait_for(client.start(token), timeout=connect_timeout) return messages def fetch(lookback_hours): token = os.environ.get("DISCORD_BOT_TOKEN", "").strip() max_per_channel = int(os.environ.get("DISCORD_MAX_MESSAGES_PER_CHANNEL", "50")) connect_timeout = float(os.environ.get("DISCORD_TIMEOUT", "120")) if not token: LOG.warning("discord: DISCORD_BOT_TOKEN not set, skipping") return [] try: messages = asyncio.run( _fetch_async(lookback_hours, token, max_per_channel, connect_timeout) ) except Exception: LOG.warning("discord: ingestion failed, returning nothing", exc_info=True) return [] LOG.info("discord: %d message(s) in the last %sh", len(messages), lookback_hours) return messages