SmartestHome/digest-engine/ingest/discord_ingest.py

91 lines
3.3 KiB
Python

"""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