156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
from AppConfig import readDiscordLog, writeDiscordLog
|
|
|
|
|
|
discordApiBase = "https://discord.com/api/v10"
|
|
discordWebhookUrl = os.getenv("DISCORD_WEBHOOK_URL")
|
|
|
|
|
|
class DiscordGatewayError(Exception):
|
|
pass
|
|
|
|
|
|
class DiscordGateway:
|
|
"""Handles Discord IO while staying easy to stub."""
|
|
|
|
dmChannelCache = {}
|
|
|
|
@staticmethod
|
|
def getMessages(userId, howMany, oldestFirst=False):
|
|
if DiscordGateway.canUseApi() and userId:
|
|
try:
|
|
channelId = DiscordGateway.ensureDmChannel(userId)
|
|
apiMessages = DiscordGateway.fetchChannelMessages(channelId, howMany)
|
|
if oldestFirst:
|
|
apiMessages = list(reversed(apiMessages))
|
|
return [DiscordGateway.describeApiMessage(message) for message in apiMessages]
|
|
except DiscordGatewayError as error:
|
|
print(f"[discord] API getMessages failed: {error}")
|
|
# fallback to local log
|
|
records = readDiscordLog()
|
|
if userId:
|
|
records = [record for record in records if record.get("userId") == userId]
|
|
if not oldestFirst:
|
|
records = list(reversed(records))
|
|
limited = records[:howMany]
|
|
return [DiscordGateway.describeLogRecord(record) for record in limited]
|
|
|
|
@staticmethod
|
|
def sendMessage(userId, content):
|
|
if discordWebhookUrl:
|
|
DiscordGateway.postViaWebhook(content)
|
|
DiscordGateway.persistLog(userId or "webhook", content)
|
|
print("[discord] Sent message via webhook")
|
|
return
|
|
if DiscordGateway.canUseApi():
|
|
try:
|
|
channelId = DiscordGateway.ensureDmChannel(userId)
|
|
DiscordGateway.postChannelMessage(channelId, content)
|
|
DiscordGateway.persistLog(userId, content)
|
|
print(f"[discord] Sent DM via API to {userId}")
|
|
return
|
|
except DiscordGatewayError as error:
|
|
print(f"[discord] API sendMessage failed: {error}")
|
|
DiscordGateway.persistLog(userId, content)
|
|
print(f"[discord] (log) DM to {userId}: {content}")
|
|
|
|
@staticmethod
|
|
def canUseApi():
|
|
return bool(os.getenv("DISCORD_BOT_TOKEN"))
|
|
|
|
@staticmethod
|
|
def ensureDmChannel(userId):
|
|
if not userId:
|
|
raise DiscordGatewayError("User ID is required for DM channel")
|
|
cacheKey = str(userId)
|
|
if cacheKey in DiscordGateway.dmChannelCache:
|
|
return DiscordGateway.dmChannelCache[cacheKey]
|
|
payload = json.dumps({"recipient_id": cacheKey}).encode("utf-8")
|
|
data = DiscordGateway.apiRequest("POST", "/users/@me/channels", payload)
|
|
channelId = data.get("id")
|
|
if not channelId:
|
|
raise DiscordGatewayError("Discord API did not return a channel id")
|
|
DiscordGateway.dmChannelCache[cacheKey] = channelId
|
|
return channelId
|
|
|
|
@staticmethod
|
|
def fetchChannelMessages(channelId, limit):
|
|
params = urllib.parse.urlencode({"limit": limit})
|
|
path = f"/channels/{channelId}/messages?{params}"
|
|
return DiscordGateway.apiRequest("GET", path)
|
|
|
|
@staticmethod
|
|
def postChannelMessage(channelId, content):
|
|
payload = json.dumps({"content": content}).encode("utf-8")
|
|
DiscordGateway.apiRequest("POST", f"/channels/{channelId}/messages", payload)
|
|
|
|
@staticmethod
|
|
def apiRequest(method, path, payload=None):
|
|
token = os.getenv("DISCORD_BOT_TOKEN")
|
|
if not token:
|
|
raise DiscordGatewayError("DISCORD_BOT_TOKEN is not set")
|
|
url = f"{discordApiBase}{path}"
|
|
headers = {
|
|
"Authorization": f"Bot {token}",
|
|
}
|
|
if payload is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
request = urllib.request.Request(url, data=payload, method=method, headers=headers)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=20) as response:
|
|
body = response.read().decode("utf-8")
|
|
if not body:
|
|
return {}
|
|
return json.loads(body)
|
|
except urllib.error.HTTPError as error:
|
|
responseBody = error.read().decode("utf-8") if hasattr(error, "read") else ""
|
|
raise DiscordGatewayError(f"HTTP {error.code}: {responseBody or str(error)}")
|
|
except urllib.error.URLError as error:
|
|
raise DiscordGatewayError(str(error))
|
|
|
|
@staticmethod
|
|
def describeApiMessage(message):
|
|
author = message.get("author") or {}
|
|
authorName = author.get("username") or author.get("id") or "unknown"
|
|
content = message.get("content") or ""
|
|
return f"{authorName}: {content}"
|
|
|
|
@staticmethod
|
|
def describeLogRecord(record):
|
|
userPart = record.get("userId") or "unknown"
|
|
contentPart = record.get("content") or ""
|
|
return f"{userPart}: {contentPart}"
|
|
|
|
@staticmethod
|
|
def persistLog(userId, content):
|
|
entry = {
|
|
"userId": userId,
|
|
"content": content,
|
|
}
|
|
records = readDiscordLog()
|
|
records.append(entry)
|
|
writeDiscordLog(records)
|
|
|
|
@staticmethod
|
|
def postViaWebhook(content):
|
|
payload = json.dumps({"content": content}).encode("utf-8")
|
|
request = urllib.request.Request(
|
|
discordWebhookUrl,
|
|
data=payload,
|
|
method="POST",
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=20):
|
|
return
|
|
except urllib.error.HTTPError as error:
|
|
body = error.read().decode("utf-8") if hasattr(error, "read") else ""
|
|
raise DiscordGatewayError(f"Webhook HTTP {error.code}: {body or str(error)}")
|
|
except urllib.error.URLError as error:
|
|
raise DiscordGatewayError(f"Webhook error: {error}")
|