91 lines
3.3 KiB
Python
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
|