From bc89d48a4123a01481e7fdbcad87bf0540205043 Mon Sep 17 00:00:00 2001 From: Jona Heitzer Date: Sat, 18 Oct 2025 22:26:16 +0200 Subject: [PATCH] Initial commit --- .gitignore | 6 ++ Dockerfile | 10 +++ docker-compose.yaml | 39 ++++++++++++ irc-topic-bot.py | 149 ++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 21 +++++++ 5 files changed, 225 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker-compose.yaml create mode 100644 irc-topic-bot.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d61f46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +gluetun +bin +include +lib +pyvenv.cfg diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65935db --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3 + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD [ "python", "./irc-topic-bot.py" ] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c9e233c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,39 @@ +services: + gluetun: + image: qmcgaw/gluetun + container_name: gluentun + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + environment: + - VPN_SERVICE_PROVIDER=airvpn + - SERVER_COUNTRIES=Germany + volumes: + - ./gluetun:/gluetun + restart: always + irc-topic-bot: + build: + context: . + dockerfile: Dockerfile + environment: + DEBUG_LOG: "${DEBUG_LOG}" + IRC_SERVER: "${IRC_SERVER}" + IRC_PORT: "${IRC_PORT}" + IRC_USE_TLS: "${IRC_USE_TLS}" + IRC_NICK: "${IRC_NICK}" + IRC_CHANNEL: "${IRC_CHANNEL}" + TOPIC_REGEX: "${TOPIC_REGEX}" + SMTP_SERVER: "${SMTP_SERVER}" + SMTP_PORT: "${SMTP_PORT}" + SMTP_USER: "${SMTP_USER}" + SMTP_PASS: "${SMTP_PASS}" + EMAIL_TO: "${EMAIL_TO}" + MIN_WAIT_TIME: "${MIN_WAIT_TIME}" + MAX_WAIT_TIME: "${MAX_WAIT_TIME}" + MIN_SLEEP_TIME: "${MIN_SLEEP_TIME}" + MAX_SLEEP_TIME: "${MAX_SLEEP_TIME}" + restart: unless-stopped + depends_on: + - gluetun + network_mode: "service:gluetun" diff --git a/irc-topic-bot.py b/irc-topic-bot.py new file mode 100644 index 0000000..99e373b --- /dev/null +++ b/irc-topic-bot.py @@ -0,0 +1,149 @@ +import os +import re +import requests +import smtplib +import socket +import ssl +import random +import time + +from datetime import datetime +from email.mime.text import MIMEText + +DEBUG_LOG = os.environ["DEBUG_LOG"].lower() == "true" + +IRC_SERVER = os.environ["IRC_SERVER"] +IRC_PORT = int(os.environ["IRC_PORT"]) +USE_TLS = os.environ["IRC_USE_TLS"].lower() == "true" +IRC_NICK = os.environ["IRC_NICK"] +IRC_USER = IRC_NICK +IRC_REALNAME = IRC_NICK +IRC_CHANNEL = os.environ["IRC_CHANNEL"] + +TOPIC_REGEX = os.environ["TOPIC_REGEX"] + +SMTP_SERVER = os.environ["SMTP_SERVER"] +SMTP_PORT = int(os.environ["SMTP_PORT"]) +SMTP_USER = os.environ["SMTP_USER"] +SMTP_PASS = os.environ["SMTP_PASS"] +EMAIL_TO = os.environ["EMAIL_TO"] + +MIN_WAIT_TIME = int(os.environ["MIN_WAIT_TIME"]) +MAX_WAIT_TIME = int(os.environ["MAX_WAIT_TIME"]) + +MIN_SLEEP_TIME = int(os.environ["MIN_SLEEP_TIME"]) +MAX_SLEEP_TIME = int(os.environ["MAX_SLEEP_TIME"]) + +topic_pattern = re.compile(TOPIC_REGEX) + +def log(level, msg): + if level == "." and not DEBUG_LOG: + return + ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + print(f"[{ts}] [{level}] {msg}", flush=True) + +def sendmail(subject, message): + try: + msg = MIMEText(message) + msg["Subject"] = subject + msg["From"] = SMTP_USER + msg["To"] = EMAIL_TO + + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + server.starttls() + server.login(SMTP_USER, SMTP_PASS) + server.send_message(msg) + log("+", f"Email sent to {EMAIL_TO}") + except Exception as e: + log("!", f"Failed to send error email: {e}") + +def connect_to_channel_and_check_topic(): + log("*", f"Connecting to {IRC_SERVER}:{IRC_PORT} {"with TLS" if USE_TLS else "plain"}") + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(10) + if USE_TLS: + context = ssl.create_default_context() + sock = context.wrap_socket(sock, server_hostname=IRC_SERVER) + + sock.connect((IRC_SERVER, IRC_PORT)) + + # Send NICK and USER + nick = f"NICK {IRC_NICK}\r\n".encode() + log(".", nick) + sock.sendall(nick) + + irc_user = f"USER {IRC_USER} 0 * :{IRC_REALNAME}\r\n".encode() + log(".", irc_user) + sock.sendall(irc_user) + + topic = None + joined = False + + while True: + data = sock.recv(4096) + if not data: + break + lines = data.decode(errors="ignore").split("\r\n") + for line in lines: + if not line: + continue + log(".", f"< {line}") + + # Respond to PING + if line.startswith("PING"): + resp = line.replace("PING", "PONG") + pong = f"{resp}\r\n".encode() + sock.sendall(pong) + log(".", pong) + + # Check for end of MOTD or welcome to join channel + if " 001 " in line and not joined: + # Join channel + join = f"JOIN {IRC_CHANNEL}\r\n".encode() + sock.sendall(join) + log(".", join) + joined = True + + # Capture topic sent by server upon joining + # IRC servers send: :server 332 nick #channel :topic here + if f" 332 {IRC_NICK} " in line: + parts = line.split(":", 2) + if len(parts) == 3: + topic = parts[2] + log("*", f"Channel topic on join: {topic}") + + if topic_pattern.search(topic): + log("+", f"Found topic, sending mail to {EMAIL_TO}") + sendmail("IRC TOPIC BOT: Found topic", topic) + + # If we have the topic, we can disconnect after a random delay + if topic is not None: + wait_time = random.randint(MIN_WAIT_TIME, MAX_WAIT_TIME) + log("*", f"Waiting for {wait_time} seconds before disconnecting") + time.sleep(wait_time) + bye = b"QUIT :Bye!\r\n" + sock.sendall(bye) + log(".", bye) + sock.close() + log("*", "Disconnected") + return + +def main(): + try: + res = requests.get("https://api.ipify.org", timeout=10) + res.raise_for_status() + ip = res.text.strip() + log("*", f"Public IP is {ip}") + except Exception as e: + log("!", f"Could not get public IP, {e}") + + while True: + connect_to_channel_and_check_topic() + sleep_time = random.randint(MIN_SLEEP_TIME, MAX_SLEEP_TIME) + log("*", f"Sleeping for {sleep_time} seconds before reconnecting") + time.sleep(sleep_time) + +if __name__ == "__main__": + main() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e56ab2e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +autocommand==2.2.2 +backports.tarfile==1.2.0 +certifi==2025.10.5 +charset-normalizer==3.4.4 +idna==3.11 +importlib_resources==6.5.2 +irc==20.5.0 +jaraco.collections==5.2.1 +jaraco.context==6.0.1 +jaraco.functools==4.3.0 +jaraco.logging==3.4.0 +jaraco.stream==3.0.4 +jaraco.text==4.0.0 +more-itertools==10.8.0 +python-dateutil==2.9.0.post0 +pytz==2025.2 +requests==2.32.5 +six==1.17.0 +tempora==5.8.1 +urllib3==2.5.0 +zipp==3.23.0