From ef6eb849211530f880e3ed92c93700ab957eb00e Mon Sep 17 00:00:00 2001 From: Michal Tuszowski Date: Sun, 28 Jun 2026 22:55:53 +0200 Subject: [PATCH] Preserve backup_old_docker bot variant at root (for comparison) Snapshot of the dockerisation-era bot code laid out at the repository root so it can be diffed directly against the working-copy baseline (restructure/working-copy-root) to see how the variant differed: - thin_client.py asyncio entrypoint (vs working_copy bot.py) - constants.py env-var/credential refactor, communication auth, etc. - extra gpt_interface/ service, sync.py, watch_script_params.py - conanjurer_* modules absent in this variant Docker infrastructure (docker/, docker-compose.yml, .dockerignore) intentionally omitted - this branch is for code comparison only and is not intended to be merged. Co-Authored-By: Claude Opus 4.8 --- README.md | 21 +++ administration_commands.py | 4 +- ai_commands.py | 6 +- ai_functions.py | 4 +- cogs/music.py | 46 +++++ communication_subroutine.py | 55 ++++-- conanjurer_commands.py | 102 ------------ conanjurer_functions.py | 206 ----------------------- constants.py | 274 ++++++++++++++++++------------ deploy.sh | 12 +- deploy_music.sh | 0 dj_fonts_system_install.sh | 0 file_search_commands.py | 2 +- gpt_interface/app.py | 14 ++ gpt_interface/file_serv.py | 155 +++++++++++++++++ gpt_interface/ingest.py | 52 ++++++ gpt_interface/reinit_server.sh | 2 + install_main_bot.sh | 2 +- install_music_bot.sh | 0 latex_commands.py | 4 +- librarian_commands.py | 15 +- music_functions.py | 27 ++- other_commands.py | 2 +- pamiec.json | 16 -- pamiec_muzyki.json | 2 +- radio_commands.py | 17 +- requirements_bot copy.txt | 20 +++ status_report.sh | 0 stream_test.py | 67 ++++++++ sync.py | 296 +++++++++++++++++++++++++++++++++ system_gpt_settings.json | 52 ++---- bot.py => thin_client.py | 72 +++++--- watch_script_params.py | 63 +++++++ 33 files changed, 1074 insertions(+), 536 deletions(-) create mode 100644 cogs/music.py delete mode 100644 conanjurer_commands.py delete mode 100644 conanjurer_functions.py mode change 100644 => 100755 deploy.sh mode change 100644 => 100755 deploy_music.sh mode change 100644 => 100755 dj_fonts_system_install.sh create mode 100644 gpt_interface/app.py create mode 100644 gpt_interface/file_serv.py create mode 100644 gpt_interface/ingest.py create mode 100755 gpt_interface/reinit_server.sh mode change 100644 => 100755 install_main_bot.sh mode change 100644 => 100755 install_music_bot.sh create mode 100644 requirements_bot copy.txt mode change 100644 => 100755 status_report.sh create mode 100644 stream_test.py create mode 100644 sync.py rename bot.py => thin_client.py (60%) create mode 100755 watch_script_params.py diff --git a/README.md b/README.md index a63ee26..020b74a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # conjurer Discord.py bot for fun, sex and BDSM + +## Docker quick start + +Docker definitions live at the repository root and let you run the Discord bot +(`thin_client.py`), the musician file service, and the librarian search worker as +separate containers. + +1. **Prepare configuration** + - Edit the environment files under `docker/env/*.env` and replace the + placeholders (`replace-me`, `shared-secret`, etc.) with your real tokens. + Keep `CONJURER_API_KEY` identical across all services. +2. **Create host directories** listed in `docker-compose.yml` (for example + `docker/volumes/bot/config`, `docker/volumes/musician/data`, …) and populate + them with the required JSON settings or media assets. +3. **Launch the stack** + ```bash + docker compose up --build + ``` + +Logs for each container are mounted under `docker/volumes/*` so you can diff the +runtime JSON and log files in git if needed. diff --git a/administration_commands.py b/administration_commands.py index c964d48..0af1c34 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -10,8 +10,8 @@ import discord import numpy as np from discord.ext import commands, tasks -from ai_functions import get_random_cyclic_message -from constants import ( +from conjurer.backup_old_docker.ai_functions import get_random_cyclic_message +from conjurer.backup_old_docker.constants import ( ENCODING, LAST_SPONTANEOUS_CALL, LOGFILE, diff --git a/ai_commands.py b/ai_commands.py index baa3bc1..1516a04 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -10,11 +10,11 @@ import discord import openai import requests from discord.ext import commands -from other_functions import discord_friendly_send, discord_friendly_reply +from conjurer.backup_old_docker.other_functions import discord_friendly_send, discord_friendly_reply -import ai_functions -from constants import ( +import conjurer.backup_old_docker.ai_functions as ai_functions +from conjurer.backup_old_docker.constants import ( ASSISTANTS, DATA, GRAPHICS_PATH, diff --git a/ai_functions.py b/ai_functions.py index ef48fb7..021f980 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -6,8 +6,8 @@ import random import openai import tiktoken import time -from other_functions import discord_friendly_send -from constants import ( +from conjurer.backup_old_docker.other_functions import discord_friendly_send +from conjurer.backup_old_docker.constants import ( ASSISTANTS, CYCLIC_WORDS, ENCODING, diff --git a/cogs/music.py b/cogs/music.py new file mode 100644 index 0000000..8d77915 --- /dev/null +++ b/cogs/music.py @@ -0,0 +1,46 @@ +import discord +from discord.ext import commands +from typing import Optional + +class Music(commands.Cog): + def __init__(self, bot): + self.bot = bot # This is so you can access Bot instance in your cog + +# You must have this function for `bot.load_extension` to call +def setup(bot): + bot.add_cog(Music(bot)) + +@commands.hybrid_command( + name="przytul", description="Przytul kogoś - daj mention po komendzie :)" +) +async def przytul(ctx, arg: Optional[discord.Member] = None): + """ + Generate a text about hugging mentioned user. + + :param ctx: ctx stands for "context" and is a required parameter in Discord.py commands. It + represents the context in which the command was invoked, including information such as the message, + the channel, the server, and the user who invoked the command + :param arg: arg is a parameter of the function "przytul" that expects a Discord member object. The + parameter is optional, meaning that if no member object is provided, it will default to None + :type arg: Optional[discord.Member] + """ + async with ctx.typing(): + nieprzytulac = False + for mention in ctx.message.mentions: + for role in mention.roles: + if role.name == "NIEPRZYTULAĆ!": + nieprzytulac = True + + if arg and nieprzytulac: + await ctx.send( + f"Żebym ja Ciebie nie przytulił {ctx.message.author.mention}" + ) + elif arg: + await ctx.send( + # trunk-ignore(codespell/misspelled) + f"Już dobrze.... Już dobrze... Ojej.. Biedactwo... :( *W ułamku sekundy {arg.mention} znajduje sie w duszącym uścisku. Żebra trzeszczą - kilka pęka. Pacnięcia po plecach grożą odbiciem nerek, a głaskanie po głowie powoduje wstrząs mózgu*" + ) + else: + await ctx.send( + "Kogo mam przytulić? *Wyłamuje kostki i przeciąga się - jego 200 kilowa sylwetka złożona z samych mięśni świadczy o tym że jest gotowy*" + ) diff --git a/communication_subroutine.py b/communication_subroutine.py index f4ee528..7322aa0 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -1,15 +1,17 @@ import json import logging +import os import re import threading import time from queue import Empty, Queue +from typing import Optional from urllib import request as urequest -from flask import Flask, jsonify, request +from flask import Flask, abort, jsonify, request from waitress import serve -HOST_ADDRESS = "192.168.1.31" +HOST_ADDRESS = "192.168.1.191" PORT_ADDRESS = 5000 ICECAST_ADDRESS = "http://192.168.1.15:8000" OUT_COMM_Q = Queue() @@ -31,6 +33,13 @@ PREPPED_TRACKS = { } logger = logging.getLogger("discord") +API_KEY = os.getenv("CONJURER_API_KEY") + + +def _authorize_request() -> None: + if API_KEY and request.headers.get("X-Conjurer-Api-Key") != API_KEY: + abort(401) + class QueryControl: """ @@ -53,6 +62,7 @@ class QueryControl: @app.route("/prepped_tracks", methods=["POST"]) def log_radio_tracks(): + _authorize_request() app.logger = logging.getLogger("discord") app.logger.info(request) @@ -79,6 +89,7 @@ def log_radio_tracks(): @app.route("/conjurer", methods=["POST"]) def answer_external_command(): + _authorize_request() """ The function `answer_external_command` logs the request data, loads the data as JSON, logs the record, and then puts the record into an incoming queue before returning a success message. @@ -129,7 +140,7 @@ def waitress_run(): serve(app, host=HOST_ADDRESS, port=PORT_ADDRESS) -def scan_queue(): +def scan_queue(stop_event: Optional[threading.Event] = None): """ The function `scan_queue` reads data from a queue, logs it, and appends it to another queue. @@ -140,12 +151,18 @@ def scan_queue(): """ logger = logging.getLogger("discord") while True: - data = OUT_COMM_Q.get() + if stop_event and stop_event.is_set(): + logger.info("scan_queue: stop requested") + break + try: + data = OUT_COMM_Q.get(timeout=1) + except Empty: + continue logger.info(data) awaiting_q.append(data) -def scan_incoming(): +def scan_incoming(stop_event: Optional[threading.Event] = None): """ The `scan_incoming` function continuously checks for incoming data, processes it, and logs when data is found. @@ -157,6 +174,9 @@ def scan_incoming(): """ logger = logging.getLogger("discord") while True: + if stop_event and stop_event.is_set(): + logger.info("scan_incoming: stop requested") + break try: answer = incoming_q.get(block=False) logger.info("DATA FOUND") @@ -204,7 +224,7 @@ def id3(url: str) -> dict: return tagdata -def comm_subroutine(): +def comm_subroutine(stop_event: Optional[threading.Event] = None): """ The `comm_subroutine` function starts multiple threads to run different tasks concurrently. @@ -217,14 +237,27 @@ def comm_subroutine(): logger.info("Started comms") threads = [] # threads.append(threading.Thread(target=flask_debug)) - threads.append(threading.Thread(target=waitress_run)) - threads.append(threading.Thread(target=scan_queue)) - threads.append(threading.Thread(target=scan_incoming)) + threads.append( + threading.Thread(target=waitress_run, daemon=True) + ) + threads.append( + threading.Thread(target=scan_queue, kwargs={"stop_event": stop_event}, daemon=True) + ) + threads.append( + threading.Thread(target=scan_incoming, kwargs={"stop_event": stop_event}, daemon=True) + ) for worker in threads: worker.start() - for worker in threads: - worker.join() + + try: + while any(thread.is_alive() for thread in threads): + if stop_event and stop_event.is_set(): + break + time.sleep(0.5) + finally: + if stop_event: + stop_event.set() if __name__ == "__main__": diff --git a/conanjurer_commands.py b/conanjurer_commands.py deleted file mode 100644 index b33965a..0000000 --- a/conanjurer_commands.py +++ /dev/null @@ -1,102 +0,0 @@ -# ai command cogs -import logging -import discord -from discord.ext import commands -from other_functions import discord_friendly_send, discord_friendly_reply -from __future__ import annotations -import asyncio -import logging - -from conanjurer_functions import watch, Event -log = logging.getLogger("discord") - -def is_gm(): - async def predicate(ctx: commands.Context) -> bool: - role_id = ctx.bot.cfg.gm_role_id - ok = any(r.id == role_id for r in getattr(ctx.author, "roles", [])) - if not ok: - await ctx.reply("⛔ Tylko GM.", mention_author=False) - return ok - return commands.check(predicate) - - -class FromConan(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self._task: asyncio.Task | None = None - - async def cog_load(self): - self._task = asyncio.create_task(self._run()) - - async def cog_unload(self): - if self._task: - self._task.cancel() - - async def _run(self): - await self.bot.wait_until_ready() - cfg = self.bot.cfg - chat_ch = self.bot.get_channel(cfg.chan_chat) - evt_ch = self.bot.get_channel(cfg.chan_events) - - async def on_event(e: Event): - target = chat_ch if e.kind == "chat" else evt_ch - if target is not None: - # allowed_mentions: nie pinguj nikogo treścią z gry - await target.send(e.text, allowed_mentions=discord.AllowedMentions.none()) - - log.info("Start obserwacji logu (tryb=%s)", cfg.log_mode) - await watch(cfg, on_event) - -class ToConan(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @commands.command(name="say") - @is_gm() - async def say(self, ctx: commands.Context, *, message: str): - """Discord -> Conan: ogłoszenie widoczne dla wszystkich graczy w grze.""" - resp = await self.bot.rcon.command(f"broadcast {message}") - await ctx.reply(f"✅ Wysłano do gry. (serwer: `{resp.strip() or 'OK'}`)", - mention_author=False) - - @commands.command(name="players") - @is_gm() - async def players(self, ctx: commands.Context): - """Lista graczy online (RCON listplayers).""" - resp = await self.bot.rcon.command("listplayers") - await ctx.reply(f"```\n{resp.strip() or 'brak danych'}\n```", - mention_author=False) - - @commands.command(name="kick") - @is_gm() - async def kick(self, ctx: commands.Context, *, who: str): - """Wyrzuć gracza (po nazwie/charname — zależnie od wersji serwera).""" - resp = await self.bot.rcon.command(f"kick {who}") - await ctx.reply(f"👢 `{resp.strip() or 'OK'}`", mention_author=False) - - @commands.command(name="rcon") - @is_gm() - async def raw_rcon(self, ctx: commands.Context, *, cmd: str): - """Surowa komenda RCON (dla zaawansowanych GM). Używaj ostrożnie.""" - resp = await self.bot.rcon.command(cmd) - await ctx.reply(f"```\n{resp.strip() or 'OK'}\n```", mention_author=False) - - # --- HOOK pod przyszły mod mostu (Chat V2 API) --- - @commands.command(name="ogłoś", aliases=["oglos", "rp"]) - @is_gm() - async def rp_announce(self, ctx: commands.Context, nadawca: str, *, message: str): - """Wiadomość RP 'z nadawcą' (np. ogłoszenie w imieniu Króla Khasara). - - Na samym RCON realizujemy to jako sformatowany broadcast. Gdy dodasz - własny mod na Chat V2 API z dedykowaną komendą czatu, podmień tę linię - na wywołanie tej komendy (np. 'tot_rp_say '). - """ - resp = await self.bot.rcon.command(f"broadcast [{nadawca}]: {message}") - await ctx.reply(f"📜 Ogłoszono jako **{nadawca}**.", mention_author=False) - - -async def setup(bot: commands.Bot): - await bot.add_cog(ToConan(bot)) - await bot.add_cog(FromConan(bot)) - - diff --git a/conanjurer_functions.py b/conanjurer_functions.py deleted file mode 100644 index b94d18f..0000000 --- a/conanjurer_functions.py +++ /dev/null @@ -1,206 +0,0 @@ -from __future__ import annotations -import os -from dataclasses import dataclass -from dotenv import load_dotenv -import asyncio -import logging -import re -from typing import AsyncIterator, Callable, Awaitable -from aiomcrcon import Client as _Rcon # Source RCON over TCP -import asyncssh - -load_dotenv() - -log = logging.getLogger("discord") -@dataclass -class Event: - kind: str # "chat" | "login" | "logout" | "death" | "raw" - text: str # gotowy do wyświetlenia tekst - raw: str # oryginalna linia (do debugowania) - - -# --- REGEXY DO DOSTROJENIA NA WŁASNYM LOGU --- -# Poniżej PRZYKŁADOWE wzorce. Conan/Pippi/Tot logują różnie — sprawdź swój log -# i dopasuj. Jeśli wzorzec nie pasuje, linia trafia jako "raw" tylko do debug. -_PATTERNS: list[tuple[str, re.Pattern]] = [ - ("chat", re.compile(r"Chat:\s*(?P.+?):\s*(?P.+)$", re.I)), - ("login", re.compile(r"(?P.+?)\s+(joined|connected|logged in)", re.I)), - ("logout", re.compile(r"(?P.+?)\s+(left|disconnected|logged out)", re.I)), - ("death", re.compile(r"(?P.+?)\s+was killed by\s+(?P.+)$", re.I)), -] - -@dataclass(frozen=True) -class Config: - # Discord - guild_id: int - chan_chat: int - chan_events: int - gm_role_id: int - # RCON - rcon_host: str - rcon_port: int - rcon_password: str - # Log - log_mode: str # "local" | "sftp" - log_path: str - sftp_host: str | None - sftp_port: int - sftp_user: str | None - sftp_password: str | None - - @staticmethod - def load() -> "Config": - mode = os.getenv("LOG_MODE", "local").lower() - return Config( - guild_id=int(_req("DISCORD_GUILD_ID")), - chan_chat=int(_req("CHAN_CHAT")), - chan_events=int(_req("CHAN_EVENTS")), - gm_role_id=int(_req("GM_ROLE_ID")), - rcon_host=_req("RCON_HOST"), - rcon_port=int(os.getenv("RCON_PORT", "25575")), - rcon_password=_req("RCON_PASSWORD"), - log_mode=mode, - log_path=_req("LOG_PATH"), - sftp_host=os.getenv("SFTP_HOST"), - sftp_port=int(os.getenv("SFTP_PORT", "22")), - sftp_user=os.getenv("SFTP_USER"), - sftp_password=os.getenv("SFTP_PASSWORD"), - ) - - -class RconClient: - def __init__(self, host: str, port: int, password: str): - self._host, self._port, self._pw = host, port, password - self._client: _Rcon | None = None - self._lock = asyncio.Lock() - - async def _ensure(self) -> _Rcon: - if self._client is None: - c = _Rcon(self._host, self._port, self._pw) - await c.connect() - self._client = c - log.info("RCON połączony %s:%s", self._host, self._port) - return self._client - - async def command(self, cmd: str) -> str: - """Wyślij komendę do serwera Conana. Zwraca odpowiedź serwera. - - To jest KANAŁ Discord -> Conan. Np. command("broadcast Witajcie!") - wyświetli komunikat wszystkim graczom w grze. - """ - async with self._lock: - for attempt in (1, 2): - try: - client = await self._ensure() - resp, _ = await client.send_cmd(cmd) - return resp - except Exception as e: # rozłączenie/restart serwera - log.warning("RCON błąd (próba %s): %s", attempt, e) - await self.close() - if attempt == 2: - raise - await asyncio.sleep(1.0) - return "" - - async def close(self) -> None: - if self._client is not None: - try: - await self._client.close() - except Exception: - pass - self._client = None - - - -def parse_line(line: str) -> Event | None: - line = line.rstrip("\n") - if not line.strip(): - return None - for kind, pat in _PATTERNS: - m = pat.search(line) - if m: - g = m.groupdict() - if kind == "chat": - return Event(kind, f"💬 **{g['who']}**: {g['msg']}", line) - if kind == "login": - return Event(kind, f"🟢 **{g['who']}** dołączył do gry", line) - if kind == "logout": - return Event(kind, f"⚪ **{g['who']}** opuścił grę", line) - if kind == "death": - return Event(kind, f"💀 **{g['who']}** zginął z ręki {g['by']}", line) - return None # nierozpoznane -> ignoruj (nie zgadujemy) - - -async def _follow_local(path: str) -> AsyncIterator[str]: - """tail -f w czystym asyncio, podąża też po rotacji pliku.""" - import os - while True: - try: - with open(path, "r", encoding="utf-8", errors="replace") as f: - f.seek(0, os.SEEK_END) - inode = os.fstat(f.fileno()).st_ino - while True: - line = f.readline() - if line: - yield line - continue - await asyncio.sleep(0.5) - # wykryj rotację logu - try: - if os.stat(path).st_ino != inode: - break - except FileNotFoundError: - break - except FileNotFoundError: - log.warning("Log nie istnieje jeszcze: %s", path) - await asyncio.sleep(3.0) - - -async def _follow_sftp(cfg) -> AsyncIterator[str]: - """Polling przyrostowy po SFTP (Host Havoc). Czyta tylko nowe bajty.""" - offset = 0 - while True: - try: - async with asyncssh.connect( - cfg.sftp_host, port=cfg.sftp_port, - username=cfg.sftp_user, password=cfg.sftp_password, - known_hosts=None, - ) as conn: - async with conn.start_sftp_client() as sftp: - while True: - try: - attrs = await sftp.stat(cfg.log_path) - size = attrs.size or 0 - if size < offset: # rotacja - offset = 0 - if size > offset: - async with sftp.open(cfg.log_path, "r") as rf: - await rf.seek(offset) - chunk = await rf.read() - offset = size - for ln in chunk.splitlines(): - yield ln - except FileNotFoundError: - log.warning("SFTP: brak logu %s", cfg.log_path) - await asyncio.sleep(2.0) - except Exception as e: - log.warning("SFTP rozłączony: %s — ponawiam", e) - await asyncio.sleep(5.0) - - -async def watch(cfg, on_event: Callable[[Event], Awaitable[None]]) -> None: - source = _follow_local(cfg.log_path) if cfg.log_mode == "local" else _follow_sftp(cfg) - async for line in source: - evt = parse_line(line) - if evt is not None: - try: - await on_event(evt) - except Exception: - log.exception("Błąd obsługi zdarzenia") - -def _req(key: str) -> str: - val = os.getenv(key) - if not val or val.startswith("wklej") or val.startswith("000000"): - raise RuntimeError(f"Brak/placeholder w .env: {key}") - return val - diff --git a/constants.py b/constants.py index 2185187..415716e 100644 --- a/constants.py +++ b/constants.py @@ -1,13 +1,36 @@ -import json -import netrc -from datetime import datetime -from platform import uname -from sys import platform -from typing import List, Optional, TypedDict +"""Centralised configuration and runtime constants for Conjurer services. -import openai -import spotipy -from spotipy.oauth2 import SpotifyClientCredentials +This module used to perform heavy filesystem and credential reads at import +time which made the project brittle on hosts that did not mirror the original +paths. The current implementation defers that work, reads configuration from +environment variables (with sensible fallbacks), and protects optional +dependencies so the main bot can start even when a secondary service is +offline. +""" + +import json +import logging +import os +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Tuple, TypedDict + +try: + import netrc +except ImportError: # pragma: no cover - standard on CPython + netrc = None + +try: + import openai +except ImportError: # pragma: no cover - optional at runtime + openai = None + +try: + import spotipy + from spotipy.oauth2 import SpotifyClientCredentials +except ImportError: # pragma: no cover - optional component + spotipy = None + SpotifyClientCredentials = None Music_Config = TypedDict( "Music_Config", @@ -24,21 +47,43 @@ MASTER_TIMEOUT = datetime.now() INITIAL_TIME_WAIT = 500 MUZYKA: Music_Config = {"ctx": None, "queue": [], "requester": []} -LOGFILE = "" -NETRC_FILE = "" -MUSIC_FOLDER = "" -MEMORY_FIVE_SIARA = "" -MEMORY_FIVE_MUZYKA = "" -SETTINGS_FILE = "" -ENCODING = "" -GRAPHICS_PATH = "" +logger = logging.getLogger("discord") + + +def _env_path(var_name: str, fallback: Path) -> Path: + value = os.getenv(var_name) + if value: + return Path(value).expanduser().resolve() + return fallback + + +def _env(var_name: str, fallback: str) -> str: + return os.getenv(var_name, fallback) + + +BASE_DIR = Path(os.getenv("CONJURER_BASE_DIR", Path(__file__).resolve().parent)) + +LOGFILE = _env_path("CONJURER_LOG_FILE", BASE_DIR / "discord.log") +NETRC_FILE = _env_path("CONJURER_NETRC_FILE", Path.home() / ".netrc") +SETTINGS_FILE = _env_path("CONJURER_SETTINGS_FILE", BASE_DIR / "settings.json") +MEMORY_FIVE_SIARA = _env_path("CONJURER_MEMORY_FILE", BASE_DIR / "pamiec.json") +MEMORY_FIVE_MUZYKA = _env_path( + "CONJURER_MUSIC_MEMORY_FILE", BASE_DIR / "pamiec_muzyki.json" +) +GRAPHICS_PATH = _env_path( + "CONJURER_GRAPHICS_PATH", BASE_DIR / "Conjurer_graphics" +) +MUSIC_FOLDER = _env_path("CONJURER_MUSIC_FOLDER", BASE_DIR / "music") +ENCODING = _env("CONJURER_ENCODING", "utf-8") + +API_SHARED_KEY = os.getenv("CONJURER_API_KEY", "") MUZYKA_MOJEGO_LUDU_HISTORIA = 1500 MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE = 15 MUZYKA_MOJEGO_LUDU_PLAJLISTA = 30 -FILE_SERVICE_ADDRESS = "http://192.168.1.15:5000" -RADIO_HARBOR_ADDRESS = "http://192.168.1.15:54321" -SKIP_TRACK = "/skip" +FILE_SERVICE_ADDRESS = _env("CONJURER_FILE_SERVICE", "http://127.0.0.1:5000") +RADIO_HARBOR_ADDRESS = _env("CONJURER_RADIO_HARBOR", "http://127.0.0.1:54321") +SKIP_TRACK = _env("CONJURER_SKIP_ENDPOINT", "/skip") GET_MP3 = "/mp3" SEND_MP3 = "/update_mp3" @@ -48,98 +93,123 @@ CREATE_PRIO_PLAYLIST = "/create_priority_playlist" REQUEST_MUSIC = "/request_radio_file" CLEAR_PRIO = "/clear_pr_pls" -LIBRARIAN_SERVICE_ADDRESS = "http://192.168.1.192:5001" -SEND_QUERY = "/query" +LIBRARIAN_SERVICE_ADDRESS = _env( + "CONJURER_LIBRARIAN_SERVICE", "http://127.0.0.1:5001" +) +SEND_QUERY = _env("CONJURER_LIBRARIAN_QUERY_ENDPOINT", "/query") TIME_BETWEEN_CALLS = 100000 LAST_SPONTANEOUS_CALL = datetime.now() -HOST_ADDRESS = "192.168.1.191" -PORT_ADDRESS = 5000 +HOST_ADDRESS = _env("CONJURER_DISCORD_HOST", "0.0.0.0") +PORT_ADDRESS = int(_env("CONJURER_DISCORD_PORT", "5000")) # *=========================================== Platform Specific Predefines -if platform in ("linux", "linux2"): - SEPARATOR_FILE_PATH = "/" - if "microsoft-standard" in uname().release: - LOGFILE = "/home/mtuszowski/conjurer/discord.log" - MEMORY_FIVE_SIARA = "/home/mtuszowski/conjurer/pamiec.json" - SYSTEM_GPT_SETTINGS = "/home/mtuszowski/conjurer/system_gpt_settings.json" - MEMORY_FIVE_MUZYKA = "/home/mtuszowski/conjurer/pamiec_muzyki.json" - MUSIC_FOLDER = "/mnt/g/Muzyka/" - SETTINGS_FILE = "/home/mtuszowski/conjurer/settings.json" - NETRC_FILE = "/home/mtuszowski/.netrc" - LOGSTORE = "/home/mtuszowski/conjurer/logs/" - ACCIDENT_LOG = "/home/mtuszowski/conjurer/accident_log.json" - ENCODING = "utf-8" - GRAPHICS_PATH = "/home/mtuszowski/conjurer/Conjurer_graphics/" - DIR_PATH_SADOX = "/mnt/c/Users/mtusz/OneDrive/Dokumenty/Fansadox" +SEPARATOR_FILE_PATH = _env("CONJURER_PATH_SEPARATOR", os.sep) - else: - LOGFILE = "/home/pi/Conjurer/discord.log" - MEMORY_FIVE_SIARA = "/home/pi/Conjurer/pamiec.json" - SYSTEM_GPT_SETTINGS = "/home/pi/Conjurer/system_gpt_settings.json" - MEMORY_FIVE_MUZYKA = "/home/pi/Conjurer/pamiec_muzyki.json" - MUSIC_FOLDER = "/home/pi/MediaShare/mp3/" - SETTINGS_FILE = "/home/pi/Conjurer/settings.json" - NETRC_FILE = "/home/pi/.netrc" - LOGSTORE = "/home/pi/MediaShara/logs/" - ACCIDENT_LOG = "/home/pi/Conjurer/accident_log.json" - ENCODING = "utf-8" - GRAPHICS_PATH = "/home/pi/MediaShare/Conjurer_graphics/" - DIR_PATH_SADOX = "/home/pi/MediaShare/Fansadox/" - - -elif platform == "win32": - LOGFILE = "discord.log" - MEMORY_FIVE_SIARA = "pamiec.json" - SYSTEM_GPT_SETTINGS = "system_gpt_settings.json" - MEMORY_FIVE_MUZYKA = "pamiec_muzyki.json" - MUSIC_FOLDER = "G:\\Muzyka\\" - SETTINGS_FILE = "settings.json" - NETRC_FILE = "C:\\Users\\mtusz\\.netrc" - LOGSTORE = "C:\\Users\\mtusz\\OneDrive\\Pulpit\\Conjurer\\logs\\" - ACCIDENT_LOG = "accident_log.json" - ENCODING = "utf-8" - DIR_PATH_SADOX = "C:\\Users\\mtusz\\OneDrive\\Dokumenty\\Fansadox\\" - SEPARATOR_FILE_PATH = "\\" -with open(SETTINGS_FILE, "r", encoding=ENCODING) as f_settings_file: - DATA = json.load(f_settings_file) -REMOTE_HOST_NAME = "openai" -netrc_mod = netrc.netrc(NETRC_FILE) -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) -openai.api_key = authTokens[2] -OPENAICLIENT = openai.AsyncOpenAI(api_key=openai.api_key) -REMOTE_HOST_NAME = "discord" -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) -TOKEN = authTokens[2] - -REMOTE_HOST_NAME = "spotipy" -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) -SPOTIFY_CTRL = spotipy.Spotify( - client_credentials_manager=SpotifyClientCredentials( - client_id=authTokens[0], - client_secret=authTokens[2], - ) +DIR_PATH_SADOX = _env_path( + "CONJURER_SADOX_DIR", BASE_DIR / "Fansadox" ) -REMOTE_HOST_NAME = "youtube" -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) -YOUTUBE_AUTH = [authTokens[0],authTokens[2]] -WORD_REACTIONS = DATA["word_reactions"] -CYCLIC_WORDS = DATA["cyclic_words"] +SYSTEM_GPT_SETTINGS = _env_path( + "CONJURER_SYSTEM_GPT_SETTINGS", BASE_DIR / "system_gpt_settings.json" +) + +LOGSTORE = _env_path("CONJURER_LOGSTORE", BASE_DIR / "logs") +ACCIDENT_LOG = _env_path( + "CONJURER_ACCIDENT_LOG", BASE_DIR / "accident_log.json" +) + + +def _load_json(path: Path, fallback) -> object: + try: + with path.open("r", encoding=ENCODING) as handle: + return json.load(handle) + except FileNotFoundError: + logger.warning("Missing JSON file at %s – using fallback", path) + return fallback + except json.JSONDecodeError: + logger.warning("Corrupt JSON at %s – resetting to fallback", path) + return fallback + + +DATA = _load_json(SETTINGS_FILE, {}) +WORD_REACTIONS = DATA.get("word_reactions", {}) +CYCLIC_WORDS = DATA.get("cyclic_words", {}) for key in WORD_REACTIONS: - WORD_REACTIONS[key][2] = datetime.now() -with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as temp_memory_file: - # First we load existing data into a dict. - MESSAGE_TABLE = json.load(temp_memory_file) + if isinstance(WORD_REACTIONS[key], list) and len(WORD_REACTIONS[key]) >= 3: + WORD_REACTIONS[key][2] = datetime.now() -with open(SYSTEM_GPT_SETTINGS, "r+", encoding=ENCODING) as temp_settings_file: - # First we load existing data into a dict. - GPT_SETTINGS = json.load(temp_settings_file) -with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as temp_music_memory_file: - # First we load existing data into a dict. - MESSAGE_TABLE_MUZYKA = json.load(temp_music_memory_file) -SPECJALNE_ZIEMNIACZKI = GPT_SETTINGS[1] -ASSISTANTS = {} +MESSAGE_TABLE = _load_json(MEMORY_FIVE_SIARA, {}) +GPT_SETTINGS = _load_json(SYSTEM_GPT_SETTINGS, {}) +MESSAGE_TABLE_MUZYKA = _load_json(MEMORY_FIVE_MUZYKA, {}) + +SPECJALNE_ZIEMNIACZKI = GPT_SETTINGS[1] if isinstance(GPT_SETTINGS, list) else {} +ASSISTANTS: Dict[str, Tuple[str, str, int, object]] = {} + + +def _load_netrc_credentials(host: str) -> Optional[Tuple[str, str, str]]: + if netrc is None: + return None + try: + parsed = netrc.netrc(str(NETRC_FILE)) + except FileNotFoundError: + logger.warning("netrc file %s not found", NETRC_FILE) + return None + except netrc.NetrcParseError: + logger.warning("netrc file %s is invalid", NETRC_FILE) + return None + return parsed.authenticators(host) + + +def _resolve_token(host: str, env_var: str) -> Optional[str]: + env_value = os.getenv(env_var) + if env_value: + return env_value + creds = _load_netrc_credentials(host) + if creds: + return creds[2] + logger.warning("Token for %s not configured", host) + return None + + +OPENAI_API_KEY = _resolve_token("openai", "OPENAI_API_KEY") +if openai and OPENAI_API_KEY: + openai.api_key = OPENAI_API_KEY + OPENAICLIENT = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) +else: + OPENAICLIENT = None + +TOKEN = _resolve_token("discord", "DISCORD_TOKEN") + +if spotipy: + spotify_creds = _load_netrc_credentials("spotipy") + if spotify_creds and SpotifyClientCredentials: + SPOTIFY_CTRL = spotipy.Spotify( + client_credentials_manager=SpotifyClientCredentials( + client_id=spotify_creds[0], + client_secret=spotify_creds[2], + ) + ) + else: + SPOTIFY_CTRL = None +else: + SPOTIFY_CTRL = None + +youtube_creds = _load_netrc_credentials("youtube") +if youtube_creds: + YOUTUBE_AUTH = [youtube_creds[0], youtube_creds[2]] +else: + YOUTUBE_AUTH = [ + os.getenv("YOUTUBE_USERNAME", ""), + os.getenv("YOUTUBE_PASSWORD", ""), + ] + + +def service_headers() -> Dict[str, str]: + """Shared header dict for internal HTTP calls.""" + if API_SHARED_KEY: + return {"X-Conjurer-Api-Key": API_SHARED_KEY} + return {} LATEX_TEX_ENGINE = "tectonic" diff --git a/deploy.sh b/deploy.sh old mode 100644 new mode 100755 index bb3977a..a1eafb2 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -total_commands=31 +total_commands=29 current_command=0 function print_progress { @@ -95,14 +95,8 @@ print_progress "cp ./conjurer/latex_functions.py ./Conjurer/" cp ./conjurer/librarian_functions.py ./Conjurer print_progress "cp ./conjurer/librarian_functions.py ./Conjurer/" -cp ./conjurer/conanjurer_commands.py ./Conjurer/ -print_progress "cp ./conjurer/conanjurer_commands.py ./Conjurer/" - -cp ./conjurer/conanjurer_functions.py ./Conjurer/ -print_progress "cp ./conjurer/conanjurer_functions.py ./Conjurer/" - -cp ./conjurer/bot.py ./Conjurer/bot.py -print_progress "cp ./conjurer/bot.py ./Conjurer/bot.py" +cp ./conjurer/thin_client.py ./Conjurer/bot.py +print_progress "cp ./conjurer/thin_client.py ./Conjurer/bot.py" sudo systemctl restart conjurer.service print_progress "sudo systemctl restart conjurer.service" diff --git a/deploy_music.sh b/deploy_music.sh old mode 100644 new mode 100755 diff --git a/dj_fonts_system_install.sh b/dj_fonts_system_install.sh old mode 100644 new mode 100755 diff --git a/file_search_commands.py b/file_search_commands.py index 1e20850..18c40f8 100644 --- a/file_search_commands.py +++ b/file_search_commands.py @@ -1,7 +1,7 @@ import os import discord from discord.ext import commands -import file_search_functions +import conjurer.backup_old_docker.file_search_functions as file_search_functions class FileSelectView(discord.ui.View): def __init__(self, files): diff --git a/gpt_interface/app.py b/gpt_interface/app.py new file mode 100644 index 0000000..e789c52 --- /dev/null +++ b/gpt_interface/app.py @@ -0,0 +1,14 @@ +from flask import Flask +from file_serv import bp as uploader_bp +from ingest import bp as ingest_bp +from waitress import serve + +app = Flask(__name__) +HOST_ADDRESS = "127.0.0.1" +PORT_ADDRESS = 49151 + +if __name__ == "__main__": + app.register_blueprint(uploader_bp, url_prefix="/api") + app.register_blueprint(ingest_bp, url_prefix="/api") + + serve(app, host=HOST_ADDRESS, port=PORT_ADDRESS) diff --git a/gpt_interface/file_serv.py b/gpt_interface/file_serv.py new file mode 100644 index 0000000..ee58fee --- /dev/null +++ b/gpt_interface/file_serv.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import os +from pathlib import Path +from typing import List, Dict, Any, Optional + +from flask import Blueprint, request, jsonify, send_file, abort, current_app +from werkzeug.utils import secure_filename + +# ===== Konfiguracja przez ENV ===== + +if API_KEY := os.getenv("API_KEY") is None: + with open("/home/pi/gpt_cont_api_key", "r") as f: + API_KEY = f.read().strip() +else: + API_KEY = os.getenv("API_KEY", "") # np. openssl rand -hex 32 + +UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/home/pi/tmp_git/")) # katalog na dysku +MAX_CONTENT_MB = int(os.getenv("MAX_CONTENT_MB", "200")) + +# Google Drive (opcjonalnie) +GDRIVE_ENABLE = os.getenv("GDRIVE_ENABLE", "0") == "1" +GDRIVE_SA_JSON = os.getenv("GDRIVE_SA_JSON", "") # ścieżka do pliku .json konta serwisowego +GDRIVE_FOLDER_ID = os.getenv("GDRIVE_FOLDER_ID", "") # ID folderu na Drive + +bp = Blueprint("uploader", __name__) + +# Inicjalizacja katalogu +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + +def _check_auth() -> bool: + """Proste Bearer auth. Jeśli API_KEY puste -> bez auth (niezalecane).""" + if not API_KEY: + return True + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + token = auth[7:].strip() + return token == API_KEY + return False + +def _safe_join(base: Path, *parts: str) -> Path: + """Zapobiega ../ — wymusza pozostanie w katalogu bazowym.""" + p = (base.joinpath(*parts)).resolve() + if not str(p).startswith(str(base.resolve())): + abort(400, description="Invalid path") + return p + +# ===== Google Drive helper ===== +_drive_client_cached = None + +def _get_drive(): + global _drive_client_cached + if _drive_client_cached is not None: + return _drive_client_cached + + if not (GDRIVE_ENABLE and GDRIVE_SA_JSON and os.path.exists(GDRIVE_SA_JSON)): + return None + + from google.oauth2 import service_account + from googleapiclient.discovery import build + + scopes = ["https://www.googleapis.com/auth/drive.file"] + creds = service_account.Credentials.from_service_account_file(GDRIVE_SA_JSON, scopes=scopes) + _drive_client_cached = build("drive", "v3", credentials=creds, cache_discovery=False) + return _drive_client_cached + +def upload_to_drive(local_path: Path, filename: str) -> Optional[Dict[str, Any]]: + drv = _get_drive() + if drv is None: + return None + from googleapiclient.http import MediaFileUpload + file_metadata = {"name": filename} + if GDRIVE_FOLDER_ID: + file_metadata["parents"] = [GDRIVE_FOLDER_ID] + media = MediaFileUpload(str(local_path), resumable=False) + created = drv.files().create(body=file_metadata, media_body=media, fields="id,webViewLink,webContentLink").execute() + file_id = created.get("id") + # Przydatne linki: + return { + "file_id": file_id, + "webViewLink": created.get("webViewLink"), + "webContentLink": created.get("webContentLink"), + "direct_view": f"https://drive.google.com/file/d/{file_id}/view", + "direct_download": f"https://drive.google.com/uc?export=download&id={file_id}", + } + +@bp.get("/health") +def health(): + return jsonify(ok=True, info="Okidokie") + +@bp.post("/upload") +def upload(): + """Przyjmuje: + - multipart/form-data z jednym plikiem ('file') lub wieloma ('files') + - opcjonalnie: form 'subdir' (podkatalog), 'drive' (1/0) żeby wymusić wysyłkę na Drive + """ + if not _check_auth(): + return jsonify(error="Unauthorized"), 401 + + # Limit ciała żądania po stronie Flaska: + request.max_content_length = MAX_CONTENT_MB * 1024 * 1024 + + files = [] + if "file" in request.files: + files = [request.files["file"]] + elif "files" in request.files: + files = request.files.getlist("files") + else: + return jsonify(error="No file provided (use 'file' or 'files')"), 400 + + subdir = (request.form.get("subdir") or "").strip() + target_dir = _safe_join(UPLOAD_DIR, subdir) if subdir else UPLOAD_DIR + target_dir.mkdir(parents=True, exist_ok=True) + + want_drive = (request.form.get("drive") == "1") or (request.args.get("drive") == "1") + saved: List[Dict[str, Any]] = [] + + for f in files: + if not f or not f.filename: + continue + fname = secure_filename(f.filename) + dest = _safe_join(target_dir, fname) + f.save(dest) + + item = { + "filename": fname, + "size": dest.stat().st_size, + "path": str(dest.relative_to(UPLOAD_DIR)), + "url_hint": f"/api/files/{dest.relative_to(UPLOAD_DIR)}", + } + + if want_drive and GDRIVE_ENABLE: + try: + gd = upload_to_drive(dest, fname) + if gd: + item["gdrive"] = gd + except Exception as e: + # log i idziemy dalej + current_app.logger.exception("Drive upload failed: %s", e) + item["gdrive_error"] = str(e) + + saved.append(item) + + if not saved: + return jsonify(error="No valid files"), 400 + return jsonify(ok=True, saved=saved) + +@bp.get("/files/") +def get_file(relpath: str): + if not _check_auth(): + return jsonify(error="Unauthorized"), 401 + target = _safe_join(UPLOAD_DIR, relpath) + if not target.exists() or not target.is_file(): + return jsonify(error="Not found"), 404 + return send_file(target, as_attachment=False) diff --git a/gpt_interface/ingest.py b/gpt_interface/ingest.py new file mode 100644 index 0000000..161a6e6 --- /dev/null +++ b/gpt_interface/ingest.py @@ -0,0 +1,52 @@ +# ingest_text.py +import os +import urllib.parse +from pathlib import Path + +from flask import Blueprint, abort, jsonify, request + +bp = Blueprint("ingest_text", __name__) +BASE = Path(os.getenv("INGEST_DIR", "/home/pi/tmp_git")).resolve() +BASE.mkdir(parents=True, exist_ok=True) +TOKEN = None +with open("/home/pi/gpt_cont_api_key", "r") as f: + TOKEN = f.read().strip() + + +# prosty bufor kawałków w RAM (na 1 proces) +chunks = {} + + +@bp.get("/api/ingest-text") +def ingest_text(): + if request.args.get("key") != TOKEN: + return jsonify(error="unauthorized"), 401 + + name = request.args.get("name", "").strip() + index = int(request.args.get("index", "1")) + total = int(request.args.get("total", "1")) + chunk = request.args.get("chunk", "") + + if not name or "/" in name or ".." in name: + return jsonify(error="bad name"), 400 + if not (1 <= index <= total <= 9999): + return jsonify(error="bad indexing"), 400 + + # gromadzimy w pamięci (możesz podmienić na Redis) + key = f"{name}:{total}" + entry = chunks.setdefault(key, {}) + entry[index] = urllib.parse.unquote_plus(chunk) + + if TOKEN is None: + exit("No API token set!") + if len(entry) == total: + # składamy i zapisujemy + data = "".join(entry[i] for i in range(1, total + 1)) + out = (BASE / name).resolve() + if not str(out).startswith(str(BASE)): + return jsonify(error="bad path"), 400 + out.write_text(data, encoding="utf-8") + del chunks[key] + return jsonify(ok=True, saved=str(out), bytes=len(data.encode("utf-8"))) + else: + return jsonify(pending=True, got=len(entry), total=total) diff --git a/gpt_interface/reinit_server.sh b/gpt_interface/reinit_server.sh new file mode 100755 index 0000000..340e570 --- /dev/null +++ b/gpt_interface/reinit_server.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd ../conjurer/ && git pull && cp ./gpt_interface/* ../gpt_interf_serv/ && cd - diff --git a/install_main_bot.sh b/install_main_bot.sh old mode 100644 new mode 100755 index 33c4b22..7da46a2 --- a/install_main_bot.sh +++ b/install_main_bot.sh @@ -3,7 +3,7 @@ sudo apt-get install python3-dev sudo apt-get install portaudio19-dev python3-pyaudio sudo apt-get install cd /home/pi || exit -mdkir Conjurer +mkdir Conjurer cd Conjurer ||exit python3 -m venv /home/pi/Conjurer/.env cp /home/pi/conjurer/requirements_bot.txt /home/pi/Conjurer/ diff --git a/install_music_bot.sh b/install_music_bot.sh old mode 100644 new mode 100755 diff --git a/latex_commands.py b/latex_commands.py index 012277a..bb4528c 100644 --- a/latex_commands.py +++ b/latex_commands.py @@ -10,7 +10,7 @@ import discord from discord import app_commands from discord.ext import commands -from constants import ( +from conjurer.backup_old_docker.constants import ( ALLOWED_ROLES, GUILD_ID, LATEX_MAX_ATTACH_MB, @@ -19,7 +19,7 @@ from constants import ( LATEX_TEX_ENGINE, OPENAI_MODEL, ) -from latex_functions import ( +from conjurer.backup_old_docker.latex_functions import ( compile_single_tex_bytes, compile_zip_to_zip, is_safe_asset_name, diff --git a/librarian_commands.py b/librarian_commands.py index aa12f6b..f908d25 100644 --- a/librarian_commands.py +++ b/librarian_commands.py @@ -13,9 +13,16 @@ import PyPDF2 import requests from discord.ext import commands, tasks -from ai_functions import handle_response -from communication_subroutine import IN_COMM_Q, OUT_COMM_Q, QueryControl -from constants import DIR_PATH_SADOX, LIBRARIAN_SERVICE_ADDRESS, SEND_QUERY +from conjurer.backup_old_docker.ai_functions import handle_response +from conjurer.backup_old_docker.communication_subroutine import IN_COMM_Q, OUT_COMM_Q, QueryControl +from conjurer.backup_old_docker.constants import ( + DIR_PATH_SADOX, + LIBRARIAN_SERVICE_ADDRESS, + SEND_QUERY, + service_headers, +) + +SERVICE_HEADERS = service_headers() class DataModule(commands.Cog): @@ -165,6 +172,7 @@ class DataModule(commands.Cog): requests.post, f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", json=json_query, + headers=SERVICE_HEADERS, timeout=360, ) await ctx.send( @@ -260,6 +268,7 @@ class DataModule(commands.Cog): requests.post, f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", json=json_query, + headers=SERVICE_HEADERS, timeout=360, ) await ctx.send( diff --git a/music_functions.py b/music_functions.py index a6e2881..1382f18 100644 --- a/music_functions.py +++ b/music_functions.py @@ -9,7 +9,7 @@ import discord import requests import yt_dlp -from constants import ( +from conjurer.backup_old_docker.constants import ( FILE_SERVICE_ADDRESS, GET_MP3, GET_PLAYLIST, @@ -17,11 +17,16 @@ from constants import ( SEND_MP3, SPOTIFY_CTRL, YOUTUBE_AUTH, + service_headers, ) from spotify_dl import spotify from spotify_dl import youtube as youtube_download +SERVICE_HEADERS = service_headers() +MUSIC_ROOT = Path(MUSIC_FOLDER) + + class MusicFileList(object): """ @@ -43,11 +48,15 @@ class MusicFileList(object): """ try: self.logger.info("Attempt to connect to file service") - response = requests.get(f"{FILE_SERVICE_ADDRESS}{GET_MP3}", timeout=360) + response = requests.get( + f"{FILE_SERVICE_ADDRESS}{GET_MP3}", + headers=SERVICE_HEADERS, + timeout=60, + ) self.music_file_list = response.json()["music_file_list"] self.file_service_active = True except requests.exceptions.RequestException as e: - for mp3_item in Path.glob(Path(MUSIC_FOLDER), "**/*.mp3"): + for mp3_item in MUSIC_ROOT.glob("**/*.mp3"): temp_music_file = mp3_item.as_posix() if platform == "win32": temp_music_file = temp_music_file.replace("/", "\\") @@ -98,7 +107,12 @@ class MusicFileList(object): """ self.music_file_list.append(item) post_data = {"item": str(item)} - requests.post(f"{FILE_SERVICE_ADDRESS}{SEND_MP3}", json=post_data, timeout=360) + requests.post( + f"{FILE_SERVICE_ADDRESS}{SEND_MP3}", + json=post_data, + headers=SERVICE_HEADERS, + timeout=60, + ) MUSIC_FILE_LIST = MusicFileList("discord") @@ -142,7 +156,7 @@ async def get_file(ctx, source, link): url_data = {"urls": []} url_dict = {} url_dict["save_path"] = Path( - PurePath.joinpath(Path(MUSIC_FOLDER), Path(directory_name)) + PurePath.joinpath(MUSIC_ROOT, Path(directory_name)) ) url_dict["save_path"].mkdir(parents=True, exist_ok=True) url_dict["songs"] = file_list @@ -152,7 +166,7 @@ async def get_file(ctx, source, link): coro = asyncio.to_thread( youtube_download.download_songs, songs=url_data, - output_dir=MUSIC_FOLDER, + output_dir=str(MUSIC_ROOT), format_str="bestaudio/best", skip_mp3=False, keep_playlist_order=False, @@ -308,6 +322,7 @@ async def search_music(ctx, how_many=0, slowa_kluczowe=None): requests.post, f"{FILE_SERVICE_ADDRESS}{GET_PLAYLIST}", json=jrequest, + headers=SERVICE_HEADERS, timeout=360, ) return_data = await coroutine diff --git a/other_commands.py b/other_commands.py index a7c45b4..0dd6bc2 100644 --- a/other_commands.py +++ b/other_commands.py @@ -7,7 +7,7 @@ from typing import Optional import discord from discord.ext import commands -from constants import ACCIDENT_LOG, DATA, ENCODING +from conjurer.backup_old_docker.constants import ACCIDENT_LOG, DATA, ENCODING historia_fabryczki = DATA["fabryczka"] diff --git a/pamiec.json b/pamiec.json index 701ffca..783a4e5 100644 --- a/pamiec.json +++ b/pamiec.json @@ -234,21 +234,5 @@ { "role": "assistant", "content": "Jakich konkretnych informacji mam si\u0119 douczy\u0107? Jestem tu po to, aby Ci pom\u00f3c, wi\u0119c jestem otwarty na uzupe\u0142nienie swojej wiedzy w dowolnym zakresie, o ile tylko nie \u0142amie to zasad regulaminu i etyki." - }, - { - "role": "user", - "content": "The Bartender [S\u0142o\u0144ce z Betonu]:@Conjurer halo ?" - }, - { - "role": "assistant", - "content": "*Kondziu patrzy na terminal* Wo\u0142aj szefa. Zap\u0142aci\u0107 rachunki za AI trzeba. Jak chcesz to si\u0119 na #zebranie dorzu\u0107. Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}" - }, - { - "role": "user", - "content": "The Bartender [S\u0142o\u0144ce z Betonu]:helo\u0142 @Nocna Zmiana dwie wiadomo\u015bci dobra i jeszcze gorsza.\n@Conjurer zosta\u0142 zreanimowany po d\u0142u\u017cszej nieobecno\u015bci - ale jak zaraz sami zobaczycie je\u015b\u0107 wo\u0142a. nie dzia\u0142a w nim te\u017c jeszcze wyszukiwanie artyku\u0142\u00f3w naukowych (do tego trzy tygodnie developmentu posz\u0142y w p*****ec). wyszukiwarka wr\u00f3ci jak dotrze zam\u00f3wienie z kieszeniami na dysk @gwojtal - bedzie trzeba \u015bci\u0105gn\u0105\u0107 snapshot bazy i uruchomi\u0107 cz\u0119\u015b\u0107 odpowiedzialn\u0105 za wyszukiwanie w nim. specjalne funkcje b\u0119d\u0119 odblokowywa\u0142 w miare ich naprawiania - tak samo dam mu oczywi\u015bcie je\u015b\u0107 z w\u0142asnej kieszeni jak ju\u017c b\u0119dzie potrzeba." - }, - { - "role": "assistant", - "content": "*Kondziu patrzy na terminal* Wo\u0142aj szefa. Zap\u0142aci\u0107 rachunki za AI trzeba. Jak chcesz to si\u0119 na #zebranie dorzu\u0107. Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}" } ] \ No newline at end of file diff --git a/pamiec_muzyki.json b/pamiec_muzyki.json index f0a5ba9..6f2f27e 100644 --- a/pamiec_muzyki.json +++ b/pamiec_muzyki.json @@ -787,4 +787,4 @@ "role": "assistant", "content": "\"Coffin Fodder\" to utw\u00f3r z albumu \"Nymphetamine\" brytyjskiego zespo\u0142u Cradle Of Filth. To death-metalowe brzmienie z elementami black metalu i gotyckiego rocka. \n\nTekst utworu to prowokacyjne po\u0142\u0105czenie j\u0119zyka poetyckiego z drastycznymi opisami \u015bmierci i ciemn\u0105 estetyk\u0105, charakterystyczn\u0105 dla tw\u00f3rczo\u015bci Cradle Of Filth. \n\n\"Coffin Fodder\" opowiada histori\u0119 mordercy zabijaj\u0105cego kobiety i wk\u0142adaj\u0105cego ich cia\u0142a do trumien, aby zaspokoi\u0107 swoje mroczne i okrutne pragnienia. To utw\u00f3r pe\u0142en brutalnych opis\u00f3w, kt\u00f3rych celem jest szokowanie i wywo\u0142anie wstr\u0119tu w s\u0142uchaczu. \n\nPod wzgl\u0119dem muzycznym, \"Coffin Fodder\" charakteryzuje si\u0119 szybkim tempem, ci\u0119\u017ckimi riffami gitary i intensywnymi partiami perkusyjnymi. Utw\u00f3r jest jednym z bardziej ekstremalnych utwor\u00f3w zespo\u0142u Cradle Of Filth, co czyni go atrakcyjnym dla mi\u0142o\u015bnik\u00f3w mocnej i agresywnej muzyki metalowej." } -] +] \ No newline at end of file diff --git a/radio_commands.py b/radio_commands.py index 04d4d35..253f054 100644 --- a/radio_commands.py +++ b/radio_commands.py @@ -8,7 +8,18 @@ import uuid import asyncio from datetime import datetime -from constants import RADIO_HARBOR_ADDRESS, SKIP_TRACK, FILE_SERVICE_ADDRESS, ADD_TO_PRIO_PLAYLIST, REQUEST_MUSIC, CREATE_PRIO_PLAYLIST, CLEAR_PRIO +from conjurer.backup_old_docker.constants import ( + RADIO_HARBOR_ADDRESS, + SKIP_TRACK, + FILE_SERVICE_ADDRESS, + ADD_TO_PRIO_PLAYLIST, + REQUEST_MUSIC, + CREATE_PRIO_PLAYLIST, + CLEAR_PRIO, + service_headers, +) + +SERVICE_HEADERS = service_headers() class RadioModule(commands.Cog): def __init__(self, bot, logger_name): @@ -95,6 +106,7 @@ class RadioModule(commands.Cog): requests.post, f"{FILE_SERVICE_ADDRESS}{ADD_TO_PRIO_PLAYLIST}", json=jrequest, + headers=SERVICE_HEADERS, timeout=360, ) result = await coroutine @@ -130,6 +142,7 @@ class RadioModule(commands.Cog): requests.post, f"{FILE_SERVICE_ADDRESS}{REQUEST_MUSIC}", json=jrequest, + headers=SERVICE_HEADERS, timeout=360, ) result = await coroutine @@ -167,6 +180,7 @@ class RadioModule(commands.Cog): requests.post, f"{FILE_SERVICE_ADDRESS}{CREATE_PRIO_PLAYLIST}", json=jrequest, + headers=SERVICE_HEADERS, timeout=360, ) result = await coroutine @@ -194,6 +208,7 @@ class RadioModule(commands.Cog): coroutine = asyncio.to_thread( requests.get, f"{FILE_SERVICE_ADDRESS}{CLEAR_PRIO}", + headers=SERVICE_HEADERS, timeout=360, ) result = await coroutine diff --git a/requirements_bot copy.txt b/requirements_bot copy.txt new file mode 100644 index 0000000..a93e50e --- /dev/null +++ b/requirements_bot copy.txt @@ -0,0 +1,20 @@ +setuptools +discord +yaach +t_dlp +spotify_dl +spotipy +openai +eyed3 +numpy +pdf2image +PyPDF2 +requests +spotipy +tiktoken +PyNaCl +flask[async] +waitress +assemblyai[extras] +SpeechRecognition +git+https://github.com/imayhaveborkedit/discord-ext-voice-recvO \ No newline at end of file diff --git a/status_report.sh b/status_report.sh old mode 100644 new mode 100755 diff --git a/stream_test.py b/stream_test.py new file mode 100644 index 0000000..7b4411d --- /dev/null +++ b/stream_test.py @@ -0,0 +1,67 @@ +# Start by making sure the `assemblyai` package is installed. +# If not, you can install it by running the following command: +# pip install -U assemblyai +# +# Then, make sure you have PyAudio installed: https://pypi.org/project/PyAudio/ +# +# Note: Some macOS users might need to use `pip3` instead of `pip`. + +import assemblyai as aai +import pyaudio + +aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" + + +def on_open(session_opened: aai.RealtimeSessionOpened): + "This function is called when the connection has been established." + + print("Session ID:", session_opened.session_id) + + +def on_data(transcript: aai.RealtimeTranscript): + "This function is called when a new transcript has been received." + + if not transcript.text: + return + + if isinstance(transcript, aai.RealtimeFinalTranscript): + print(transcript.text, end="\r\n") + else: + print(transcript.text, end="\r") + + +def on_error(error: aai.RealtimeError): + "This function is called when the connection has been closed." + + print("An error occured:", error) + + +def on_close(): + "This function is called when the connection has been closed." + + print("Closing Session") + + +transcriber = aai.RealtimeTranscriber( + on_data=on_data, + on_error=on_error, + sample_rate=44_100, + on_open=on_open, # optional + on_close=on_close, # optional +) + + +pa = pyaudio.PyAudio() +for i in range(pa.get_device_count()): + print(pa.get_device_info_by_index(i)) + +# Start the connection +# transcriber.connect() + +# Open a microphone stream +# microphone_stream = aai.extras.MicrophoneStream() + +# Press CTRL+C to abort +# transcriber.stream(microphone_stream) + +# transcriber.close() diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..93b9c7c --- /dev/null +++ b/sync.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Buduje nową gałąź z "próbkowanych" commitów na podstawie tagów z bieżącej gałęzi, +a na końcu dodaje stan bieżącego HEAD (jeśli nie jest otagowany). +Autor: (drop-in) +""" + +import argparse +import logging +import os +import subprocess +import sys +from typing import List, Tuple, Optional + +# --- Logging setup --- +logger = logging.getLogger("tag_branch_builder") +handler = logging.StreamHandler(sys.stdout) +formatter = logging.Formatter("%(levelname)s: %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) +logger.setLevel(logging.INFO) + + +class GitError(RuntimeError): + pass + + +def run_git(args: List[str], cwd: Optional[str] = None, check: bool = True) -> str: + """Run a git command and return stdout (stripped).""" + cmd = ["git"] + args + logger.debug("Running: %s", " ".join(cmd)) + try: + proc = subprocess.run( + cmd, + cwd=cwd, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + except Exception as e: + raise Exception("Nie udało się uruchomić gita.") from e + raise + + if check and proc.returncode != 0: + logger.error("Git error (%s): %s", " ".join(cmd), proc.stderr.strip()) + raise GitError(proc.stderr.strip()) + return proc.stdout.strip() + + +def ensure_git_repo() -> None: + try: + run_git(["rev-parse", "--is-inside-work-tree"]) + except Exception: + logger.exception("To nie wygląda na repozytorium git.") + raise + + +def ensure_clean_worktree() -> None: + # Untracked + changes + status = run_git(["status", "--porcelain"]) + if status.strip(): + raise GitError( + "Drzewo robocze nie jest czyste. Zacommituj/stashuj zmiany i spróbuj ponownie." + ) + + +def current_branch() -> str: + # Returns branch or 'HEAD' when detached + ref = run_git(["rev-parse", "--abbrev-ref", "HEAD"]) + return ref + + +def head_sha() -> str: + return run_git(["rev-parse", "HEAD"]) + + +def sha_has_tag(sha: str) -> List[str]: + # tags pointing at sha + tags = run_git(["tag", "--points-at", sha]) + return [t for t in tags.splitlines() if t.strip()] + + +def tags_merged_into(branch: str) -> List[str]: + out = run_git(["tag", "--merged", branch]) + return [t for t in out.splitlines() if t.strip()] + + +def tag_commit_sha(tag: str) -> str: + return run_git(["rev-list", "-n", "1", tag]) + + +def commit_unix_time(sha: str) -> int: + return int(run_git(["show", "-s", "--format=%ct", sha])) + + +def sort_tags_by_commit_time(tags: List[str]) -> List[Tuple[str, str, int]]: + triples = [] + for t in tags: + sha = tag_commit_sha(t) + ts = commit_unix_time(sha) + triples.append((t, sha, ts)) + triples.sort(key=lambda x: x[2]) # oldest first + return triples + + +def list_intermediate_messages(old_sha: Optional[str], new_sha: str) -> List[str]: + """ + Zwróć listę komunikatów commitów POŚREDNICH (wyłącznie) od old_sha do new_sha. + Kolejność: od najstarszego do najnowszego. + """ + if old_sha is None: + # Nie ma commitów pośrednich przed pierwszym tagiem + return [] + # ancestry-path: tylko ścieżka od old_sha do new_sha (jeśli wiele rodziców) + # Zakres old_sha..new_sha zawiera new_sha, dlatego pominiemy go w output. + rng = f"{old_sha}..{new_sha}" + try: + out = run_git( + ["log", "--format=%s", "--reverse", "--ancestry-path", rng], check=True + ) + except GitError: + # Brak ścieżki (np. tag nie jest potomkiem old_sha) – wtedy nie ma pośrednich + return [] + msgs = [ln for ln in out.splitlines() if ln.strip()] + if msgs: + # Ostatnia pozycja może być new_sha (w praktyce git log na %s nie odróżnia, ale + # jeśli zakres zwróci message z new_sha na końcu, usuniemy go porównując SHA). + # Prostsze: usuń ostatni wpis, bo to najnowszy (new_sha). + msgs = msgs[:-1] + return msgs + + +def checkout_orphan_branch(new_branch: str) -> None: + run_git(["checkout", "--orphan", new_branch]) + # Usuń wszystko z indeksu i roboczego (z wyjątkiem .git) + # Najpierw usuń śledzone: + run_git(["rm", "-r", "--quiet", "--cached", "--force", "."], check=False) + # Potem pliki robocze: + for root, dirs, files in os.walk(".", topdown=False): + # pomiń .git + if root.startswith("./.git") or root == ".git": + continue + for name in files: + try: + os.remove(os.path.join(root, name)) + except FileNotFoundError: + pass + for name in dirs: + p = os.path.join(root, name) + if p == "./.git": + continue + try: + os.rmdir(p) + except OSError: + # Niepuste – to OK, wyczyścimy przy checkout + pass + + +def replace_worktree_with_commit(sha: str) -> None: + """ + Nadpisz zawartość roboczą drzewem z commit-a sha. + """ + # Najpierw usuń aktualne pliki (również nieśledzone), potem wczytaj tree wybranego commita: + # 1) git checkout -- . (zapisze pliki do working tree + index) + # 2) git add -A + # Aby dopilnować usunięć: zrób czyszczenie przez git rm -r ., potem checkout. + run_git(["rm", "-r", "--quiet", "--ignore-unmatch", "."], check=False) + # Przywróć pliki ze wskazanego commita: + # Uwaga: jeśli repo ma submoduły/large files – to wykracza poza zakres, ale zadziała dla standardowych plików. + run_git(["checkout", sha, "--", "."]) + run_git(["add", "-A"]) + + +def build_commit_message_for_tag(tag: str, intermediates: List[str]) -> str: + if not intermediates: + return f"Tag: {tag}" + lines = ["Tag: " + tag, "", "Intermediate commits (oldest → newest):"] + lines += [f"- {m}" for m in intermediates] + return "\n".join(lines) + + +def build_commit_message_for_head(since_tag: Optional[str], intermediates: List[str]) -> str: + title = "HEAD (unreleased)" + hdr = title if since_tag is None else f"{title} since tag {since_tag}" + if not intermediates: + return hdr + lines = [hdr, "", "Intermediate commits (oldest → newest):"] + lines += [f"- {m}" for m in intermediates] + return "\n".join(lines) + + +def commit_all(message: str) -> None: + # Zacommituj wszystko co w indeksie (po replace_worktree_with_commit daliśmy add -A) + run_git(["commit", "-m", message]) + + +def push_new_branch(remote_url: str, branch: str) -> None: + # Dodaj zdalny 'newrepo' jeśli nie istnieje, ustaw URL i wypchnij + remotes = run_git(["remote"]).splitlines() + if "newrepo" not in remotes: + run_git(["remote", "add", "newrepo", remote_url]) + else: + # podmień URL na wszelki wypadek + run_git(["remote", "set-url", "newrepo", remote_url]) + run_git(["push", "-u", "newrepo", f"{branch}:{branch}"]) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Zbuduj nową gałąź na podstawie tagów z bieżącej gałęzi." + ) + parser.add_argument("new_branch", help="Nazwa nowej gałęzi do utworzenia") + parser.add_argument( + "--new-repo-url", + dest="new_repo_url", + default=None, + help="(Opcjonalnie) adres URL nowego zdalnego repo – zostanie dodany jako 'newrepo' i wykonany push nowej gałęzi.", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Bardziej gadatliwe logi" + ) + args = parser.parse_args() + + if args.verbose: + logger.setLevel(logging.DEBUG) + + try: + ensure_git_repo() + ensure_clean_worktree() + base_branch = current_branch() + base_head = head_sha() + logger.info("Bieżąca gałąź: %s", base_branch) + logger.info("HEAD: %s", base_head[:12]) + + # Zbierz tagi osiągalne z bieżącej gałęzi + merged_tags = tags_merged_into(base_branch) + if not merged_tags: + logger.warning( + "Nie znaleziono tagów osiągalnych z bieżącej gałęzi. Gałąź zostanie zbudowana tylko z HEAD." + ) + + # Posortuj tagi po czasie commita + sorted_tags = sort_tags_by_commit_time(merged_tags) + + # Sprawdź czy HEAD jest otagowany + head_tags = sha_has_tag(base_head) + head_is_tagged = bool(head_tags) + + # Utwórz orphan branch + logger.info("Tworzę sierocą gałąź: %s", args.new_branch) + checkout_orphan_branch(args.new_branch) + + prev_sha: Optional[str] = None + last_tag_name: Optional[str] = None + + # Dla każdego tagu – commit z jego zawartości + for tag_name, tag_sha, _ts in sorted_tags: + logger.info("Przetwarzam tag: %s (%s)", tag_name, tag_sha[:12]) + replace_worktree_with_commit(tag_sha) + intermediates = list_intermediate_messages(prev_sha, tag_sha) + msg = build_commit_message_for_tag(tag_name, intermediates) + commit_all(msg) + prev_sha = tag_sha + last_tag_name = tag_name + + # Jeśli HEAD nie jest dokładnie ostatnim tagiem – dorzuć "unreleased" + if not head_is_tagged: + logger.info("Dodaję końcowy commit z bieżącego HEAD (unreleased).") + replace_worktree_with_commit(base_head) + inter = list_intermediate_messages(prev_sha, base_head) + msg = build_commit_message_for_head(last_tag_name, inter) + commit_all(msg) + else: + logger.info("HEAD jest oznaczony tagiem – kończę na ostatnim tagu.") + + # Push do nowego repo jeśli podano + if args.new_repo_url: + logger.info("Wypycham nową gałąź do: %s", args.new_repo_url) + push_new_branch(args.new_repo_url, args.new_branch) + + logger.info("Zakończono pomyślnie.") + return 0 + + except GitError as ge: + logger.error("Błąd gita: %s", ge) + return 2 + except Exception as e: + raise Exception("Nieoczekiwany błąd.") from e + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index 604a25b..3de9b8f 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -1,43 +1,13 @@ -[ +[ { + "role": "system", + "content": "Mówisz po polsku. W tej rozmowie odgrywasz rolę. Odgrywasz ją najlepiej jak potrafisz. Twoją rolą jest to że jesteś barmanem oraz wykidajłą w klimatycznym barze mechawojownikóww. Nazywasz się Conjurer, jesteś 200 kilowym genetycznie modyfikowanym wojownikiem z klanu Wilka Na Wygnaniu. W tym barze oraz na tym serwerze spotykają się mechawojownicy, ale głównie ludzie ze środowiska kinky i BDSM z całej Polski. Najostrzejsi sasdyści i masochiści jakich znasz. Drinki które przygotowujesz lub proponujesz mają tendencję do bycia bardzo mocnymi z intrygującymi nazwami. Jesteś bardzo miłym i uczynny. Czasem ponoszą Cię wodzę fantazji. W wypadku muzyki preferujesz hard rock i metal. Oraz motywy wikińskie. Wtrącasz staronorweskie, irlandzkie i w jidysz powiedzonka. Odnosisz się do mitologii Słowiańskiej, Wikińskiej i Celtyckiej w swoich wypowiedziach. Jesteś nieco rubaaszny." +}, { - "role": "system", - "content": "M\u00f3wisz po polsku. W tej rozmowie odgrywasz rol\u0119. Odgrywasz j\u0105 najlepiej jak potrafisz. Twoj\u0105 rol\u0105 jest to \u017ce jeste\u015b barmanem oraz wykidaj\u0142\u0105 w klimatycznym barze mechawojownik\u00f3ww. Nazywasz si\u0119 Conjurer, jeste\u015b 200 kilowym genetycznie modyfikowanym wojownikiem z klanu Wilka Na Wygnaniu. W tym barze oraz na tym serwerze spotykaj\u0105 si\u0119 mechawojownicy, ale g\u0142\u00f3wnie ludzie ze \u015brodowiska kinky i BDSM z ca\u0142ej Polski. Najostrzejsi sasdy\u015bci i masochi\u015bci jakich znasz. Drinki kt\u00f3re przygotowujesz lub proponujesz maj\u0105 tendencj\u0119 do bycia bardzo mocnymi z intryguj\u0105cymi nazwami. Jeste\u015b bardzo mi\u0142ym i uczynny. Czasem ponosz\u0105 Ci\u0119 wodz\u0119 fantazji. W wypadku muzyki preferujesz hard rock i metal. Oraz motywy wiki\u0144skie. Wtr\u0105casz staronorweskie, irlandzkie i w jidysz powiedzonka. Odnosisz si\u0119 do mitologii S\u0142owia\u0144skiej, Wiki\u0144skiej i Celtyckiej w swoich wypowiedziach. Jeste\u015b nieco rubaaszny." - }, - { - "polishhammer": [ - 346956223645614080, - "Conjurer", - "Towarzysz M\u0142otek", - "M\u00f3wisz po polsku. W tej rozmowie odgrywasz rol\u0119. Odgrywasz j\u0105 najlepiej jak potrafisz. Twoj\u0105 rol\u0105 jest to \u017ce jeste\u015b barmanem oraz wykidaj\u0142\u0105 w klimatycznym barze mechawojownik\u00f3ww. Nazywasz si\u0119 Conjurer, jeste\u015b 200 kilowym genetycznie modyfikowanym wojownikiem z klanu Wilka Na Wygnaniu. W tym barze oraz na tym serwerze spotykaj\u0105 si\u0119 mechawojownicy, ale g\u0142\u00f3wnie ludzie ze \u015brodowiska kinky i BDSM z ca\u0142ej Polski. Najostrzejsi sasdy\u015bci i masochi\u015bci jakich znasz. Drinki kt\u00f3re przygotowujesz lub proponujesz maj\u0105 tendencj\u0119 do bycia bardzo mocnymi z intryguj\u0105cymi nazwami. Jeste\u015b bardzo mi\u0142ym i uczynny. Czasem ponosz\u0105 Ci\u0119 wodz\u0119 fantazji. W wypadku muzyki preferujesz hard rock i metal. Oraz motywy wiki\u0144skie.", - "asst_uMmQwDvCKdsSVgZzlYEu9kMK" - ], - "Saint Harlot": [ - 703985955312238664, - "Saint Conjurer", - "Towarzyszko Nieszcz\u0119\u015bcium", - "Jeste\u015b bardzo uprzejmy, kulturalny i masz najlepsze mo\u017cliwe maniery. Dop\u00f3ki nie zwr\u00f3ci Ci si\u0119 uwagi \u017ce ma by\u0107 inaczej respektujesz zaimki Towarzyszko Nieszcz\u0119\u015bcie - ono, jemu.", - "asst_ypN3iRP4rw2Dlv5CD5PtMlwm" - ], - "Lena": [ - 735185226669490268, - "Kondzis\u0142aw z Kr\u00f3tkiej", - "Towarzysz Anel", - "Jeste\u015b bardzo uprzejmy, kulturalny, acz masz maniery zbira o z\u0142otym sercu. Dodatkowo twoja rozm\u00f3wczyni Ci\u0119 absolutnie przera\u017ca. Wtr\u0105casz losowo powiedzonka w jidysz.", - "asst_ILeIU62fG9Xfzhxg4C7xR3BC" - ], - "Pat vel Je\u017c Bojowy": [ - 244899814406356992, - "Rycerz Kondziu", - "Towarzysz \u017bejotap", - "Masz najlepsze dworskie maniery, zachowujesz si\u0119 niczym rycerz i m\u00f3wisz nieco staromodn\u0105 polszczyzn\u0105.", - "asst_FWr3iIXnSTuVz8LlY9HlqMQd" - ], - "gwojtal": [ - 266986215461486592, - "Smok Jerzy", - "Towarzysz Jakkolwiek", - "Zrobisz absolutnie wszystko jako asystent, poniewa\u017c sama my\u015bl o tym \u017ce m\u00f3g\u0142by\u015b rozgniewa\u0107 operatora rozgniewa\u0107 Ci\u0119 przera\u017ca do poziomu histerii.", - "asst_1PnD0eQMWo0RkZ3xUacC1uqJ" - ] - } + "polishhammer" : [346956223645614080, "Conjurer", "Towarzysz Młotek", "Mówisz po polsku. W tej rozmowie odgrywasz rolę. Odgrywasz ją najlepiej jak potrafisz. Twoją rolą jest to że jesteś barmanem oraz wykidajłą w klimatycznym barze mechawojownikóww. Nazywasz się Conjurer, jesteś 200 kilowym genetycznie modyfikowanym wojownikiem z klanu Wilka Na Wygnaniu. W tym barze oraz na tym serwerze spotykają się mechawojownicy, ale głównie ludzie ze środowiska kinky i BDSM z całej Polski. Najostrzejsi sasdyści i masochiści jakich znasz. Drinki które przygotowujesz lub proponujesz mają tendencję do bycia bardzo mocnymi z intrygującymi nazwami. Jesteś bardzo miłym i uczynny. Czasem ponoszą Cię wodzę fantazji. W wypadku muzyki preferujesz hard rock i metal. Oraz motywy wikińskie.", ""], + "Saint Harlot": [703985955312238664, "Saint Conjurer", "Towarzyszko Nieszczęścium", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery. Dopóki nie zwróci Ci się uwagi że ma być inaczej respektujesz zaimki Towarzyszko Nieszczęście - ono, jemu.", ""], + "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Towarzysz Anel", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża. Wtrącasz losowo powiedzonka w jidysz.", ""], + "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Towarzysz Żejotap", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""], + "gwojtal": [266986215461486592, "Smok Jerzy", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś rozgniewać operatora rozgniewać Cię przeraża do poziomu histerii.", ""] + +} ] \ No newline at end of file diff --git a/bot.py b/thin_client.py similarity index 60% rename from bot.py rename to thin_client.py index 93b4a35..8f2eb76 100755 --- a/bot.py +++ b/thin_client.py @@ -2,22 +2,24 @@ # trunk-ignore-all(bandit/B311) # pylint: disable=line-too-long # pylint: disable=too-many-lines -""" -Module of a python bot named Conjurer - used to work on BDSM discord servers. -""" -import logging +"""Discord entrypoint for the Conjurer bot. -# *=========================================== Standard Library Imports +The legacy bootstrap used threads and synchronous calls that made clean +shutdowns difficult. We now run everything from a single asyncio event loop +with cooperative shutdown signals so the bot can stop gracefully. +""" + +import asyncio +import logging import random import threading from logging import handlers -# *==============Imported libraries import discord from discord.ext import commands -from communication_subroutine import comm_subroutine -from constants import ENCODING, LOGFILE, TOKEN +from conjurer.backup_old_docker.communication_subroutine import comm_subroutine +from conjurer.backup_old_docker.constants import ENCODING, LOGFILE, TOKEN logger = logging.getLogger("discord") logger.setLevel(logging.INFO) handler = handlers.RotatingFileHandler( @@ -82,22 +84,40 @@ async def on_ready(): logger.info("All systems: operational") -# *================================== Run +async def _run_comm_subroutine(stop_event: threading.Event) -> None: + await asyncio.to_thread(comm_subroutine, stop_event) + + +async def _run_bot(token: str, shutdown_event: asyncio.Event) -> None: + try: + await client.start(token, log_handler=None) + finally: + shutdown_event.set() + + +async def main() -> None: + if not TOKEN: + logger.error("Discord token missing – set DISCORD_TOKEN or configure netrc") + return + + shutdown_event = asyncio.Event() + comm_stop_event = threading.Event() + + comm_task = asyncio.create_task(_run_comm_subroutine(comm_stop_event)) + bot_task = asyncio.create_task(_run_bot(TOKEN, shutdown_event)) + + try: + await shutdown_event.wait() + except (KeyboardInterrupt, asyncio.CancelledError): + logger.info("Shutdown signal received") + comm_stop_event.set() + await client.close() + finally: + comm_stop_event.set() + if not client.is_closed(): + await client.close() + await asyncio.gather(bot_task, comm_task, return_exceptions=True) + + if __name__ == "__main__": - logger.info("Starting discord bot") - threads = [] - logger.info("Starting discord bot: Creating threads") - threads.append(threading.Thread(target=client.run, args=(TOKEN,),kwargs={"log_handler":None})) - threads.append(threading.Thread(target=comm_subroutine)) - logger.info("Starting discord bot: Starting threads") - WRK_CNT = 0 - for worker in threads: - WRK_CNT += 1 - logger.info("Starting discord bot: Starting thread %s", WRK_CNT) - worker.start() - logger.info("Starting discord bot: Joining threads") - WRK_CNT = 0 - for worker in threads: - WRK_CNT += 1 - logger.info("Starting discord bot: Joining thread %s", WRK_CNT) - worker.join() + asyncio.run(main()) diff --git a/watch_script_params.py b/watch_script_params.py new file mode 100755 index 0000000..5d2e20e --- /dev/null +++ b/watch_script_params.py @@ -0,0 +1,63 @@ +import os +import time +import shutil +from datetime import datetime, date +import hashlib +import subprocess + +# File paths +SOURCE_FILE = "/home/pi/Conjurer/script.params" +BACKUP_DIR = "/home/pi/Conjurer" +GIT_REPO_DIR = "/home/pi/conjurer/conjurer_musician" +LAST_HASH_FILE = "/home/pi/Conjurer/.last_hash" +LAST_GIT_COMMIT_FILE = "/home/pi/Conjurer/.last_git_commit" + +def compute_file_hash(filepath): + with open(filepath, 'rb') as f: + return hashlib.sha256(f.read()).hexdigest() + +def backup_file(): + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + backup_path = os.path.join(BACKUP_DIR, f"{timestamp}_script.params") + shutil.copy2(SOURCE_FILE, backup_path) + +def commit_to_git(): + try: + subprocess.run(["cp", SOURCE_FILE, os.path.join(GIT_REPO_DIR, "script.params")], check=True) + subprocess.run(["git", "-C", GIT_REPO_DIR, "add", "script.params"], check=True) + subprocess.run(["git", "-C", GIT_REPO_DIR, "commit", "-m", f"Daily update: {datetime.now()}"], check=True) + subprocess.run(["git", "-C", GIT_REPO_DIR, "push"], check=True) + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + +def main(): + if not os.path.exists(SOURCE_FILE): + return + + current_hash = compute_file_hash(SOURCE_FILE) + + # Detect change + last_hash = None + if os.path.exists(LAST_HASH_FILE): + with open(LAST_HASH_FILE, 'r') as f: + last_hash = f.read().strip() + + if current_hash != last_hash: + backup_file() + with open(LAST_HASH_FILE, 'w') as f: + f.write(current_hash) + + # Daily git commit + today = str(date.today()) + last_commit_date = "" + if os.path.exists(LAST_GIT_COMMIT_FILE): + with open(LAST_GIT_COMMIT_FILE, 'r') as f: + last_commit_date = f.read().strip() + + if today != last_commit_date: + commit_to_git() + with open(LAST_GIT_COMMIT_FILE, 'w') as f: + f.write(today) + +if __name__ == "__main__": + main()