12def load_messages(path):
14 with open(path,
"r", encoding=
"utf-8")
as fh:
16 except FileNotFoundError:
17 print(f
"[discord] messages file not found: {path}; skipping")
19 except json.JSONDecodeError
as exc:
20 print(f
"[discord] invalid JSON in {path}: {exc}; skipping")
23 if not isinstance(data, list):
24 print(f
"[discord] expected list in {path}; skipping")
29 if isinstance(entry, dict)
and isinstance(entry.get(
"content"), str)
and entry[
"content"].strip():
30 filtered.append({
"content": entry[
"content"]})
34def post_message(webhook, payload):
35 body = json.dumps(payload).encode(
"utf-8")
36 request = urllib.request.Request(
39 headers={
"Content-Type":
"application/json"},
42 with urllib.request.urlopen(request, timeout=20)
as response:
43 return response.status
46def parse_retry_after_seconds(headers, details):
47 retry_after = headers.get(
"Retry-After")
if headers
else None
50 return max(float(retry_after), 0.0)
55 data = json.loads(details)
56 if isinstance(data, dict)
and "retry_after" in data:
57 value = float(data[
"retry_after"])
58 return max(value, 0.0)
66 parser = argparse.ArgumentParser()
67 parser.add_argument(
"--webhook", required=
True)
68 parser.add_argument(
"--messages-file", required=
True)
69 parser.add_argument(
"--label", default=
"messages")
70 parser.add_argument(
"--max-messages", type=int, default=5)
71 parser.add_argument(
"--delay-seconds", type=float, default=1.5)
72 parser.add_argument(
"--max-retries", type=int, default=3)
73 args = parser.parse_args()
75 block_file = os.environ.get(
"DISCORD_BLOCK_FILE",
".discord_webhook_blocked")
76 if os.path.exists(block_file):
77 print(f
"[discord] webhook is marked blocked ({block_file}); skipping {args.label}")
80 messages = load_messages(args.messages_file)
82 print(f
"[discord] no {args.label} messages to send")
85 if args.max_messages > 0
and len(messages) > args.max_messages:
87 f
"[discord] limiting {args.label} messages from {len(messages)} to {args.max_messages}",
89 messages = messages[: args.max_messages]
91 for idx, payload
in enumerate(messages, start=1):
96 status = post_message(args.webhook, payload)
98 print(f
"[discord] {args.label} message {idx} failed with status {status}; skipping remainder")
101 except urllib.error.HTTPError
as exc:
102 details = exc.read().decode(
"utf-8", errors=
"replace")
104 if exc.code
in (401, 403, 404):
105 with open(block_file,
"w", encoding=
"utf-8")
as fh:
106 fh.write(f
"http={exc.code}\n")
109 f
"[discord] {args.label} message {idx} HTTP error {exc.code}: {details}; skipping remainder",
113 if exc.code == 429
and attempt <= args.max_retries:
114 wait_seconds = parse_retry_after_seconds(exc.headers, details)
116 f
"[discord] {args.label} message {idx} rate limited; retrying in {wait_seconds:.2f}s (attempt {attempt}/{args.max_retries})",
118 time.sleep(wait_seconds)
121 if 500 <= exc.code < 600
and attempt <= args.max_retries:
122 wait_seconds = min(2.0 * attempt, 8.0)
124 f
"[discord] {args.label} message {idx} server error {exc.code}; retrying in {wait_seconds:.2f}s (attempt {attempt}/{args.max_retries})",
126 time.sleep(wait_seconds)
130 f
"[discord] {args.label} message {idx} HTTP error {exc.code}: {details}; skipping remainder",
133 except Exception
as exc:
134 if attempt <= args.max_retries:
135 wait_seconds = min(1.5 * attempt, 6.0)
137 f
"[discord] {args.label} message {idx} error: {exc}; retrying in {wait_seconds:.2f}s (attempt {attempt}/{args.max_retries})",
139 time.sleep(wait_seconds)
142 print(f
"[discord] {args.label} message {idx} error: {exc}; skipping remainder")
145 time.sleep(max(args.delay_seconds, 0.0))
147 print(f
"[discord] sent {len(messages)} {args.label} message(s)")
151if __name__ ==
"__main__":