commit 2e730b85297da725cda44bc3cb32eed767c4fee0 Author: q3aql Date: Sat Aug 23 15:48:14 2025 +0200 ircbothist.py diff --git a/ircbothist.py b/ircbothist.py new file mode 100755 index 0000000..6b84417 --- /dev/null +++ b/ircbothist.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +################################################################ +# IRC Bot – Persistent history (last 200 messages per channel) # +# # +# Created by q3aql (q3aql@duck.com) # +# Licensed by GPL v2.0 # +# Last update: 23-08-2025 # +# # +# Requirements: # +# pip install irc # +################################################################ + +import sys +import time +import signal +import logging +import pickle +import os +from collections import defaultdict, deque +from threading import Event, Thread + +import irc.client + +######## CONFIGURATION (Edit with your settings) +SERVER = "irc.example.net" +PORT = 6667 +USE_TLS = False +NICK = "history-bot" +REALNAME = "IRC Message History" +CHANNELS = ["#support", "#linux"] +MAX_HISTORY = 200 +PERSIST_FILE = "history.pkl" +SAVE_INTERVAL = 60 +######### + +history = defaultdict(lambda: deque(maxlen=MAX_HISTORY)) + +def load_history() -> None: + if os.path.isfile(PERSIST_FILE): + try: + with open(PERSIST_FILE, "rb") as f: + data = pickle.load(f) + for chan, msgs in data.items(): + history[chan] = deque(msgs, maxlen=MAX_HISTORY) + logging.info("History loaded from %s", PERSIST_FILE) + except Exception as exc: + logging.error("Error loading history: %s", exc) + + +def save_history() -> None: + try: + serializable = {chan: list(msgs) for chan, msgs in history.items()} + with open(PERSIST_FILE, "wb") as f: + pickle.dump(serializable, f) + logging.info("History saved in %s", PERSIST_FILE) + except Exception as exc: + logging.error("Error saving history: %s", exc) + +def periodic_saver(stop_event: Event) -> None: + while not stop_event.is_set(): + stop_event.wait(SAVE_INTERVAL) + if not stop_event.is_set(): + save_history() + +def on_connect(conn, event): + logging.info("Connected to %s:%s", SERVER, PORT) + for chan in CHANNELS: + conn.join(chan) + logging.info("Joining %s", chan) + +def on_join(conn, event): + nick = irc.client.NickMask(event.source).nick + channel = event.target + + if nick == NICK: + return + + logging.info("%s has joined %s – sending history", nick, channel) + + if history[channel]: + for idx, line in enumerate(history[channel], start=1): + conn.privmsg(nick, f"[{channel}] ({idx}) {line}") + else: + conn.privmsg(nick, f"{channel}: No history yet.") + + +def on_pubmsg(conn, event): + channel = event.target + nick = irc.client.NickMask(event.source).nick + message = event.arguments[0] + + formatted = f"<{nick}> {message}" + history[channel].append(formatted) + logging.debug("Saved in %s: %s", channel, formatted) + + +def on_disconnect(conn, event): + logging.warning("Disconnected from the server – reconnecting in 10s...") + time.sleep(10) + try: + connect_and_start() + except Exception as exc: + logging.error("Connection failure: %s", exc) + logging.warning("Retrying connection in 10s...") + time.sleep(10) + connect_and_start() + #sys.exit(1) + + +def on_error(conn, event): + logging.error("ERROR from server: %s", event.arguments) + +def connect_and_start(): + reactor = irc.client.Reactor() + + try: + if USE_TLS: + conn = reactor.server().connect_ssl( + SERVER, PORT, NICK, password=None, ssl_verify=False + ) + else: + conn = reactor.server().connect(SERVER, PORT, NICK, password=None) + except irc.client.ServerConnectionError as e: + logging.error("Unable to connect: %s", e) + logging.warning("Retrying connection in 10s...") + time.sleep(10) + connect_and_start() + #sys.exit(1) + + conn.add_global_handler("welcome", on_connect) # 001 + conn.add_global_handler("join", on_join) + conn.add_global_handler("pubmsg", on_pubmsg) + conn.add_global_handler("disconnect", on_disconnect) + conn.add_global_handler("error", on_error) + + reactor.process_forever() + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + load_history() + + stop_saver = Event() + saver_thread = Thread(target=periodic_saver, args=(stop_saver,), daemon=True) + saver_thread.start() + + def shutdown(signum, frame): + logging.info("Termination signal received – saving history and exiting...") + stop_saver.set() + saver_thread.join() + save_history() + sys.exit(0) + + signal.signal(signal.SIGINT, shutdown) + signal.signal(signal.SIGTERM, shutdown) + + try: + connect_and_start() + finally: + stop_saver.set() + saver_thread.join() + save_history()