From b87cf7f4d8f4344de89276a320e56ff5a4ed9a49 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Mon, 22 Jul 2024 16:20:06 +0200 Subject: [PATCH 001/283] Changes for too long messages handling --- bot.py | 178 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 117 insertions(+), 61 deletions(-) diff --git a/bot.py b/bot.py index bba940f..6706ded 100644 --- a/bot.py +++ b/bot.py @@ -16,10 +16,10 @@ import os import random import re import shutil +import subprocess import sys import threading import uuid -import subprocess from datetime import datetime from logging import handlers from pathlib import Path, PurePath @@ -42,11 +42,15 @@ from discord.ext import commands, tasks from spotipy.oauth2 import SpotifyClientCredentials import yt_dlp +from communication_subroutine import ( + IN_COMM_Q, + OUT_COMM_Q, + QueryControl, + comm_subroutine, +) from spotify_dl import spotify from spotify_dl import youtube as youtube_download -from communication_subroutine import comm_subroutine, IN_COMM_Q, OUT_COMM_Q, QueryControl - # *=========================== Classes definition # The above code is defining a TypedDict called `Music_Config` in Python. This TypedDict has three @@ -63,7 +67,6 @@ Music_Config = TypedDict( ) - class MusicFileList(object): """ The `MusicFileList` class manages a list of music files, with the ability to refresh the list from a @@ -284,7 +287,6 @@ for key in word_reactions: logger.info("Done") - # *=========================================== Create Client logger.info("Creating discord bot") client = commands.Bot(intents=intents, command_prefix="$") @@ -404,7 +406,13 @@ async def on_message(message): result, MESSAGE_TABLE = await handle_response( prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" ) - await message.reply(result) + + if len(result) < 1500: + await message.reply(result) + else: + while len(result) > 1500: + await message.reply(result[:1500]) + result = result[1500:] if "imaginuje sobie:" in message.content: async with channel.typing(): logger.info("Poczatek procedury obrazkowej") @@ -637,7 +645,12 @@ async def play(ctx, zamawial=None, arg=None): query, vykidailo, bartender, message_table_muzyka, username, "MUSIC" ) logger.debug("Obecna historia czatu: %s", message_table_muzyka) - await ctx.send(result) + if len(result) < 1500: + await ctx.send(result) + else: + while len(result) > 1500: + await ctx.send(result[:1500]) + result = result[1500:] async def archive_channel(channel_no): @@ -676,6 +689,7 @@ async def check_music(): await play(MUZYKA["ctx"], MUZYKA["requester"]) await asyncio.sleep(2) + @tasks.loop(seconds=3) async def check_data_q(): """ @@ -691,40 +705,42 @@ async def check_data_q(): for doi in fresh_data.entries: logger.info(doi) desc = fresh_data.entries[doi] - title = desc['Title'][0] - entries.append(f"{l_p}. {title} pod linkiem https://www.sci-hub.se/{doi} i jest to {desc['type']}\n") - l_p+=1 - message = '*Z podłogi wysuwa się winda na książki*' + title = desc["Title"][0] + entries.append( + f"{l_p}. {title} pod linkiem https://www.sci-hub.se/{doi} i jest to {desc['type']}\n" + ) + l_p += 1 + message = "*Z podłogi wysuwa się winda na książki*" if fresh_data.ctx is not None: ctx = fresh_data.ctx - message += f' Dzielny poszukiwaczu @{searcher} w odpowiedzi na twoje zapytanie {query} nr {fresh_data.uuid}' + message += f" Dzielny poszukiwaczu @{searcher} w odpowiedzi na twoje zapytanie {query} nr {fresh_data.uuid}" else: ctx = client.get_channel(1062047571557744721) - message += f' Mam wynik dla zapytania {fresh_data.uuid} ale że Hammer coś grzebał nie pamiętam kto to chciał i co chciał' + message += f" Mam wynik dla zapytania {fresh_data.uuid} ale że Hammer coś grzebał nie pamiętam kto to chciał i co chciał" - - message += 'nasza biblioteka służy Ci odpowiedzią. Przy dźwięku fanfar winda się otwiera a w środku ' - if len(entries)<1: + message += "nasza biblioteka służy Ci odpowiedzią. Przy dźwięku fanfar winda się otwiera a w środku " + if len(entries) < 1: message += "niestety nie ma nic" await ctx.send(message) - elif len(entries)>=1 and len(entries)<5: + elif len(entries) >= 1 and len(entries) < 5: message += " znajduje się coś:\n" for item in entries: - message+=item + message += item await ctx.send(message) else: - message+= " znajduje się cholernie dużo:\n" + message += " znajduje się cholernie dużo:\n" for item in entries: - message+=item + message += item if len(message) > 1500: await ctx.send(message) message = "" - #Kept for sentimental reasons - #await ctx.send(f"O. A tak będzie wyglądało coś ciekawego w przyszłości: {data}") + # Kept for sentimental reasons + # await ctx.send(f"O. A tak będzie wyglądało coś ciekawego w przyszłości: {data}") except Empty: pass + @tasks.loop(seconds=120) async def check_self(): """ @@ -1082,9 +1098,13 @@ async def get_file(ctx, source, link): logger.info(item_type) logger.info(item_id) logger.info("Spotify link provided") - file_list = await asyncio.to_thread(spotify.fetch_tracks, spotify_ctrl, item_type, link) + file_list = await asyncio.to_thread( + spotify.fetch_tracks, spotify_ctrl, item_type, link + ) logger.info(file_list) - directory_name = await asyncio.to_thread(spotify.get_item_name, spotify_ctrl, item_type, item_id) + directory_name = await asyncio.to_thread( + spotify.get_item_name, spotify_ctrl, item_type, item_id + ) logger.info(directory_name) logger.info("Spotify scrape done") url_data = {"urls": []} @@ -1351,6 +1371,7 @@ async def max_weight(lista): # *=========================================== Define Commands + @client.hybrid_command( name="skip_track", description="Przeskocz kawałek w radiu", @@ -1360,15 +1381,15 @@ async def skip_track(ctx): async with ctx.typing(): allowed = False for role in ctx.author.roles: - if role.name in ("Thane","Jarl"): + if role.name in ("Thane", "Jarl"): allowed = True if not allowed: await ctx.send("Łapy precz od radia") else: coroutine = asyncio.to_thread( - requests.get, - f"{RADIO_HARBOR_ADDRESS}{SKIP_TRACK}", - timeout=360, + requests.get, + f"{RADIO_HARBOR_ADDRESS}{SKIP_TRACK}", + timeout=360, ) result = await coroutine logger.info("Done %s", result) @@ -1384,13 +1405,19 @@ async def zrestartuj_radio(ctx): async with ctx.typing(): allowed = False for role in ctx.author.roles: - if role.name in ("Thane","Jarl"): + if role.name in ("Thane", "Jarl"): allowed = True if not allowed: await ctx.send("Łapy precz od radia") else: - retcode = subprocess.run("/home/pi/Conjurer/restart_radio.sh", shell=False, check = False, capture_output = True) - logger.info("Wynik: %s",retcode) + retcode = subprocess.run( + "/home/pi/Conjurer/restart_radio.sh", + shell=False, + check=False, + capture_output=True, + ) + logger.info("Wynik: %s", retcode) + @client.hybrid_command( name="dej_co_ze_spotifaja", @@ -1633,6 +1660,7 @@ async def graj_muzyko(ctx): logger.info("Press play on tape completed") check_music.start() + @client.hybrid_command( name="radio_hardkor", description="Włącza radio Conjurer na kanale #nocna-zmiana.", @@ -1657,9 +1685,10 @@ async def radio_hardkor(ctx): voice_client = client.voice_clients[0] voice_client.play( discord.PCMVolumeTransformer( - discord.FFmpegPCMAudio("http://95.175.16.246:666/mp3-stream"), volume=0.3 - ) + discord.FFmpegPCMAudio("http://95.175.16.246:666/mp3-stream"), + volume=0.3, ) + ) logger.info("Press play on radio completed") else: logger.error("Already playing") @@ -1667,7 +1696,6 @@ async def radio_hardkor(ctx): check_music.start() - @client.hybrid_command( name="cisza", description="Wyłącza muzykę i czyści plejliste.", @@ -1810,6 +1838,7 @@ async def zagraj_mi_kawalek(ctx): "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" ) + @client.hybrid_command( name="dodaj_do_ulubionych", description="Dodaje do listy ulubionych w radiu", @@ -1844,6 +1873,8 @@ async def dodaj_do_ulubionych(ctx): result = await coroutine logger.info("Done %s", result) await ctx.send("Zrobione szefie!") + + @client.hybrid_command( name="ja_chciol", description="Dodaje do listy ulubionych w radiu", @@ -1877,6 +1908,7 @@ async def request_radio(ctx): logger.info("Done %s", result) await ctx.send("Zrobione szefie!") + @client.hybrid_command( name="stworz_audycje", description="Dodaje do listy ulubionych w radiu", @@ -2316,11 +2348,18 @@ async def wyszukaj_linki_do_dokumentow(ctx): """ query = ctx.message.content query_uuid = uuid.uuid4() - #TODO: TESTING ONLY!! - #query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') - ctx.message.content = ctx.message.content.replace("$wyszukaj_linki_do_dokumentow", "") + # TODO: TESTING ONLY!! + # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') + ctx.message.content = ctx.message.content.replace( + "$wyszukaj_linki_do_dokumentow", "" + ) - json_query = {"UUID": str(query_uuid), "query": str(query), "page": 1, "deep_search": False} + json_query = { + "UUID": str(query_uuid), + "query": str(query), + "page": 1, + "deep_search": False, + } coroutine = asyncio.to_thread( requests.post, f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", @@ -2328,14 +2367,16 @@ async def wyszukaj_linki_do_dokumentow(ctx): timeout=360, ) await ctx.send( - "*Conjurer powoli notuje podane przez Ciebie. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + - " wrzuca twój liścik do środka* To teraz trza poczekać kilka godzin. Biblioteka to 3/4 stacji."+ - " A przecież pół stacji to browar, motelik dla zabaw cielesnych i ten Bar. Więc 12/8 stacji teraz będzie ciężko pracować" + "*Conjurer powoli notuje podane przez Ciebie. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + + " wrzuca twój liścik do środka* To teraz trza poczekać kilka godzin. Biblioteka to 3/4 stacji." + + " A przecież pół stacji to browar, motelik dla zabaw cielesnych i ten Bar. Więc 12/8 stacji teraz będzie ciężko pracować" ) query_response = await coroutine if not query_response.status_code == 200: - await ctx.send("*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić."+ - " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało") + await ctx.send( + "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." + + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" + ) return query, query_uuid, queue_size = ( @@ -2349,8 +2390,11 @@ async def wyszukaj_linki_do_dokumentow(ctx): username = ctx.message.author.name query_object = QueryControl(username, query_uuid, query, logger, ctx) OUT_COMM_Q.put(query_object) - await ctx.send(f"No dobra poszło. Zapamiętaj proszę {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce."+ - " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni.") + await ctx.send( + f"No dobra poszło. Zapamiętaj proszę {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce." + + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni." + ) + @client.hybrid_command( name="glebokie_gardlo", @@ -2394,13 +2438,23 @@ async def wyszukaj_linki_do_dokumentow_deep(ctx): result, MESSAGE_TABLE = await handle_response( prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" ) - await ctx.send(result) + if len(result) < 1500: + await ctx.send(result) + else: + while len(result) > 1500: + await ctx.send(result[:1500]) + result = result[1500:] return query_uuid = uuid.uuid4() - #TODO: TESTING ONLY!! - #query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') - json_query = {"UUID": str(query_uuid), "query": str(query), "page": 1, "deep_search": True} + # TODO: TESTING ONLY!! + # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') + json_query = { + "UUID": str(query_uuid), + "query": str(query), + "page": 1, + "deep_search": True, + } coroutine = asyncio.to_thread( requests.post, f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", @@ -2408,14 +2462,16 @@ async def wyszukaj_linki_do_dokumentow_deep(ctx): timeout=360, ) await ctx.send( - "*Conjurer mruga okiem i zamiast drinka wyjmuje dysk usb z terminala. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + - " wrzuca ten dysk do środk* To teraz trza poczekać kilka godzin. TO będzie głębokie wyszukanie"+ - " Rozgląda się dookoła i stawia przed tobą mętnego drinka o zapachu mięty, gwiezdnego pyłu i oleju silnikowego" + "*Conjurer mruga okiem i zamiast drinka wyjmuje dysk usb z terminala. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + + " wrzuca ten dysk do środk* To teraz trza poczekać kilka godzin. TO będzie głębokie wyszukanie" + + " Rozgląda się dookoła i stawia przed tobą mętnego drinka o zapachu mięty, gwiezdnego pyłu i oleju silnikowego" ) query_response = await coroutine if not query_response.status_code == 200: - await ctx.send("*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić."+ - " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało") + await ctx.send( + "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." + + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" + ) return query, query_uuid, queue_size = ( @@ -2429,9 +2485,10 @@ async def wyszukaj_linki_do_dokumentow_deep(ctx): username = ctx.message.author.name query_object = QueryControl(username, query_uuid, query, logger, ctx) OUT_COMM_Q.put(query_object) - await ctx.send(f"Wypij wypi {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce niestety kolejka obowiazuje zawsze."+ - " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzin - ale mogą być też dni. Głębokie zabawy trwają dłużej. *Znowu mruga*") - + await ctx.send( + f"Wypij wypi {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce niestety kolejka obowiazuje zawsze." + + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzin - ale mogą być też dni. Głębokie zabawy trwają dłużej. *Znowu mruga*" + ) @client.hybrid_command( @@ -2468,7 +2525,6 @@ async def krecimy_pornola(ctx): ctx.reply("Jak ładnie szefa poprosisz.") - # *================================== Run if __name__ == "__main__": logger.info("Starting discord bot") @@ -2479,12 +2535,12 @@ if __name__ == "__main__": logger.info("Starting discord bot: Starting threads") WRK_CNT = 0 for worker in threads: - WRK_CNT+=1 + 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 + WRK_CNT = 0 for worker in threads: - WRK_CNT+=1 + WRK_CNT += 1 logger.info("Starting discord bot: Joining thread %s", WRK_CNT) worker.join() From 2c9c86990cdb1a40432a185a840ef1aca38f7189 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Mon, 29 Jul 2024 14:20:11 +0200 Subject: [PATCH 002/283] Track name broadcast att 1 --- conjurer_musician/conjurer_musician.py | 34 +++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index a993d57..ca8dcc3 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -6,6 +6,7 @@ API endpoints. """ import json import logging +import os import re import threading import time @@ -45,7 +46,8 @@ def rescan(): The `rescan` function logs a message, scans for mp3 files in a specified folder, and adds them to a list of music files. """ - logging.info("Rescan triggered") + logger = logging.getLogger("conjurer_musician") + logger.info("Rescan triggered") for mp3_item in Path.glob(Path(MUSIC_FOLDER), "**/*.mp3"): temp_music_file = mp3_item.as_posix() @@ -82,13 +84,38 @@ def thread_rescan(): The `thread_rescan` function periodically triggers a rescan operation after a specified time interval. """ - logging.info("Starting filesystemupdater") + logger = logging.getLogger("conjurer_musician") + logger.info("Starting filesystemupdater") while True: time.sleep(60 * 60 * 24) - logging.info("Rescan triggered") + logger.info("Rescan triggered") rescan() +def scan_tracks(): + #Set the filename and open the file + logger = logging.getLogger("conjurer_musician") + + filename = '/home/pi/Conjurer/radio_log.log' + file = open(filename,'r') + + #Find the size of the file and move to the end + st_results = os.stat(filename) + st_size = st_results[6] + file.seek(st_size) + + while 1: + where = file.tell() + line = file.readline() + if not line: + time.sleep(1) + file.seek(where) + else: + if re.match(".*Prepared.*",line): + logger.info(line) # already has newline + time.sleep(0.1) + + app = Flask(__name__) @@ -485,6 +512,7 @@ if __name__ == "__main__": # threads.append(threading.Thread(target=flask_debug)) threads.append(threading.Thread(target=waitress_run)) threads.append(threading.Thread(target=thread_rescan)) + threads.append(threading.Thread(target=scan_tracks)) for worker in threads: worker.start() From 5d51e99c70c6d6fb4b3dd14612cca1305c0b2ac3 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 17:35:54 +0200 Subject: [PATCH 003/283] Music tracker --- communication_subroutine.py | 17 +++++++++++ conjurer_musician/conjurer_musician.py | 39 +++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index d9a648b..2ab60f9 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -13,6 +13,13 @@ IN_COMM_Q = Queue() awaiting_q = [] incoming_q = Queue() app = Flask(__name__) +prepped_tracks = { + "requests" : "", + "hits" : "", + "all": "", + "prio":"", + "jingles":"" +} class QueryControl: """ This class `QueryControl` is used to manage queries with information about the author, UUID, @@ -30,6 +37,16 @@ class QueryControl: ) self.replies = [] +@app.route("/prepped_tracks", methods=["POST"]) +def log_radio_tracks(): + app.logger.info(request) + record = json.loads(request.data) + app.logger.info(record) + app.logger.info("DATA RECEIVED") + incoming_q.put(record) + return jsonify("SUCCESS") + + @app.route("/conjurer", methods=["POST"]) def answer_external_command(): """ diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index ca8dcc3..1d16474 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -8,6 +8,7 @@ import json import logging import os import re +import requests import threading import time from datetime import datetime @@ -18,6 +19,8 @@ from sys import platform from flask import Flask, jsonify, redirect, request, send_from_directory, render_template from waitress import serve +MAIN_BOT_ADDRESS = "http://192.168.1.191:5000" +MUSIC_TRACKER = "/prepped_tracks" HOST_ADDRESS = "192.168.1.15" PORT_ADDRESS = 5000 if platform in ("linux", "linux2"): @@ -28,6 +31,7 @@ if platform in ("linux", "linux2"): NETRC_FILE = "/home/mtuszowski/.netrc" LOGSTORE = "/home/mtuszowski/conjurer/logs/" ENCODING = "utf-8" + RADIOLOG_PATH = '/home/pi/Conjurer/radio_log.log' else: LOGFILE = "/home/pi/Conjurer/discord_mus_service.log" @@ -36,6 +40,7 @@ if platform in ("linux", "linux2"): ENCODING = "utf-8" MUSIC_FOLDER = "/home/pi/RetroPie/mp3/" PRIORITY_FOLDER = "/home/pi/RetroPie/mp3/Magiczne i chuj/" + RADIOLOG_PATH = '/home/pi/Conjurer/radio_log.log' music_file_list = [] @@ -96,11 +101,10 @@ def scan_tracks(): #Set the filename and open the file logger = logging.getLogger("conjurer_musician") - filename = '/home/pi/Conjurer/radio_log.log' - file = open(filename,'r') + file = open(RADIOLOG_PATH,'r') #Find the size of the file and move to the end - st_results = os.stat(filename) + st_results = os.stat(RADIOLOG_PATH) st_size = st_results[6] file.seek(st_size) @@ -112,7 +116,34 @@ def scan_tracks(): file.seek(where) else: if re.match(".*Prepared.*",line): - logger.info(line) # already has newline + if re.match("*.jingles*.", line): + logger.info("jingles") + logger.info(line) # already has newline + result = ["jingles", line] + elif re.match("*.priority*.", line): + logger.info("priority") + logger.info(line) # already has newline + result = ["priority", line] + elif re.match("*.hit*.", line): + logger.info("priority") + logger.info(line) # already has newline + result = ["hit", line] + elif re.match("*.all_playlist*.", line): + logger.info("all") + logger.info(line) # already has newline + result = ["all", line] + elif re.match("*.request*.", line): + logger.info("requests") + logger.info(line) # already has newline + result = ["requests", line] + result = requests.post( + f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", + json=result, + timeout=360) + logger.info("SENT") + logger.info(result.status_code) + logger.info("SEND CONFIRMED") + time.sleep(1) time.sleep(0.1) From c3dd4ad8cf520a711a736f6521343753ccb61428 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 17:42:50 +0200 Subject: [PATCH 004/283] fx --- conjurer_musician/conjurer_musician.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 1d16474..cc834ec 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -116,6 +116,7 @@ def scan_tracks(): file.seek(where) else: if re.match(".*Prepared.*",line): + result = None if re.match("*.jingles*.", line): logger.info("jingles") logger.info(line) # already has newline @@ -136,12 +137,13 @@ def scan_tracks(): logger.info("requests") logger.info(line) # already has newline result = ["requests", line] - result = requests.post( + if result: + returned = requests.post( f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", json=result, timeout=360) logger.info("SENT") - logger.info(result.status_code) + logger.info(returned.status_code) logger.info("SEND CONFIRMED") time.sleep(1) time.sleep(0.1) From 9c3e51631c3698aceefcb97b7d55842a58e222d2 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 17:46:18 +0200 Subject: [PATCH 005/283] fx2 --- conjurer_musician/conjurer_musician.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index cc834ec..7cfefc5 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -137,15 +137,14 @@ def scan_tracks(): logger.info("requests") logger.info(line) # already has newline result = ["requests", line] - if result: - returned = requests.post( - f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", - json=result, - timeout=360) - logger.info("SENT") - logger.info(returned.status_code) - logger.info("SEND CONFIRMED") - time.sleep(1) + if result: + returned = requests.post( + f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", + json=result, + timeout=360) + logger.info("SENT") + logger.info(returned.status_code) + logger.info("SEND CONFIRMED") time.sleep(0.1) From 3a02846a172f308915cf0a3a61eaeae3f11929c1 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 17:50:12 +0200 Subject: [PATCH 006/283] fx --- conjurer_musician/conjurer_musician.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 7cfefc5..8965df1 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -117,23 +117,23 @@ def scan_tracks(): else: if re.match(".*Prepared.*",line): result = None - if re.match("*.jingles*.", line): + if re.match(".*jingles.*", line): logger.info("jingles") logger.info(line) # already has newline result = ["jingles", line] - elif re.match("*.priority*.", line): + elif re.match(".*priority.*", line): logger.info("priority") logger.info(line) # already has newline result = ["priority", line] - elif re.match("*.hit*.", line): + elif re.match(".*hit.*", line): logger.info("priority") logger.info(line) # already has newline result = ["hit", line] - elif re.match("*.all_playlist*.", line): + elif re.match(".*all_playlist.*", line): logger.info("all") logger.info(line) # already has newline result = ["all", line] - elif re.match("*.request*.", line): + elif re.match(".*request.*", line): logger.info("requests") logger.info(line) # already has newline result = ["requests", line] From 167fe59456c9a3def14f96db330edda4adb61fb1 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 17:58:40 +0200 Subject: [PATCH 007/283] logg --- communication_subroutine.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 2ab60f9..66a0068 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -20,6 +20,8 @@ prepped_tracks = { "prio":"", "jingles":"" } +logger = logging.getLogger("discord") + class QueryControl: """ This class `QueryControl` is used to manage queries with information about the author, UUID, @@ -29,7 +31,7 @@ class QueryControl: self.author = query_author self.uuid = query_uuid self.content = query_content - self.logger = uplogger + self.logger = logging.getLogger("discord") self.stop = False self.ctx = ctx self.logger.info( @@ -43,7 +45,6 @@ def log_radio_tracks(): record = json.loads(request.data) app.logger.info(record) app.logger.info("DATA RECEIVED") - incoming_q.put(record) return jsonify("SUCCESS") @@ -145,6 +146,7 @@ def comm_subroutine(logger): the provided code snippet, the logger is used to log messages at the "info" level """ #logger.setLevel(logging.DEBUG) + logging.getLogger("discord") logger.info("Started") threads = [] #threads.append(threading.Thread(target=flask_debug, args=(logger,))) From e54150fd5dcca8434004370b15f6a4028a4eaf04 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 18:07:50 +0200 Subject: [PATCH 008/283] fx --- communication_subroutine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 66a0068..0ae01db 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -146,7 +146,7 @@ def comm_subroutine(logger): the provided code snippet, the logger is used to log messages at the "info" level """ #logger.setLevel(logging.DEBUG) - logging.getLogger("discord") + logger = logging.getLogger("discord") logger.info("Started") threads = [] #threads.append(threading.Thread(target=flask_debug, args=(logger,))) From 82bb6cbbad5275386175a8ccc91bcd5a30b97b9d Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Thu, 1 Aug 2024 18:18:26 +0200 Subject: [PATCH 009/283] fx --- communication_subroutine.py | 52 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 0ae01db..ef4302b 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -20,14 +20,13 @@ prepped_tracks = { "prio":"", "jingles":"" } -logger = logging.getLogger("discord") class QueryControl: """ This class `QueryControl` is used to manage queries with information about the author, UUID, content, logger, context, and replies. """ - def __init__(self, query_author, query_uuid, query_content, uplogger, ctx) -> None: + def __init__(self, query_author, query_uuid, query_content, ctx) -> None: self.author = query_author self.uuid = query_uuid self.content = query_content @@ -41,10 +40,12 @@ class QueryControl: @app.route("/prepped_tracks", methods=["POST"]) def log_radio_tracks(): - app.logger.info(request) + logger = logging.getLogger("discord") + + logger.info(request) record = json.loads(request.data) - app.logger.info(record) - app.logger.info("DATA RECEIVED") + logger.info(record) + logger.info("DATA RECEIVED") return jsonify("SUCCESS") @@ -56,10 +57,11 @@ def answer_external_command(): :return: The function `answer_external_command()` is returning a JSON response with the message "SUCCESS". """ - app.logger.info(request) + logger = logging.getLogger("discord") + logger.info(request) record = json.loads(request.data) - app.logger.info(record) - app.logger.info("DATA RECEIVED") + logger.info(record) + logger.info("DATA RECEIVED") incoming_q.put(record) return jsonify("SUCCESS") @@ -74,25 +76,29 @@ def check_alive(): """ return jsonify("ALIVE") -def flask_debug(_logger): +def flask_debug(): """ The `flask_debug` function starts a Flask application in debug mode without using the reloader. Do not use for production for fucks sake. """ - _logger.info("Attempt debug") + logger = logging.getLogger("discord") + + logger.info("Attempt debug") # trunk-ignore(bandit/B201) app.run(debug=True, use_reloader=False, host=HOST_ADDRESS, port=PORT_ADDRESS) -def waitress_run(_logger): +def waitress_run(): """ The `waitress_run` function serves the `app` on host "0.0.0.0" and port 5000 using the Waitress WSGI server. """ - _logger.info("Attempt waitress") + logger = logging.getLogger("discord") + + logger.info("Attempt waitress") serve(app, host=HOST_ADDRESS, port=PORT_ADDRESS) -def scan_queue(_logger): +def scan_queue(): """ The function `scan_queue` reads data from a queue, logs it, and appends it to another queue. @@ -101,12 +107,13 @@ def scan_queue(_logger): and provide information for debugging purposes. In this code snippet, the `_logger` object is used to log the """ + logger = logging.getLogger("discord") while True: data = OUT_COMM_Q.get() - _logger.info(data) + logger.info(data) awaiting_q.append(data) -def scan_incoming(_logger): +def scan_incoming(): """ The `scan_incoming` function continuously checks for incoming data, processes it, and logs when data is found. @@ -116,10 +123,11 @@ def scan_incoming(_logger): debugging, monitoring, or tracking the flow of the program. In this case, the `_logger` is being used """ + logger = logging.getLogger("discord") while True: try: answer = incoming_q.get(block=False) - _logger.info("DATA FOUND") + logger.info("DATA FOUND") record_stored = False for record in awaiting_q: if record.uuid in answer.keys(): @@ -129,7 +137,7 @@ def scan_incoming(_logger): IN_COMM_Q.put(record) if not record_stored: for key in answer.keys(): - record = QueryControl("Orphaned", key, "Orphan", _logger, None) + record = QueryControl("Orphaned", key, "Orphan", None) record.stop = True record.entries = answer[record.uuid] IN_COMM_Q.put(record) @@ -147,12 +155,12 @@ def comm_subroutine(logger): """ #logger.setLevel(logging.DEBUG) logger = logging.getLogger("discord") - logger.info("Started") + logger.info("Started comms") threads = [] - #threads.append(threading.Thread(target=flask_debug, args=(logger,))) - threads.append(threading.Thread(target=waitress_run, args=(logger,))) - threads.append(threading.Thread(target=scan_queue,args=(logger,))) - threads.append(threading.Thread(target=scan_incoming,args=(logger,))) + #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)) for worker in threads: From 1ebd58b8aceae1334ccf652b5878bd725ff976eb Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 11:43:38 +0200 Subject: [PATCH 010/283] UPdate for music tracker --- bot.py | 22 +++++++++++++++++++++- communication_subroutine.py | 12 ++++++------ conjurer_musician/conjurer_musician.py | 2 +- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/bot.py b/bot.py index 6706ded..1379683 100644 --- a/bot.py +++ b/bot.py @@ -47,6 +47,7 @@ from communication_subroutine import ( OUT_COMM_Q, QueryControl, comm_subroutine, + PREPPED_TRACKS, ) from spotify_dl import spotify from spotify_dl import youtube as youtube_download @@ -2524,6 +2525,25 @@ async def krecimy_pornola(ctx): else: ctx.reply("Jak ładnie szefa poprosisz.") +@client.hybrid_command( + name="co_na_plejliscie_wariacie", + description="Wyswietl kawalki ktore sa na pocztku list radia", + guild=discord.Object(id=664789470779932693), +) +async def co_na_plejliscie_wariacie(ctx): + """ + Download a music number from youtube. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. It allows the command to interact + with the Discord API and + """ + await ctx.send("Dej mnie chwilkę") + async with ctx.typing: + logger.info("plejlista") + logger.info(PREPPED_TRACKS) + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS["priority"]}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS["hit"]}, na liście wszystkich kawałków {PREPPED_TRACKS["all"]} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS["requests"]}. Z kolei jingiel to: {PREPPED_TRACKS["jingles"]}") # *================================== Run if __name__ == "__main__": @@ -2531,7 +2551,7 @@ if __name__ == "__main__": threads = [] logger.info("Starting discord bot: Creating threads") threads.append(threading.Thread(target=client.run, args=(TOKEN,))) - threads.append(threading.Thread(target=comm_subroutine, args=(logger,))) + threads.append(threading.Thread(target=comm_subroutine)) logger.info("Starting discord bot: Starting threads") WRK_CNT = 0 for worker in threads: diff --git a/communication_subroutine.py b/communication_subroutine.py index ef4302b..553fca2 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -13,11 +13,11 @@ IN_COMM_Q = Queue() awaiting_q = [] incoming_q = Queue() app = Flask(__name__) -prepped_tracks = { +PREPPED_TRACKS = { "requests" : "", - "hits" : "", + "hit" : "", "all": "", - "prio":"", + "priority":"", "jingles":"" } @@ -45,6 +45,7 @@ def log_radio_tracks(): logger.info(request) record = json.loads(request.data) logger.info(record) + PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") return jsonify("SUCCESS") @@ -145,7 +146,7 @@ def scan_incoming(): time.sleep(1) -def comm_subroutine(logger): +def comm_subroutine(): """ The `comm_subroutine` function starts multiple threads to run different tasks concurrently. @@ -169,5 +170,4 @@ def comm_subroutine(logger): worker.join() if __name__ == "__main__": - logging_client = logging.getLogger(__name__) - comm_subroutine(logging_client) + comm_subroutine() diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 8965df1..aded752 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -126,7 +126,7 @@ def scan_tracks(): logger.info(line) # already has newline result = ["priority", line] elif re.match(".*hit.*", line): - logger.info("priority") + logger.info("hit") logger.info(line) # already has newline result = ["hit", line] elif re.match(".*all_playlist.*", line): From 759c06aa5001c34ae981844a46bb17bf3afdd944 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 11:47:13 +0200 Subject: [PATCH 011/283] Buffx --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 1379683..f6606c8 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing: logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS["priority"]}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS["hit"]}, na liście wszystkich kawałków {PREPPED_TRACKS["all"]} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS["requests"]}. Z kolei jingiel to: {PREPPED_TRACKS["jingles"]}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS['priority']}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, na liście wszystkich kawałków {PREPPED_TRACKS['all']} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") # *================================== Run if __name__ == "__main__": From ce91aec6ff006c03eaeaf2e8d5648970e7885e88 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 11:50:54 +0200 Subject: [PATCH 012/283] Fix --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index f6606c8..df47d90 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing: logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS['priority']}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, na liście wszystkich kawałków {PREPPED_TRACKS['all']} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") + ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS['priority']}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, na liście wszystkich kawałków {PREPPED_TRACKS['all']} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") # *================================== Run if __name__ == "__main__": From 0f7e5d4a25787c48c71e27d72a4870f3597fa63b Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 11:53:03 +0200 Subject: [PATCH 013/283] Fix --- bot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index df47d90..63b7abf 100644 --- a/bot.py +++ b/bot.py @@ -2521,9 +2521,9 @@ async def krecimy_pornola(ctx): sciezka, files = await get_file(ctx, "Pornol", item_yt) if files: logger.info("Pornol udany") - ctx.send(f"Jest w tajnym archiwum pod adresem{sciezka})") + await ctx.send(f"Jest w tajnym archiwum pod adresem{sciezka})") else: - ctx.reply("Jak ładnie szefa poprosisz.") + await ctx.reply("Jak ładnie szefa poprosisz.") @client.hybrid_command( name="co_na_plejliscie_wariacie", @@ -2540,10 +2540,10 @@ async def co_na_plejliscie_wariacie(ctx): with the Discord API and """ await ctx.send("Dej mnie chwilkę") - async with ctx.typing: + async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS['priority']}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, na liście wszystkich kawałków {PREPPED_TRACKS['all']} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS['priority']}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, na liście wszystkich kawałków {PREPPED_TRACKS['all']} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") # *================================== Run if __name__ == "__main__": From c72839620570a97766997c59ae2c059fc2620061 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 11:58:51 +0200 Subject: [PATCH 014/283] tst fx --- communication_subroutine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/communication_subroutine.py b/communication_subroutine.py index 553fca2..e353ef7 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -20,6 +20,7 @@ PREPPED_TRACKS = { "priority":"", "jingles":"" } +logger = logging.getLogger("discord") class QueryControl: """ From c39900e15feb83d01156a87201d06c0ed9d4f0c7 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 12:01:05 +0200 Subject: [PATCH 015/283] Fx frmt --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 63b7abf..0c8021c 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują - na liście priorytetowej {PREPPED_TRACKS['priority']}, na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, na liście wszystkich kawałków {PREPPED_TRACKS['all']} zaś najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- na liście priorytetowej {PREPPED_TRACKS['priority']}, \n-na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, \n-na liście wszystkich kawałków {PREPPED_TRACKS['all']} \n-najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. \n-Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") # *================================== Run if __name__ == "__main__": From f320b208505e3b38c25e3058f57dae1ff5e0cc52 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 12:02:21 +0200 Subject: [PATCH 016/283] fx frmt --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 0c8021c..29708dd 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- na liście priorytetowej {PREPPED_TRACKS['priority']}, \n-na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, \n-na liście wszystkich kawałków {PREPPED_TRACKS['all']} \n-najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. \n-Z kolei jingiel to: {PREPPED_TRACKS['jingles']}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- na liście priorytetowej {PREPPED_TRACKS['priority']}, \n- na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, \n- na liście wszystkich kawałków {PREPPED_TRACKS['all']} \n- najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. \nZ kolei jingiel to: {PREPPED_TRACKS['jingles']}") # *================================== Run if __name__ == "__main__": From 4d79c51e20ac14baaa119d6e4dee7f20845394e5 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 12:07:17 +0200 Subject: [PATCH 017/283] Fx frmt --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 29708dd..5b61a5f 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- na liście priorytetowej {PREPPED_TRACKS['priority']}, \n- na liście hitów obecnie czeka kawałek {PREPPED_TRACKS['hit']}, \n- na liście wszystkich kawałków {PREPPED_TRACKS['all']} \n- najświeższym zamówieniem od słuchaczy jest {PREPPED_TRACKS['requests']}. \nZ kolei jingiel to: {PREPPED_TRACKS['jingles']}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") # *================================== Run if __name__ == "__main__": From 81d5fcfb8faa283f77e868691490e25ad52e5535 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 12:10:55 +0200 Subject: [PATCH 018/283] Now playing --- bot.py | 2 +- communication_subroutine.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 5b61a5f..09bbc29 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS["now_playing"]).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") # *================================== Run if __name__ == "__main__": diff --git a/communication_subroutine.py b/communication_subroutine.py index e353ef7..eb212db 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -18,7 +18,8 @@ PREPPED_TRACKS = { "hit" : "", "all": "", "priority":"", - "jingles":"" + "jingles":"", + "now_playing":"" } logger = logging.getLogger("discord") @@ -46,6 +47,7 @@ def log_radio_tracks(): logger.info(request) record = json.loads(request.data) logger.info(record) + PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS[record[0]] PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") return jsonify("SUCCESS") From 803ad01a83f6294c6b2904fb16b2857a79671d8c Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 12:12:12 +0200 Subject: [PATCH 019/283] bgfx --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 09bbc29..0e3e1bf 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS["now_playing"]).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") # *================================== Run if __name__ == "__main__": From 71d8e039338a7b431fc91719eadcfd470ae64fe0 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 12:32:44 +0200 Subject: [PATCH 020/283] fx --- communication_subroutine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/communication_subroutine.py b/communication_subroutine.py index eb212db..e30d94d 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -47,6 +47,7 @@ def log_radio_tracks(): logger.info(request) record = json.loads(request.data) logger.info(record) + PREPPED_TRACKS["now_playing"] = "" PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS[record[0]] PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") From a827c5e429bfd812d5355e13bacad92f457c3b1b Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 19:22:25 +0200 Subject: [PATCH 021/283] Fix tracker --- communication_subroutine.py | 2 -- conjurer_musician/conjurer_musician.py | 28 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index e30d94d..77122f3 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -47,8 +47,6 @@ def log_radio_tracks(): logger.info(request) record = json.loads(request.data) logger.info(record) - PREPPED_TRACKS["now_playing"] = "" - PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS[record[0]] PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") return jsonify("SUCCESS") diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index aded752..18ff084 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -32,6 +32,7 @@ if platform in ("linux", "linux2"): LOGSTORE = "/home/mtuszowski/conjurer/logs/" ENCODING = "utf-8" RADIOLOG_PATH = '/home/pi/Conjurer/radio_log.log' + PERSISTENCE_PATH = '/home/pi/Conjurer/persistence.log' else: LOGFILE = "/home/pi/Conjurer/discord_mus_service.log" @@ -41,6 +42,7 @@ if platform in ("linux", "linux2"): MUSIC_FOLDER = "/home/pi/RetroPie/mp3/" PRIORITY_FOLDER = "/home/pi/RetroPie/mp3/Magiczne i chuj/" RADIOLOG_PATH = '/home/pi/Conjurer/radio_log.log' + PERSISTENCE_PATH = '/home/pi/Conjurer/persistence.log' music_file_list = [] @@ -102,13 +104,31 @@ def scan_tracks(): logger = logging.getLogger("conjurer_musician") file = open(RADIOLOG_PATH,'r') - #Find the size of the file and move to the end st_results = os.stat(RADIOLOG_PATH) st_size = st_results[6] file.seek(st_size) + st_results1 = os.stat(PERSISTENCE_PATH) + prev_st_size1 = st_results[6] while 1: + + st_results1 = os.stat(PERSISTENCE_PATH) + st_size1 = st_results1[6] + if prev_st_size1 != st_size1: + prev_st_size1 = st_size1 + file1 = open(PERSISTENCE_PATH, 'r') + lines = file1.getlines() + result = ["now_playing", lines[3]] + file1.close() + returned = requests.post( + f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", + json=result, + timeout=360) + logger.info("SENT") + logger.info(returned.status_code) + logger.info("SEND CONFIRMED") + where = file.tell() line = file.readline() if not line: @@ -142,9 +162,9 @@ def scan_tracks(): f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", json=result, timeout=360) - logger.info("SENT") - logger.info(returned.status_code) - logger.info("SEND CONFIRMED") + logger.info("SENT") + logger.info(returned.status_code) + logger.info("SEND CONFIRMED") time.sleep(0.1) From d226d99236d183b1b98c9c8cfad5f48ed7c4817f Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 19:57:56 +0200 Subject: [PATCH 022/283] bgfx --- conjurer_musician/conjurer_musician.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 18ff084..7a77a62 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -118,7 +118,7 @@ def scan_tracks(): if prev_st_size1 != st_size1: prev_st_size1 = st_size1 file1 = open(PERSISTENCE_PATH, 'r') - lines = file1.getlines() + lines = file1.readlines() result = ["now_playing", lines[3]] file1.close() returned = requests.post( From a54a6686c2665ec49f625f95117ec310052ec7c4 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 20:02:02 +0200 Subject: [PATCH 023/283] fx --- conjurer_musician/conjurer_musician.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 7a77a62..bbc2a69 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -116,6 +116,7 @@ def scan_tracks(): st_results1 = os.stat(PERSISTENCE_PATH) st_size1 = st_results1[6] if prev_st_size1 != st_size1: + time.sleep(0.3) prev_st_size1 = st_size1 file1 = open(PERSISTENCE_PATH, 'r') lines = file1.readlines() From ce806c873f3a6409fbfae71a203db06e106df42c Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 20:19:21 +0200 Subject: [PATCH 024/283] Fx --- conjurer_musician/conjurer_musician.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index bbc2a69..beafa64 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -116,8 +116,10 @@ def scan_tracks(): st_results1 = os.stat(PERSISTENCE_PATH) st_size1 = st_results1[6] if prev_st_size1 != st_size1: - time.sleep(0.3) - prev_st_size1 = st_size1 + while prev_st_size1 != st_size1: + prev_st_size1 = st_size1 + st_results1 = os.stat(PERSISTENCE_PATH) + st_size1 = st_results1[6] file1 = open(PERSISTENCE_PATH, 'r') lines = file1.readlines() result = ["now_playing", lines[3]] From adb0f9b468196d28c4c63a8a9dd947413b032310 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 2 Aug 2024 20:27:27 +0200 Subject: [PATCH 025/283] fx --- conjurer_musician/conjurer_musician.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index beafa64..f626452 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -120,9 +120,10 @@ def scan_tracks(): prev_st_size1 = st_size1 st_results1 = os.stat(PERSISTENCE_PATH) st_size1 = st_results1[6] + time.sleep(0.1) file1 = open(PERSISTENCE_PATH, 'r') lines = file1.readlines() - result = ["now_playing", lines[3]] + result = ["now_playing", lines[2]] file1.close() returned = requests.post( f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", From 4842ccdc22d48fc23a057dbf667d336aab2007ac Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Sat, 3 Aug 2024 15:32:47 +0200 Subject: [PATCH 026/283] Music trackin upgrade --- bot.py | 2 +- communication_subroutine.py | 7 +++++-- conjurer_musician/conjurer_musician.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index 0e3e1bf..fb18b11 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} \n- następny będzie{(PREPPED_TRACKS['next']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") # *================================== Run if __name__ == "__main__": diff --git a/communication_subroutine.py b/communication_subroutine.py index 77122f3..d4a6774 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -19,7 +19,8 @@ PREPPED_TRACKS = { "all": "", "priority":"", "jingles":"", - "now_playing":"" + "now_playing":"", + "next": "" } logger = logging.getLogger("discord") @@ -47,7 +48,9 @@ def log_radio_tracks(): logger.info(request) record = json.loads(request.data) logger.info(record) - PREPPED_TRACKS[record[0]] = record[1] + if "next" in record[0]: + PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS["next"] + PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") return jsonify("SUCCESS") diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index f626452..8b39283 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -123,7 +123,7 @@ def scan_tracks(): time.sleep(0.1) file1 = open(PERSISTENCE_PATH, 'r') lines = file1.readlines() - result = ["now_playing", lines[2]] + result = ["next", lines[2]] file1.close() returned = requests.post( f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", From 0345573cb818d471b62fb03888deee77d22e63d0 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Sat, 3 Aug 2024 16:38:42 +0200 Subject: [PATCH 027/283] fx --- communication_subroutine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index d4a6774..d7f1488 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -50,7 +50,7 @@ def log_radio_tracks(): logger.info(record) if "next" in record[0]: PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS["next"] - PREPPED_TRACKS[record[0]] = record[1] + PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") return jsonify("SUCCESS") From 314a77c1f9f81f5a5353714da4d2ec9d15f53263 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Mon, 5 Aug 2024 11:24:13 +0200 Subject: [PATCH 028/283] Fx --- bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index fb18b11..bc9474d 100644 --- a/bot.py +++ b/bot.py @@ -2389,7 +2389,7 @@ async def wyszukaj_linki_do_dokumentow(ctx): username = ctx.message.author.nick else: username = ctx.message.author.name - query_object = QueryControl(username, query_uuid, query, logger, ctx) + query_object = QueryControl(username, query_uuid, query, ctx) OUT_COMM_Q.put(query_object) await ctx.send( f"No dobra poszło. Zapamiętaj proszę {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce." @@ -2484,7 +2484,7 @@ async def wyszukaj_linki_do_dokumentow_deep(ctx): username = ctx.message.author.nick else: username = ctx.message.author.name - query_object = QueryControl(username, query_uuid, query, logger, ctx) + query_object = QueryControl(username, query_uuid, query, ctx) OUT_COMM_Q.put(query_object) await ctx.send( f"Wypij wypi {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce niestety kolejka obowiazuje zawsze." From c31390859f17de65ff9a083eed5c6540da9164b9 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Fri, 9 Aug 2024 16:43:52 +0200 Subject: [PATCH 029/283] Metadata --- bot.py | 2 +- communication_subroutine.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index bc9474d..afd87be 100644 --- a/bot.py +++ b/bot.py @@ -2543,7 +2543,7 @@ async def co_na_plejliscie_wariacie(ctx): async with ctx.typing(): logger.info("plejlista") logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} \n- następny będzie{(PREPPED_TRACKS['next']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} (wg metadanych: {PREPPED_TRACKS['meta']}) \n- następny będzie{(PREPPED_TRACKS['next']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") # *================================== Run if __name__ == "__main__": diff --git a/communication_subroutine.py b/communication_subroutine.py index d7f1488..716d483 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -2,14 +2,20 @@ import logging import threading import json import time +import re +from urllib import request as urequest + from queue import Queue, Empty from flask import Flask, jsonify, request from waitress import serve HOST_ADDRESS = "192.168.1.191" PORT_ADDRESS = 5000 +ICECAST_ADDRESS = "192.168.1.12:666" OUT_COMM_Q = Queue() IN_COMM_Q = Queue() +SRCHTITLE = re.compile(br'StreamTitle=\\*(?P[^;]*);').search + awaiting_q = [] incoming_q = Queue() app = Flask(__name__) @@ -20,7 +26,8 @@ PREPPED_TRACKS = { "priority":"", "jingles":"", "now_playing":"", - "next": "" + "next": "", + "meta" : "" } logger = logging.getLogger("discord") @@ -49,7 +56,9 @@ def log_radio_tracks(): record = json.loads(request.data) logger.info(record) if "next" in record[0]: + metadata = id3(ICECAST_ADDRESS) PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS["next"] + PREPPED_TRACKS["meta"] = metadata["name"] + " - " + metadata["title"] + "(" + metadata["genre"] + ")" PREPPED_TRACKS[record[0]] = record[1] logger.info("DATA RECEIVED") return jsonify("SUCCESS") @@ -150,6 +159,28 @@ def scan_incoming(): except Empty: time.sleep(1) +def get_stream_title(tag:bytes) -> str: + title = '' + if m := SRCHTITLE(tag): + #decode, strip, unescape and remove surrounding quotes (may not even be the same type of quote) + title = m.group('title').decode('utf-8').strip().replace('\\', '')[1:-1] + return title + +def id3(url:str) -> dict: + request = urequest.Request(url, headers={'Icy-MetaData': 1}) + + with urequest.urlopen(request) as resp: + metaint = int(resp.headers.get('icy-metaint', '-1')) + if metaint<0: + return False + resp.read(metaint) #this isn't seekable so, arbitrarily read to the point we want + tagdata = dict( + site_url = resp.headers.get('icy-url' ) , + name = resp.headers.get('icy-name' ).title(), + genre = resp.headers.get('icy-genre').title(), + title = get_stream_title(resp.read(255)) ) + return tagdata + def comm_subroutine(): """ From 96b93488c7fc0a4ceac15e3182bea9ed5939470c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 16:47:35 +0200 Subject: [PATCH 030/283] 1 --- communication_subroutine.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 716d483..35eea73 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -50,17 +50,17 @@ class QueryControl: @app.route("/prepped_tracks", methods=["POST"]) def log_radio_tracks(): - logger = logging.getLogger("discord") + app.logger = logging.getLogger("discord") - logger.info(request) + app.logger.info(request) record = json.loads(request.data) - logger.info(record) + app.logger.info(record) if "next" in record[0]: metadata = id3(ICECAST_ADDRESS) PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS["next"] PREPPED_TRACKS["meta"] = metadata["name"] + " - " + metadata["title"] + "(" + metadata["genre"] + ")" PREPPED_TRACKS[record[0]] = record[1] - logger.info("DATA RECEIVED") + app.logger.info("DATA RECEIVED") return jsonify("SUCCESS") From 8c6e80d832723be913864e722cb3de15fcad99da Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 16:53:17 +0200 Subject: [PATCH 031/283] Fix --- communication_subroutine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 35eea73..10f7f0e 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -11,7 +11,7 @@ from waitress import serve HOST_ADDRESS = "192.168.1.191" PORT_ADDRESS = 5000 -ICECAST_ADDRESS = "192.168.1.12:666" +ICECAST_ADDRESS = "https://192.168.1.12:666" OUT_COMM_Q = Queue() IN_COMM_Q = Queue() SRCHTITLE = re.compile(br'StreamTitle=\\*(?P<title>[^;]*);').search From fcefaf11ef9e26893c73ea5b49c53aedd5628364 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 16:59:33 +0200 Subject: [PATCH 032/283] Fixing time mismatch --- conjurer_musician/conjurer_musician.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 8b39283..e89c7da 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -568,10 +568,13 @@ if __name__ == "__main__": # threads.append(threading.Thread(target=flask_debug)) threads.append(threading.Thread(target=waitress_run)) threads.append(threading.Thread(target=thread_rescan)) - threads.append(threading.Thread(target=scan_tracks)) for worker in threads: worker.start() + time.sleep(60) + threads.append(threading.Thread(target=scan_tracks)) + threads[2].start() + for worker in threads: worker.join() From c90e90ea10373e7133e5d16f07491a1cfafb2ef8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 17:02:05 +0200 Subject: [PATCH 033/283] Port fix --- communication_subroutine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 10f7f0e..31d7037 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -11,7 +11,7 @@ from waitress import serve HOST_ADDRESS = "192.168.1.191" PORT_ADDRESS = 5000 -ICECAST_ADDRESS = "https://192.168.1.12:666" +ICECAST_ADDRESS = "https://192.168.1.15:666" OUT_COMM_Q = Queue() IN_COMM_Q = Queue() SRCHTITLE = re.compile(br'StreamTitle=\\*(?P<title>[^;]*);').search From 20f045e92a4b485c64969b9efb300c6c2c6011a7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 17:03:15 +0200 Subject: [PATCH 034/283] fx --- communication_subroutine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index 31d7037..c59b7ca 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -11,7 +11,7 @@ from waitress import serve HOST_ADDRESS = "192.168.1.191" PORT_ADDRESS = 5000 -ICECAST_ADDRESS = "https://192.168.1.15:666" +ICECAST_ADDRESS = "http://192.168.1.15:666" OUT_COMM_Q = Queue() IN_COMM_Q = Queue() SRCHTITLE = re.compile(br'StreamTitle=\\*(?P<title>[^;]*);').search From ecabfc8876f29401a39e21c7ecc5635f371d6fed Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 17:06:01 +0200 Subject: [PATCH 035/283] fx --- communication_subroutine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index c59b7ca..d1df765 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -11,7 +11,7 @@ from waitress import serve HOST_ADDRESS = "192.168.1.191" PORT_ADDRESS = 5000 -ICECAST_ADDRESS = "http://192.168.1.15:666" +ICECAST_ADDRESS = "http://192.168.1.15:8000" OUT_COMM_Q = Queue() IN_COMM_Q = Queue() SRCHTITLE = re.compile(br'StreamTitle=\\*(?P<title>[^;]*);').search From 215a36f91e8b810ecf11f99ff457fa058672f2e8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 9 Aug 2024 17:10:14 +0200 Subject: [PATCH 036/283] Fx --- communication_subroutine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/communication_subroutine.py b/communication_subroutine.py index d1df765..bc44fff 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -58,7 +58,10 @@ def log_radio_tracks(): if "next" in record[0]: metadata = id3(ICECAST_ADDRESS) PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS["next"] - PREPPED_TRACKS["meta"] = metadata["name"] + " - " + metadata["title"] + "(" + metadata["genre"] + ")" + if metadata: + PREPPED_TRACKS["meta"] = metadata["name"] + " - " + metadata["title"] + "(" + metadata["genre"] + ")" + else: + PREPPED_TRACKS["meta"] = "Nie znaju" PREPPED_TRACKS[record[0]] = record[1] app.logger.info("DATA RECEIVED") return jsonify("SUCCESS") From dbcb8cce270341224df7057ed162489a8bc99642 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 13 Aug 2024 12:32:02 +0200 Subject: [PATCH 037/283] Testing transcribe --- conjurer_librarian/voice_recognition.py | 27 +++++++++++++++++++++++++ requirements_bot.txt | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 conjurer_librarian/voice_recognition.py diff --git a/conjurer_librarian/voice_recognition.py b/conjurer_librarian/voice_recognition.py new file mode 100644 index 0000000..c0bd227 --- /dev/null +++ b/conjurer_librarian/voice_recognition.py @@ -0,0 +1,27 @@ +# Start by making sure the `assemblyai` package is installed. +# If not, you can install it by running the following command: +# pip install -U assemblyai +# +# Note: Some macOS users may need to use `pip3` instead of `pip`. + +import assemblyai as aai + +# Replace with your API key +aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" + +# URL of the file to transcribe +FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3" + +# You can also transcribe a local file by passing in a file path +# FILE_URL = './path/to/file.mp3' + +config = aai.TranscriptionConfig(speaker_labels=True) + +transcriber = aai.Transcriber() +transcript = transcriber.transcribe( + FILE_URL, + config=config +) + +for utterance in transcript.utterances: + print(f"Speaker {utterance.speaker}: {utterance.text}") diff --git a/requirements_bot.txt b/requirements_bot.txt index 7c8fed6..b2afdbe 100644 --- a/requirements_bot.txt +++ b/requirements_bot.txt @@ -13,4 +13,5 @@ tiktoken PyNaCl flask waitress -clickupython \ No newline at end of file +clickupython +assemblyai \ No newline at end of file From 1d520c4aebde9cd89728c42540076121757d94eb Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 15 Aug 2024 10:52:29 +0200 Subject: [PATCH 038/283] Additions before wsl purge --- conjurer_librarian/voice_recognition.py | 10 +++++++++- requirements_bot.txt | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/conjurer_librarian/voice_recognition.py b/conjurer_librarian/voice_recognition.py index c0bd227..44a7bd9 100644 --- a/conjurer_librarian/voice_recognition.py +++ b/conjurer_librarian/voice_recognition.py @@ -5,7 +5,7 @@ # Note: Some macOS users may need to use `pip3` instead of `pip`. import assemblyai as aai - +from discord.ext import commands # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" @@ -25,3 +25,11 @@ transcript = transcriber.transcribe( for utterance in transcript.utterances: print(f"Speaker {utterance.speaker}: {utterance.text}") + + + + +class Transcriber(commands.Cog): + def __init__(self,bot): + self.bot = bot + self._last_member = None \ No newline at end of file diff --git a/requirements_bot.txt b/requirements_bot.txt index b2afdbe..17d5cd6 100644 --- a/requirements_bot.txt +++ b/requirements_bot.txt @@ -14,4 +14,5 @@ PyNaCl flask waitress clickupython -assemblyai \ No newline at end of file +assemblyai +git+https://github.com/imayhaveborkedit/discord-ext-voice-recv \ No newline at end of file From 5ea792bcd2a5cafb1a7a70895187e612569dae0a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 15 Aug 2024 14:11:17 +0200 Subject: [PATCH 039/283] Transcribe cog --- bot.py | 2 + conjurer_librarian/voice_recognition.py | 35 ----------------- voice_recognition.py | 51 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 35 deletions(-) delete mode 100644 conjurer_librarian/voice_recognition.py create mode 100644 voice_recognition.py diff --git a/bot.py b/bot.py index afd87be..08ca2a5 100644 --- a/bot.py +++ b/bot.py @@ -49,6 +49,7 @@ from communication_subroutine import ( comm_subroutine, PREPPED_TRACKS, ) +import voice_recognition from spotify_dl import spotify from spotify_dl import youtube as youtube_download @@ -291,6 +292,7 @@ logger.info("Done") # *=========================================== Create Client logger.info("Creating discord bot") client = commands.Bot(intents=intents, command_prefix="$") +client.add_cog(voice_recognition.Transcriber(client)) logger.info("Done") logger.info("Creating flask app") logger.info("Done") diff --git a/conjurer_librarian/voice_recognition.py b/conjurer_librarian/voice_recognition.py deleted file mode 100644 index 44a7bd9..0000000 --- a/conjurer_librarian/voice_recognition.py +++ /dev/null @@ -1,35 +0,0 @@ -# Start by making sure the `assemblyai` package is installed. -# If not, you can install it by running the following command: -# pip install -U assemblyai -# -# Note: Some macOS users may need to use `pip3` instead of `pip`. - -import assemblyai as aai -from discord.ext import commands -# Replace with your API key -aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" - -# URL of the file to transcribe -FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3" - -# You can also transcribe a local file by passing in a file path -# FILE_URL = './path/to/file.mp3' - -config = aai.TranscriptionConfig(speaker_labels=True) - -transcriber = aai.Transcriber() -transcript = transcriber.transcribe( - FILE_URL, - config=config -) - -for utterance in transcript.utterances: - print(f"Speaker {utterance.speaker}: {utterance.text}") - - - - -class Transcriber(commands.Cog): - def __init__(self,bot): - self.bot = bot - self._last_member = None \ No newline at end of file diff --git a/voice_recognition.py b/voice_recognition.py new file mode 100644 index 0000000..e11b5ab --- /dev/null +++ b/voice_recognition.py @@ -0,0 +1,51 @@ +# Start by making sure the `assemblyai` package is installed. +# If not, you can install it by running the following command: +# pip install -U assemblyai +# +# Note: Some macOS users may need to use `pip3` instead of `pip`. + +import assemblyai as aai +import discord +from discord.ext import commands +# Replace with your API key +aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" + +# URL of the file to transcribe +FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3" + +# You can also transcribe a local file by passing in a file path +# FILE_URL = './path/to/file.mp3' + + + +class Transcriber(commands.Cog): + def __init__(self, bot): + self.bot = bot + self._last_member = None + + + @commands.Cog.listener() + async def on_member_join(self, member): + channel = member.guild.system_channel + if channel is not None: + await channel.send(f'Witaj u Nas {member.mention}.') + + @commands.command() + async def witaj(self, ctx, *, member: discord.Member = None): + """Says hello""" + member = member or ctx.author + if self._last_member is None or self._last_member.id != member.id: + await ctx.send(f'Mhm {member.name}~') + else: + await ctx.send(f'Powtarzas sie {member.name}') + self._last_member = member + async def transcribe(): + config = aai.TranscriptionConfig(speaker_labels=True) + + transcriber = aai.Transcriber() + transcript = transcriber.transcribe( + FILE_URL, + config=config + ) + for utterance in transcript.utterances: + print(f"Speaker {utterance.speaker}: {utterance.text}") From fd23518f7a348af247f915c7e5225e408c527548 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 15 Aug 2024 16:46:42 +0200 Subject: [PATCH 040/283] Tst --- bot.py | 2 +- voice_recognition.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 08ca2a5..f787f79 100644 --- a/bot.py +++ b/bot.py @@ -292,7 +292,7 @@ logger.info("Done") # *=========================================== Create Client logger.info("Creating discord bot") client = commands.Bot(intents=intents, command_prefix="$") -client.add_cog(voice_recognition.Transcriber(client)) +client.load_extension("voice_recognition") logger.info("Done") logger.info("Creating flask app") logger.info("Done") diff --git a/voice_recognition.py b/voice_recognition.py index e11b5ab..1e18a74 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -30,7 +30,7 @@ class Transcriber(commands.Cog): if channel is not None: await channel.send(f'Witaj u Nas {member.mention}.') - @commands.command() + @commands.hybrid_command() async def witaj(self, ctx, *, member: discord.Member = None): """Says hello""" member = member or ctx.author @@ -49,3 +49,6 @@ class Transcriber(commands.Cog): ) for utterance in transcript.utterances: print(f"Speaker {utterance.speaker}: {utterance.text}") + +def setup(bot): + bot.add_cog(Transcriber(bot)) \ No newline at end of file From 63fb7a3b8a553ee658077553f04c74f4e65f0569 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 15 Aug 2024 16:51:24 +0200 Subject: [PATCH 041/283] fx --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index f787f79..65a7894 100644 --- a/bot.py +++ b/bot.py @@ -292,7 +292,6 @@ logger.info("Done") # *=========================================== Create Client logger.info("Creating discord bot") client = commands.Bot(intents=intents, command_prefix="$") -client.load_extension("voice_recognition") logger.info("Done") logger.info("Creating flask app") logger.info("Done") @@ -303,6 +302,8 @@ logger.info("Done") async def on_ready(): """Metoda wywoływana przy połączeniu do serwera.""" logger.debug("%s has connected to Discord!", client.user) + client.load_extension("voice_recognition") + await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) await client.tree.sync() check_self.start() From ce9beb5ecfcca3c16333fdedec7fade299a5b896 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 15 Aug 2024 16:54:08 +0200 Subject: [PATCH 042/283] fx --- bot.py | 4 ++-- voice_recognition.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 65a7894..03f9ef2 100644 --- a/bot.py +++ b/bot.py @@ -49,7 +49,6 @@ from communication_subroutine import ( comm_subroutine, PREPPED_TRACKS, ) -import voice_recognition from spotify_dl import spotify from spotify_dl import youtube as youtube_download @@ -296,13 +295,14 @@ logger.info("Done") logger.info("Creating flask app") logger.info("Done") +client.load_extension("voice_recognition") # *=========================================== Define Events @client.event async def on_ready(): """Metoda wywoływana przy połączeniu do serwera.""" logger.debug("%s has connected to Discord!", client.user) - client.load_extension("voice_recognition") + #client.load_extension("voice_recognition") await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) await client.tree.sync() diff --git a/voice_recognition.py b/voice_recognition.py index 1e18a74..7821699 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -30,7 +30,7 @@ class Transcriber(commands.Cog): if channel is not None: await channel.send(f'Witaj u Nas {member.mention}.') - @commands.hybrid_command() + @commands.hybrid_command(name="witaj") async def witaj(self, ctx, *, member: discord.Member = None): """Says hello""" member = member or ctx.author From e6648926a64f22c72caf8f87d63b3aeaebbb5533 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 15 Aug 2024 17:09:18 +0200 Subject: [PATCH 043/283] Fx --- bot.py | 8 +++++--- voice_recognition.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index 03f9ef2..1e14598 100644 --- a/bot.py +++ b/bot.py @@ -295,17 +295,19 @@ logger.info("Done") logger.info("Creating flask app") logger.info("Done") -client.load_extension("voice_recognition") # *=========================================== Define Events @client.event async def on_ready(): """Metoda wywoływana przy połączeniu do serwera.""" logger.debug("%s has connected to Discord!", client.user) - #client.load_extension("voice_recognition") - + await client.load_extension("voice_recognition") + logger.info("Cogs") + logger.info(client.cogs) await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) await client.tree.sync() + for com in client.commands: + logger.info(com.qualified_name) check_self.start() check_data_q.start() diff --git a/voice_recognition.py b/voice_recognition.py index 7821699..b543873 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -50,5 +50,5 @@ class Transcriber(commands.Cog): for utterance in transcript.utterances: print(f"Speaker {utterance.speaker}: {utterance.text}") -def setup(bot): - bot.add_cog(Transcriber(bot)) \ No newline at end of file +async def setup(bot): + await bot.add_cog(Transcriber(bot)) \ No newline at end of file From 7857226e129ec3ec1e0fec8aefb4c028ab703bcc Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 23 Aug 2024 18:46:40 +0200 Subject: [PATCH 044/283] Fix --- conjurer_musician/radio_conjurer.liq | 20 +-- install_main_bot.sh | 5 + install_music_bot.sh | 4 + sr_example.py | 229 +++++++++++++++++++++++++++ voice_recognition.py | 26 ++- 5 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 sr_example.py diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index ec3a6c5..77c4e42 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -64,21 +64,23 @@ b = bass_boost(frequency=f, gain=g, s) s = add([s, b]) # Set up interactive control for main volume -a = interactive.float("main_volume", min=0., max=20., 1.) +a = interactive.float("main_volume", min=0., max=20., 0.4) s = compress.multiband.interactive(bands=7, s) +mic_gain = interactive.float("mic_volume", min=0., max=20., 6.) +# Apply audio processing effects +tmic = buffer(input.pulseaudio()) # Microphone +mic = amplify(mic_gain, tmic) +mic = gate(threshold=-80., range=-120., mic) +mic = compress(threshold=0., ratio=2.,mic) +mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) + +s = add([s, mic]) + # Apply audio processing effects s = nrj(normalize(s)) s = amplify(a, s) -mic = buffer(input.pulseaudio()) # Microphone -mic = gate(threshold=-30., range=-80., mic) -mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) - -s = add([s, mic]) -#w = interactive.float("wetness", min=0., max=1., 1.) -#s = dry_wet(w,s,s2) - # Skip blank sections in the stream s = blank.skip(max_blank=10., s) diff --git a/install_main_bot.sh b/install_main_bot.sh index ddac526..6bf447e 100755 --- a/install_main_bot.sh +++ b/install_main_bot.sh @@ -9,6 +9,11 @@ source ./env/bin/activate ./env/bin/python3 -m pip install -r requirements_bot.txt sed -i -e 's/os.rename/shutil.copy/g' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py sed -i '1i\import shutil' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py +sed -i '1i\import logging' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py + +sed -i '21i\ logger = logging.getLogger("discord")' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py +sed -i '22i\ logger.info("Playlist")' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py + deactivate sudo cp ./conjurer.service /etc/systemd/system/ sudo systemctl daemon-reload diff --git a/install_music_bot.sh b/install_music_bot.sh index 210c6c5..3d1364a 100755 --- a/install_music_bot.sh +++ b/install_music_bot.sh @@ -32,3 +32,7 @@ sudo cp /home/pi/conjurer/conjurer_musician/radio_service.service /etc/systemd/s sudo systemctl daemon-reload sudo systemctl start radio_service.service sudo systemctl enable radio_service.service + + +sed -i -e 's/os.rename/shutil.copy/g' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py +sed -i '1i\import shutil' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py diff --git a/sr_example.py b/sr_example.py new file mode 100644 index 0000000..1362457 --- /dev/null +++ b/sr_example.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from discord.ext.voice_recv.sinks import AudioSink + +log = logging.getLogger(__name__) + +__all__ = [ + 'SpeechRecognitionSink', +] + +try: + import discord.ext.speech_recognition as sr # type: ignore +except ImportError: + + def SpeechRecognitionSink(**kwargs) -> AudioSink: + """A stub for when the SpeechRecognition module isn't found.""" + raise RuntimeError('The SpeechRecognition module is required to use this sink.') + +else: + import time + import array + import asyncio + import audioop + + from collections import defaultdict + + from discord.ext.voice_recv.sinks import SilencePacket + + from typing import TYPE_CHECKING, TypedDict + + if TYPE_CHECKING: + from concurrent.futures import Future as CFuture + from typing import Literal, Callable, Optional, Any, Final, Protocol, Awaitable, TypeVar + + from discord import Member + + from ..opus import VoiceData + from ..types import MemberOrUser as User + + T = TypeVar('T') + + SRRecognizerMethod = Literal[ + 'sphinx', + 'google', + 'google_cloud', + 'wit', + 'azure', + 'bing', + 'lex', + 'houndify', + 'amazon', + 'assemblyai', + 'ibm', + 'tensorflow', + 'whisper', + 'vosk', + ] + + class SRStopper(Protocol): + def __call__(self, wait: bool = True, /) -> None: + ... + + SRProcessDataCB = Callable[[sr.Recognizer, sr.AudioData, User], Optional[str]] + SRTextCB = Callable[[User, str], Any] + + class _StreamData(TypedDict): + stopper: Optional[SRStopper] + recognizer: sr.Recognizer + buffer: array.array[int] + + class SpeechRecognitionSink(AudioSink): # type: ignore + def __init__( + self, + *, + process_cb: Optional[SRProcessDataCB] = None, + text_cb: Optional[SRTextCB] = None, + default_recognizer: SRRecognizerMethod = 'google', + phrase_time_limit: int = 10, + ignore_silence_packets: bool = True, + ): + super().__init__(None) + self.process_cb: Optional[SRProcessDataCB] = process_cb + self.text_cb: Optional[SRTextCB] = text_cb + self.phrase_time_limmit: int = phrase_time_limit + self.ignore_silence_packets: bool = ignore_silence_packets + + self.default_recognizer: SRRecognizerMethod = default_recognizer + self._stream_data: defaultdict[int, _StreamData] = defaultdict( + lambda: _StreamData(stopper=None, recognizer=sr.Recognizer(), buffer=array.array('B')) + ) + + def _await(self, coro: Awaitable[T]) -> CFuture[T]: + assert self.client is not None + return asyncio.run_coroutine_threadsafe(coro, self.client.loop) + + def wants_opus(self) -> bool: + return False + + def write(self, user: Optional[User], data: VoiceData) -> None: + if self.ignore_silence_packets and isinstance(data.packet, SilencePacket): + return + + if user is None: + return + + sdata = self._stream_data[user.id] + sdata['buffer'].extend(data.pcm) + + if not sdata['stopper']: + sdata['stopper'] = sdata['recognizer'].listen_in_background( + DiscordSRAudioSource(sdata['buffer']), self.background_listener(user), self.phrase_time_limmit + ) + + def background_listener(self, user: User): + process_cb = self.process_cb or self.get_default_process_callback() + text_cb = self.text_cb or self.get_default_text_callback() + + def callback(_recognizer: sr.Recognizer, _audio: sr.AudioData): + output = process_cb(_recognizer, _audio, user) + if output is not None: + text_cb(user, output) + + return callback + + def get_default_process_callback(self) -> SRProcessDataCB: + def cb(recognizer: sr.Recognizer, audio: sr.AudioData, user: Optional[User]) -> Optional[str]: + log.debug("Got %s, %s, %s", audio, audio.sample_rate, audio.sample_width) + text: Optional[str] = None + try: + # they changed recognize_google to be optionally assigned at runtime... + func = getattr(recognizer, 'recognize_' + self.default_recognizer, recognizer.recognize_google) # type: ignore + text = func(audio) # type: ignore + except sr.UnknownValueError: + log.debug("Bad speech chunk") + # self._debug_audio_chunk(audio) + + return text + + return cb + + def get_default_text_callback(self) -> SRTextCB: + def cb(user: Optional[User], text: Optional[str]) -> Any: + log.info("%s said: %s", user.display_name if user else 'Someone', text) + + return cb + + @AudioSink.listener() + def on_voice_member_disconnect(self, member: Member, ssrc: Optional[int]) -> None: + self._drop(member.id) + + def cleanup(self) -> None: + for user_id in tuple(self._stream_data.keys()): + self._drop(user_id) + + def _drop(self, user_id: int) -> None: + data = self._stream_data.pop(user_id) + + stopper = data.get('stopper') + if stopper: + stopper() + + buffer = data.get('buffer') + if buffer: + # arrays don't have a clear function + del buffer[:] + + def _debug_audio_chunk(self, audio: sr.AudioData, filename: str = 'sound.wav') -> None: + import io, wave, discord + + with io.BytesIO() as b: + with wave.open(b, 'wb') as writer: + writer.setframerate(48000) + writer.setsampwidth(2) + writer.setnchannels(2) + writer.writeframes(audio.get_wav_data()) + + b.seek(0) + f = discord.File(b, filename) + self._await(self.voice_client.channel.send(file=f)) # type: ignore + + class DiscordSRAudioSource(sr.AudioSource): + little_endian: Final[bool] = True + SAMPLE_RATE: Final[int] = 48_000 + SAMPLE_WIDTH: Final[int] = 2 + CHANNELS: Final[int] = 2 + CHUNK: Final[int] = 960 + + def __init__(self, buffer: array.array[int]): + self.buffer = buffer + self._entered: bool = False + + @property + def stream(self): + return self + + def __enter__(self): + if self._entered: + log.warning('Already entered sr audio source') + self._entered = True + return self + + def __exit__(self, *exc) -> None: + self._entered = False + if any(exc): + log.exception('Error closing sr audio source') + + def read(self, size: int) -> bytes: + # TODO: make this timeout configurable + for _ in range(10): + if len(self.buffer) < size * self.CHANNELS: + time.sleep(0.1) + else: + break + else: + if len(self.buffer) == 0: + return b'' + + chunksize = size * self.CHANNELS + audiochunk = self.buffer[:chunksize].tobytes() + del self.buffer[: min(chunksize, len(audiochunk))] + audiochunk = audioop.tomono(audiochunk, 2, 1, 1) + return audiochunk + + def close(self) -> None: + self.buffer.clear() \ No newline at end of file diff --git a/voice_recognition.py b/voice_recognition.py index b543873..b208080 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -6,9 +6,10 @@ import assemblyai as aai import discord -from discord.ext import commands +from discord.ext import commands, voice_recv # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" +discord.opus._load_default() # URL of the file to transcribe FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3" @@ -49,6 +50,29 @@ class Transcriber(commands.Cog): ) for utterance in transcript.utterances: print(f"Speaker {utterance.speaker}: {utterance.text}") + @commands.hybrid_command(name="test") + async def test(self, ctx): + def callback(user, data: voice_recv.VoiceData): + print(f"Got packet from {user}") + + ## voice power level, how loud the user is speaking + # ext_data = packet.extension_data.get(voice_recv.ExtensionID.audio_power) + # value = int.from_bytes(ext_data, 'big') + # power = 127-(value & 127) + # print('#' * int(power * (79/128))) + ## instead of 79 you can use shutil.get_terminal_size().columns-1 + + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + vc.listen(voice_recv.BasicSink(callback)) + + @commands.command() + async def stop(self, ctx): + await ctx.voice_client.disconnect() + + @commands.command() + async def die(self, ctx): + ctx.voice_client.stop() + await ctx.bot.close() async def setup(bot): await bot.add_cog(Transcriber(bot)) \ No newline at end of file From c448201823a0bb2cfd6e709913ccee552173ac05 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 28 Aug 2024 14:44:54 +0200 Subject: [PATCH 045/283] Testing for transcribe --- install_main_bot.sh | 1 + requirements_bot.txt | 2 +- stream_test.py | 62 ++++++++++++++++++++++++++++++++++++++++++++ voice_recognition.py | 6 ++--- 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 stream_test.py diff --git a/install_main_bot.sh b/install_main_bot.sh index 6bf447e..4eb0f03 100755 --- a/install_main_bot.sh +++ b/install_main_bot.sh @@ -1,4 +1,5 @@ #!/bin/bash +sudo apt-get install python3-dev cd /home/pi mdkir Conjurer cd Conjurer diff --git a/requirements_bot.txt b/requirements_bot.txt index 17d5cd6..9c7deaa 100644 --- a/requirements_bot.txt +++ b/requirements_bot.txt @@ -14,5 +14,5 @@ PyNaCl flask waitress clickupython -assemblyai +assemblyai[extras] git+https://github.com/imayhaveborkedit/discord-ext-voice-recv \ No newline at end of file diff --git a/stream_test.py b/stream_test.py new file mode 100644 index 0000000..82a410e --- /dev/null +++ b/stream_test.py @@ -0,0 +1,62 @@ +# 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 pyaudio +import assemblyai as aai + +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/voice_recognition.py b/voice_recognition.py index b208080..30a0628 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -50,7 +50,7 @@ class Transcriber(commands.Cog): ) for utterance in transcript.utterances: print(f"Speaker {utterance.speaker}: {utterance.text}") - @commands.hybrid_command(name="test") + @commands.hybrid_command(name="connect_for_transcribe") async def test(self, ctx): def callback(user, data: voice_recv.VoiceData): print(f"Got packet from {user}") @@ -65,11 +65,11 @@ class Transcriber(commands.Cog): vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) vc.listen(voice_recv.BasicSink(callback)) - @commands.command() + @commands.command(name="stop_transcribe") async def stop(self, ctx): await ctx.voice_client.disconnect() - @commands.command() + @commands.command(name="kill_transcribe_client") async def die(self, ctx): ctx.voice_client.stop() await ctx.bot.close() From 169552e8d5cd6cf6725a143f93ba9dd223eb648f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 28 Aug 2024 14:57:50 +0200 Subject: [PATCH 046/283] work --- voice_recognition.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 30a0628..d801533 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -7,6 +7,7 @@ import assemblyai as aai import discord from discord.ext import commands, voice_recv +import logging # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() @@ -17,7 +18,7 @@ FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/2023 # You can also transcribe a local file by passing in a file path # FILE_URL = './path/to/file.mp3' - +logger = logging.getLogger("discord") class Transcriber(commands.Cog): def __init__(self, bot): @@ -61,8 +62,9 @@ class Transcriber(commands.Cog): # power = 127-(value & 127) # print('#' * int(power * (79/128))) ## instead of 79 you can use shutil.get_terminal_size().columns-1 - + logger.info("Attempt transcribe") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + logger.info("Connected") vc.listen(voice_recv.BasicSink(callback)) @commands.command(name="stop_transcribe") From f9c854e2fc6515641c3ce8e45fd960af9e48148c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:03:34 +0200 Subject: [PATCH 047/283] Test --- sr_example.py | 2 +- voice_example.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ voice_recognition.py | 47 +++++++++++++++++++++++--------------------- 3 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 voice_example.py diff --git a/sr_example.py b/sr_example.py index 1362457..3c176cf 100644 --- a/sr_example.py +++ b/sr_example.py @@ -78,7 +78,7 @@ else: *, process_cb: Optional[SRProcessDataCB] = None, text_cb: Optional[SRTextCB] = None, - default_recognizer: SRRecognizerMethod = 'google', + default_recognizer: SRRecognizerMethod = 'assemblyai', phrase_time_limit: int = 10, ignore_silence_packets: bool = True, ): diff --git a/voice_example.py b/voice_example.py new file mode 100644 index 0000000..36cec22 --- /dev/null +++ b/voice_example.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import discord +from discord.ext import commands, voice_recv + +discord.opus._load_default() + +bot = commands.Bot(command_prefix=commands.when_mentioned, intents=discord.Intents.all()) + +class Testing(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def test(self, ctx): + def callback(user, data: voice_recv.VoiceData): + print(f"Got packet from {user}") + + ## voice power level, how loud the user is speaking + # ext_data = packet.extension_data.get(voice_recv.ExtensionID.audio_power) + # value = int.from_bytes(ext_data, 'big') + # power = 127-(value & 127) + # print('#' * int(power * (79/128))) + ## instead of 79 you can use shutil.get_terminal_size().columns-1 + + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + vc.listen(voice_recv.BasicSink(callback)) + + @commands.command() + async def stop(self, ctx): + await ctx.voice_client.disconnect() + + @commands.command() + async def die(self, ctx): + ctx.voice_client.stop() + await ctx.bot.close() + +@bot.event +async def on_ready(): + print('Logged in as {0.id}/{0}'.format(bot.user)) + print('------') + await bot.add_cog(Testing(bot)) + +REMOTE_HOST_NAME = "discord" + + +bot.run("NzUxNDQxODgzNDA2MDA4NDEw.Go--wb.Nr28Oo4eYAP1p2XFVHF5uIQfcPD7s_jO2NfMCQ") diff --git a/voice_recognition.py b/voice_recognition.py index d801533..5764dc6 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -20,27 +20,35 @@ FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/2023 logger = logging.getLogger("discord") + +class SRAudioSinkHammerVersion(AudioSink): + def __init__( + self, + *, + process_cb: Optional[SRProcessDataCB] = None, + text_cb: Optional[SRTextCB] = None, + default_recognizer: SRRecognizerMethod = 'google', + phrase_time_limit: int = 10, + ignore_silence_packets: bool = True, +): + super().__init__(None) + self.process_cb: Optional[SRProcessDataCB] = process_cb + self.text_cb: Optional[SRTextCB] = text_cb + self.phrase_time_limmit: int = phrase_time_limit + self.ignore_silence_packets: bool = ignore_silence_packets + + self.default_recognizer: SRRecognizerMethod = default_recognizer + self._stream_data: defaultdict[int, _StreamData] = defaultdict( + lambda: _StreamData(stopper=None, recognizer=sr.Recognizer(), buffer=array.array('B')) + ) + + + class Transcriber(commands.Cog): def __init__(self, bot): self.bot = bot self._last_member = None - - @commands.Cog.listener() - async def on_member_join(self, member): - channel = member.guild.system_channel - if channel is not None: - await channel.send(f'Witaj u Nas {member.mention}.') - - @commands.hybrid_command(name="witaj") - async def witaj(self, ctx, *, member: discord.Member = None): - """Says hello""" - member = member or ctx.author - if self._last_member is None or self._last_member.id != member.id: - await ctx.send(f'Mhm {member.name}~') - else: - await ctx.send(f'Powtarzas sie {member.name}') - self._last_member = member async def transcribe(): config = aai.TranscriptionConfig(speaker_labels=True) @@ -51,7 +59,7 @@ class Transcriber(commands.Cog): ) for utterance in transcript.utterances: print(f"Speaker {utterance.speaker}: {utterance.text}") - @commands.hybrid_command(name="connect_for_transcribe") + @commands.hybrid_command(name="transcribe") async def test(self, ctx): def callback(user, data: voice_recv.VoiceData): print(f"Got packet from {user}") @@ -71,10 +79,5 @@ class Transcriber(commands.Cog): async def stop(self, ctx): await ctx.voice_client.disconnect() - @commands.command(name="kill_transcribe_client") - async def die(self, ctx): - ctx.voice_client.stop() - await ctx.bot.close() - async def setup(bot): await bot.add_cog(Transcriber(bot)) \ No newline at end of file From 7fae6b601eb75092b2d3ebad8e8cecb3cd8e8f5a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:06:00 +0200 Subject: [PATCH 048/283] Att2 --- voice_recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 5764dc6..05015ff 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -20,7 +20,7 @@ FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/2023 logger = logging.getLogger("discord") - +''' class SRAudioSinkHammerVersion(AudioSink): def __init__( self, @@ -30,7 +30,7 @@ class SRAudioSinkHammerVersion(AudioSink): default_recognizer: SRRecognizerMethod = 'google', phrase_time_limit: int = 10, ignore_silence_packets: bool = True, -): + ): super().__init__(None) self.process_cb: Optional[SRProcessDataCB] = process_cb self.text_cb: Optional[SRTextCB] = text_cb @@ -41,7 +41,7 @@ class SRAudioSinkHammerVersion(AudioSink): self._stream_data: defaultdict[int, _StreamData] = defaultdict( lambda: _StreamData(stopper=None, recognizer=sr.Recognizer(), buffer=array.array('B')) ) - +''' class Transcriber(commands.Cog): From b6cb5e03b7bd5557da890b2e689b43b87e3b7098 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:07:42 +0200 Subject: [PATCH 049/283] Att 3 --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 05015ff..aec6c1f 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -62,7 +62,7 @@ class Transcriber(commands.Cog): @commands.hybrid_command(name="transcribe") async def test(self, ctx): def callback(user, data: voice_recv.VoiceData): - print(f"Got packet from {user}") + logger.info(f"Got packet from {user}") ## voice power level, how loud the user is speaking # ext_data = packet.extension_data.get(voice_recv.ExtensionID.audio_power) From 6dd0e1865d3269b764bfce3ce4d0320a61c05c76 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:15:03 +0200 Subject: [PATCH 050/283] saving --- voice_recognition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index aec6c1f..46e7100 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -73,7 +73,9 @@ class Transcriber(commands.Cog): logger.info("Attempt transcribe") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") - vc.listen(voice_recv.BasicSink(callback)) + #basic = voice_recv.BasicSink(callback) + sink = voice_recv.WaveSink(destination="Test.wav") + vc.listen(sink) @commands.command(name="stop_transcribe") async def stop(self, ctx): From b59bd4c41d5db5c71c0ec978bc349172b81d02a7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:20:37 +0200 Subject: [PATCH 051/283] ffmpeg --- voice_recognition.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 46e7100..c63245b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -73,9 +73,11 @@ class Transcriber(commands.Cog): logger.info("Attempt transcribe") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") - #basic = voice_recv.BasicSink(callback) - sink = voice_recv.WaveSink(destination="Test.wav") - vc.listen(sink) + bsink = voice_recv.BasicSink(callback) + sink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.wav") + fsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.mp3") + + vc.listen(fsink) @commands.command(name="stop_transcribe") async def stop(self, ctx): From 7e4e8f167fd21ae30687a9e034f819f889b16ea8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:25:18 +0200 Subject: [PATCH 052/283] Fsink wsink --- voice_recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index c63245b..79f68dc 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -73,12 +73,12 @@ class Transcriber(commands.Cog): logger.info("Attempt transcribe") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") - bsink = voice_recv.BasicSink(callback) - sink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.wav") - fsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.mp3") + wsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.mp3") + fsink = voice_recv.FFmpegSink(destination="/home/pi/Conjurer/Test.mp3") vc.listen(fsink) + @commands.command(name="stop_transcribe") async def stop(self, ctx): await ctx.voice_client.disconnect() From 944bb7e5fa0f9413f71a6f1a24c81a826518bb39 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 16:29:11 +0200 Subject: [PATCH 053/283] fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 79f68dc..fcae128 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -74,7 +74,7 @@ class Transcriber(commands.Cog): vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") wsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.mp3") - fsink = voice_recv.FFmpegSink(destination="/home/pi/Conjurer/Test.mp3") + fsink = voice_recv.FFmpegSink(filename="/home/pi/Conjurer/Test.mp3") vc.listen(fsink) From 781b83c9b19c75268fbecdceafc99bf641e0f703 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 17:19:00 +0200 Subject: [PATCH 054/283] msink --- voice_recognition.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index fcae128..3907912 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -46,8 +46,15 @@ class SRAudioSinkHammerVersion(AudioSink): class Transcriber(commands.Cog): def __init__(self, bot): + def callback(user, data: voice_recv.VoiceData): + logger.info(f"Got packet from {user}") + self.bot = bot self._last_member = None + bsink = voice_recv.BasicSink(callback) + wsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/wav.wav") + fsink = voice_recv.FFmpegSink(filename = ".home/pi/Conjurer/mp3.mp3") + self.sink_list = [bsink, wsink, fsink] async def transcribe(): config = aai.TranscriptionConfig(speaker_labels=True) @@ -61,22 +68,11 @@ class Transcriber(commands.Cog): print(f"Speaker {utterance.speaker}: {utterance.text}") @commands.hybrid_command(name="transcribe") async def test(self, ctx): - def callback(user, data: voice_recv.VoiceData): - logger.info(f"Got packet from {user}") - - ## voice power level, how loud the user is speaking - # ext_data = packet.extension_data.get(voice_recv.ExtensionID.audio_power) - # value = int.from_bytes(ext_data, 'big') - # power = 127-(value & 127) - # print('#' * int(power * (79/128))) - ## instead of 79 you can use shutil.get_terminal_size().columns-1 logger.info("Attempt transcribe") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") - wsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/Test.mp3") - fsink = voice_recv.FFmpegSink(filename="/home/pi/Conjurer/Test.mp3") - - vc.listen(fsink) + msink = voice_recv.MultiAudioSink(destinations= self.sink_list) + vc.listen(msink) @commands.command(name="stop_transcribe") From 89f4b0e51e03a7f7c813f470b57bf0892f8e9639 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 21:48:30 +0200 Subject: [PATCH 055/283] tst --- bot.py | 2 +- requirements_bot.txt | 1 + voice_recognition.py | 92 ++++++++++++++++++++++++++++---------------- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/bot.py b/bot.py index 1e14598..711fac0 100644 --- a/bot.py +++ b/bot.py @@ -122,7 +122,6 @@ class MusicFileList(object): # *=========================================== Predefines -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") MASTER_TIMEOUT = datetime.now() INITIAL_TIME_WAIT = 500 MUZYKA: Music_Config = {"ctx": None, "queue": [], "requester": []} @@ -243,6 +242,7 @@ handler = handlers.RotatingFileHandler( maxBytes=6 * 1024 * 1024, backupCount=6, ) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) random.seed() diff --git a/requirements_bot.txt b/requirements_bot.txt index 9c7deaa..dd2fcd4 100644 --- a/requirements_bot.txt +++ b/requirements_bot.txt @@ -15,4 +15,5 @@ flask waitress clickupython assemblyai[extras] +SpeechRecognition git+https://github.com/imayhaveborkedit/discord-ext-voice-recv \ No newline at end of file diff --git a/voice_recognition.py b/voice_recognition.py index 3907912..b203d08 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -1,13 +1,10 @@ -# Start by making sure the `assemblyai` package is installed. -# If not, you can install it by running the following command: -# pip install -U assemblyai -# -# Note: Some macOS users may need to use `pip3` instead of `pip`. - import assemblyai as aai import discord from discord.ext import commands, voice_recv import logging +from discord.opus import Decoder as OpusDecoder +import wave + # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() @@ -20,28 +17,55 @@ FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/2023 logger = logging.getLogger("discord") -''' -class SRAudioSinkHammerVersion(AudioSink): - def __init__( - self, - *, - process_cb: Optional[SRProcessDataCB] = None, - text_cb: Optional[SRTextCB] = None, - default_recognizer: SRRecognizerMethod = 'google', - phrase_time_limit: int = 10, - ignore_silence_packets: bool = True, - ): - super().__init__(None) - self.process_cb: Optional[SRProcessDataCB] = process_cb - self.text_cb: Optional[SRTextCB] = text_cb - self.phrase_time_limmit: int = phrase_time_limit - self.ignore_silence_packets: bool = ignore_silence_packets - self.default_recognizer: SRRecognizerMethod = default_recognizer - self._stream_data: defaultdict[int, _StreamData] = defaultdict( - lambda: _StreamData(stopper=None, recognizer=sr.Recognizer(), buffer=array.array('B')) - ) -''' +class SRBuffer(voice_recv.AudioSink): + """Endpoint AudioSink that generates a wav file. + Best used in conjunction with a silence generating sink. (TBD) + """ + + CHANNELS = OpusDecoder.CHANNELS + SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS + SAMPLING_RATE = OpusDecoder.SAMPLING_RATE + + ##on member join - 5 plików buforowych na człowieka otwarty jest tylko aktualny i następny + #on member disconnect - dropujemy go + + + # event: BasicSinkWriteCB, + # rtcp_event: Optional[BasicSinkWriteRTCPCB] = None, + # self.cb = event + # self.cb_rtcp = rtcp_event + # def write(self, user: Optional[User], data: VoiceData) -> None: + # self.cb(user, data) + # @AudioSink.listener() + # def on_rtcp_packet(self, packet: RTCPPacket, guild: discord.Guild) -> None: + # self.cb_rtcp(packet) if self.cb_rtcp else None + + + + def __init__(self, destination: wave._File): + super().__init__() + + self._file: wave.Wave_write = wave.open(destination, 'wb') + self._file.setnchannels(self.CHANNELS) + self._file.setsampwidth(self.SAMPLE_WIDTH) + self._file.setframerate(self.SAMPLING_RATE) + self.user_list = [] + + def wants_opus(self) -> bool: + return False + + def write(self, user, data) -> None: + logger.info("writing to file %s", self._file) + logger.info("user: %s", user) + self._file.writeframes(data.pcm) + + def cleanup(self) -> None: + try: + self._file.close() + except Exception: + logger.warning("WaveSink got error closing file on cleanup", exc_info=True) + class Transcriber(commands.Cog): @@ -51,10 +75,7 @@ class Transcriber(commands.Cog): self.bot = bot self._last_member = None - bsink = voice_recv.BasicSink(callback) - wsink = voice_recv.WaveSink(destination="/home/pi/Conjurer/wav.wav") - fsink = voice_recv.FFmpegSink(filename = ".home/pi/Conjurer/mp3.mp3") - self.sink_list = [bsink, wsink, fsink] + self.wsink = voice_recv.SRBuffer(destination="/home/pi/Conjurer/wav.wav") async def transcribe(): config = aai.TranscriptionConfig(speaker_labels=True) @@ -71,8 +92,7 @@ class Transcriber(commands.Cog): logger.info("Attempt transcribe") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") - msink = voice_recv.MultiAudioSink(destinations= self.sink_list) - vc.listen(msink) + vc.listen(self.wsink) @commands.command(name="stop_transcribe") @@ -80,4 +100,8 @@ class Transcriber(commands.Cog): await ctx.voice_client.disconnect() async def setup(bot): - await bot.add_cog(Transcriber(bot)) \ No newline at end of file + await bot.add_cog(Transcriber(bot)) + + +#1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. +#2. Transkrypcja to abstrakt - w zależności od tego która metoda jest włączona wysyła do odpowiedniego silnika, silniki offline odpalam na activcomie. Zaczynamy od assemblyai bo najlepiej wspiera polski. Potem zobaczymy co dalej. \ No newline at end of file From fddc6b19763b8423e3d70f47d667ad0e2ffdff58 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 21:50:32 +0200 Subject: [PATCH 056/283] Fix --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index b203d08..a8a73f3 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -43,7 +43,7 @@ class SRBuffer(voice_recv.AudioSink): - def __init__(self, destination: wave._File): + def __init__(self, destination): super().__init__() self._file: wave.Wave_write = wave.open(destination, 'wb') @@ -70,7 +70,7 @@ class SRBuffer(voice_recv.AudioSink): class Transcriber(commands.Cog): def __init__(self, bot): - def callback(user, data: voice_recv.VoiceData): + def callback(user, data): logger.info(f"Got packet from {user}") self.bot = bot From 6c58ce90ac4a74a9e317a1bc27ebd088e0492780 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 21:51:29 +0200 Subject: [PATCH 057/283] 2 --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index a8a73f3..66f4221 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -75,7 +75,7 @@ class Transcriber(commands.Cog): self.bot = bot self._last_member = None - self.wsink = voice_recv.SRBuffer(destination="/home/pi/Conjurer/wav.wav") + self.wsink = SRBuffer(destination="/home/pi/Conjurer/wav.wav") async def transcribe(): config = aai.TranscriptionConfig(speaker_labels=True) From cea6a37de555cf0d4083282fdf5ab16cc842aef3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 23:18:09 +0200 Subject: [PATCH 058/283] Events --- voice_recognition.py | 68 ++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 66f4221..91606b1 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -10,13 +10,47 @@ aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() # URL of the file to transcribe -FILE_URL = "https://github.com/AssemblyAI-Community/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3" - # You can also transcribe a local file by passing in a file path -# FILE_URL = './path/to/file.mp3' +# FILE_URL = './path/to/file.mWp3 logger = logging.getLogger("discord") +location = "/home/pi/Conjurer/" +class Events(commands.Cog): + def __init__(self, bot): + self.bot = bot + @commands.Cog.listener() + async def on_voice_state_update(self, before, after): + logger.info("before %s", before) + logger.info("after %s", after) + + +class UserTranscript(): + def __init__(self, user_id): + + self.username = str(user_id) + self.present_file_id = 0 + self.file_name_past = None + + self.file_name_present = location + self.username + str(self.present_file_id) + self.transcript_file_present = wave.Wave_write = wave.open(self.file_name_present, 'wb') + + self.file_name_future = location + self.username + str(self.present_file_id+1) + self.transcript_file_future = wave.Wave_write = wave.open(self.file_name_future, 'wb') + def rotate(self): + self.transcript_file_present.close() + self.name_file_past = self.file_name_present + self.present_file_id += 1 + self.file_name_present = self.file_name_future + self.transcript_file_present = self.transcript_file_future + self.transcript_file_future = location + self.username + str(self.present_file_id+1) + self.transcript_file_future = wave.Wave_write = wave.open(self.file_name_future, 'wb') + #send past to transcription + + def cleanup(self): + self.transcript_file_present.close() + self.transcript_file_future.close() + #send present to transcription class SRBuffer(voice_recv.AudioSink): """Endpoint AudioSink that generates a wav file. @@ -27,22 +61,10 @@ class SRBuffer(voice_recv.AudioSink): SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS SAMPLING_RATE = OpusDecoder.SAMPLING_RATE - ##on member join - 5 plików buforowych na człowieka otwarty jest tylko aktualny i następny + #on member join dodajemytypa do listy #on member disconnect - dropujemy go - # event: BasicSinkWriteCB, - # rtcp_event: Optional[BasicSinkWriteRTCPCB] = None, - # self.cb = event - # self.cb_rtcp = rtcp_event - # def write(self, user: Optional[User], data: VoiceData) -> None: - # self.cb(user, data) - # @AudioSink.listener() - # def on_rtcp_packet(self, packet: RTCPPacket, guild: discord.Guild) -> None: - # self.cb_rtcp(packet) if self.cb_rtcp else None - - - def __init__(self, destination): super().__init__() @@ -50,7 +72,7 @@ class SRBuffer(voice_recv.AudioSink): self._file.setnchannels(self.CHANNELS) self._file.setsampwidth(self.SAMPLE_WIDTH) self._file.setframerate(self.SAMPLING_RATE) - self.user_list = [] + self.user_list = {} def wants_opus(self) -> bool: return False @@ -67,22 +89,18 @@ class SRBuffer(voice_recv.AudioSink): logger.warning("WaveSink got error closing file on cleanup", exc_info=True) - class Transcriber(commands.Cog): def __init__(self, bot): - def callback(user, data): - logger.info(f"Got packet from {user}") - self.bot = bot self._last_member = None self.wsink = SRBuffer(destination="/home/pi/Conjurer/wav.wav") - async def transcribe(): - config = aai.TranscriptionConfig(speaker_labels=True) + async def transcribe(file_url): + config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") transcriber = aai.Transcriber() transcript = transcriber.transcribe( - FILE_URL, + file_url, config=config ) for utterance in transcript.utterances: @@ -101,7 +119,7 @@ class Transcriber(commands.Cog): async def setup(bot): await bot.add_cog(Transcriber(bot)) - + await bot.add_cog(Events(bot)) #1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. #2. Transkrypcja to abstrakt - w zależności od tego która metoda jest włączona wysyła do odpowiedniego silnika, silniki offline odpalam na activcomie. Zaczynamy od assemblyai bo najlepiej wspiera polski. Potem zobaczymy co dalej. \ No newline at end of file From 94208918d5534d3b85c349682089ff94234dc853 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 23:19:56 +0200 Subject: [PATCH 059/283] fx1 --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 91606b1..a2a7939 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -20,7 +20,7 @@ class Events(commands.Cog): self.bot = bot @commands.Cog.listener() - async def on_voice_state_update(self, before, after): + async def on_voice_state_update(before, after): logger.info("before %s", before) logger.info("after %s", after) From acfda1e29e4956cac1ded660eac9b08f7eefd4d8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 17 Sep 2024 23:21:45 +0200 Subject: [PATCH 060/283] fx2 --- voice_recognition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index a2a7939..4470412 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -20,9 +20,11 @@ class Events(commands.Cog): self.bot = bot @commands.Cog.listener() - async def on_voice_state_update(before, after): + async def on_voice_state_update(self, before, after, third): + logger.info("self %s", self) logger.info("before %s", before) logger.info("after %s", after) + logger.info("third %s", third) class UserTranscript(): From 7f57da86f096ba45b4afaa4f2a56f6bac0d04635 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Sep 2024 00:24:53 +0200 Subject: [PATCH 061/283] fix --- voice_recognition.py | 85 +++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 4470412..3724036 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -1,33 +1,54 @@ +import logging +import wave + import assemblyai as aai import discord from discord.ext import commands, voice_recv -import logging from discord.opus import Decoder as OpusDecoder -import wave # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() - +# VoiceState +# self_mute=False +# self_deaf=False +# self_stream=False +# suppress=False +# requested_to_speak_at=None +# channel=<VoiceChannel id=1285599336318632036 +# name='testing' +# rtc_region=None +# position=10 +# bitrate=64000 +# video_quality_mode=<VideoQualityMode.auto: 1> +# user_limit=0 +# category_id=1084451744496496753 # URL of the file to transcribe # You can also transcribe a local file by passing in a file path # FILE_URL = './path/to/file.mWp3 + logger = logging.getLogger("discord") location = "/home/pi/Conjurer/" + + class Events(commands.Cog): def __init__(self, bot): self.bot = bot @commands.Cog.listener() - async def on_voice_state_update(self, before, after, third): - logger.info("self %s", self) - logger.info("before %s", before) - logger.info("after %s", after) - logger.info("third %s", third) + async def on_voice_state_update(self, user, before, after): + if before.channel is None: + logger.info("User %s connected to channel %s", user, after.channel.name) + elif after.channel is None: + logger.info("User %s disconnected from channel %s", user, before.channel.name) + else: + logger.info("User VC status changed %s", user) + logger.info("Before %s", before) + logger.info("After %s", after) -class UserTranscript(): +class UserTranscript: def __init__(self, user_id): self.username = str(user_id) @@ -35,24 +56,34 @@ class UserTranscript(): self.file_name_past = None self.file_name_present = location + self.username + str(self.present_file_id) - self.transcript_file_present = wave.Wave_write = wave.open(self.file_name_present, 'wb') + self.transcript_file_present = wave.Wave_write = wave.open( + self.file_name_present, "wb" + ) + + self.file_name_future = location + self.username + str(self.present_file_id + 1) + self.transcript_file_future = wave.Wave_write = wave.open( + self.file_name_future, "wb" + ) - self.file_name_future = location + self.username + str(self.present_file_id+1) - self.transcript_file_future = wave.Wave_write = wave.open(self.file_name_future, 'wb') def rotate(self): self.transcript_file_present.close() self.name_file_past = self.file_name_present self.present_file_id += 1 self.file_name_present = self.file_name_future self.transcript_file_present = self.transcript_file_future - self.transcript_file_future = location + self.username + str(self.present_file_id+1) - self.transcript_file_future = wave.Wave_write = wave.open(self.file_name_future, 'wb') - #send past to transcription + self.transcript_file_future = ( + location + self.username + str(self.present_file_id + 1) + ) + self.transcript_file_future = wave.Wave_write = wave.open( + self.file_name_future, "wb" + ) + # send past to transcription def cleanup(self): self.transcript_file_present.close() self.transcript_file_future.close() - #send present to transcription + # send present to transcription + class SRBuffer(voice_recv.AudioSink): """Endpoint AudioSink that generates a wav file. @@ -63,14 +94,13 @@ class SRBuffer(voice_recv.AudioSink): SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS SAMPLING_RATE = OpusDecoder.SAMPLING_RATE - #on member join dodajemytypa do listy - #on member disconnect - dropujemy go - + # on member join dodajemytypa do listy + # on member disconnect - dropujemy go def __init__(self, destination): super().__init__() - self._file: wave.Wave_write = wave.open(destination, 'wb') + self._file: wave.Wave_write = wave.open(destination, "wb") self._file.setnchannels(self.CHANNELS) self._file.setsampwidth(self.SAMPLE_WIDTH) self._file.setframerate(self.SAMPLING_RATE) @@ -101,12 +131,10 @@ class Transcriber(commands.Cog): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") transcriber = aai.Transcriber() - transcript = transcriber.transcribe( - file_url, - config=config - ) + transcript = transcriber.transcribe(file_url, config=config) for utterance in transcript.utterances: - print(f"Speaker {utterance.speaker}: {utterance.text}") + print(f"Speaker {utterance.speaker}: {utterance.text}") + @commands.hybrid_command(name="transcribe") async def test(self, ctx): logger.info("Attempt transcribe") @@ -114,14 +142,15 @@ class Transcriber(commands.Cog): logger.info("Connected") vc.listen(self.wsink) - @commands.command(name="stop_transcribe") async def stop(self, ctx): await ctx.voice_client.disconnect() + async def setup(bot): await bot.add_cog(Transcriber(bot)) await bot.add_cog(Events(bot)) -#1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. -#2. Transkrypcja to abstrakt - w zależności od tego która metoda jest włączona wysyła do odpowiedniego silnika, silniki offline odpalam na activcomie. Zaczynamy od assemblyai bo najlepiej wspiera polski. Potem zobaczymy co dalej. \ No newline at end of file + +# 1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. +# 2. Transkrypcja to abstrakt - w zależności od tego która metoda jest włączona wysyła do odpowiedniego silnika, silniki offline odpalam na activcomie. Zaczynamy od assemblyai bo najlepiej wspiera polski. Potem zobaczymy co dalej. From 554852bc53c7062cd6499d6eef32f7e2a318862c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 12:40:30 +0200 Subject: [PATCH 062/283] 11 --- voice_recognition.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 3724036..edd7b6e 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -5,6 +5,7 @@ import assemblyai as aai import discord from discord.ext import commands, voice_recv from discord.opus import Decoder as OpusDecoder +from queue import Queue, Empty # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" @@ -126,6 +127,8 @@ class Transcriber(commands.Cog): self.bot = bot self._last_member = None self.wsink = SRBuffer(destination="/home/pi/Conjurer/wav.wav") + self.threads = [] + self.comm_queue = Queue() async def transcribe(file_url): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") @@ -138,7 +141,11 @@ class Transcriber(commands.Cog): @commands.hybrid_command(name="transcribe") async def test(self, ctx): logger.info("Attempt transcribe") - vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + if ctx.client.voice_clients: + voice_client = ctx.client.voice_clients[0] + if not voice_client.is_connected(): + logger.info("Connecting to voice") + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") vc.listen(self.wsink) From d3ac0ed0b17f1bac86381b41365bdf290744e03c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 12:43:12 +0200 Subject: [PATCH 063/283] A --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index edd7b6e..6e14412 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -141,8 +141,8 @@ class Transcriber(commands.Cog): @commands.hybrid_command(name="transcribe") async def test(self, ctx): logger.info("Attempt transcribe") - if ctx.client.voice_clients: - voice_client = ctx.client.voice_clients[0] + if self.bot.voice_clients: + voice_client = self.bot.voice_clients[0] if not voice_client.is_connected(): logger.info("Connecting to voice") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) From 12efaceaf630fc0d6bbb3ef2f999b2aa2d3d85d8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 12:51:43 +0200 Subject: [PATCH 064/283] TR --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 6e14412..a1afd80 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -145,9 +145,9 @@ class Transcriber(commands.Cog): voice_client = self.bot.voice_clients[0] if not voice_client.is_connected(): logger.info("Connecting to voice") - vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + voice_client = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + voice_client.listen(self.wsink) logger.info("Connected") - vc.listen(self.wsink) @commands.command(name="stop_transcribe") async def stop(self, ctx): From 3930b940e8d5ff9d115ad90988cd5064eefaecf7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 12:53:38 +0200 Subject: [PATCH 065/283] Fix --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index a1afd80..48ebf6a 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -146,7 +146,7 @@ class Transcriber(commands.Cog): if not voice_client.is_connected(): logger.info("Connecting to voice") voice_client = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) - voice_client.listen(self.wsink) + voice_client.listen(self.wsink) logger.info("Connected") @commands.command(name="stop_transcribe") From c6c1c2f25d75080f4479126db63d2ab4473bd1f5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 12:55:18 +0200 Subject: [PATCH 066/283] Fix --- voice_recognition.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index 48ebf6a..ac2eb73 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -146,6 +146,10 @@ class Transcriber(commands.Cog): if not voice_client.is_connected(): logger.info("Connecting to voice") voice_client = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + voice_client.listen(self.wsink) + else: + voice_client = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + voice_client.listen(self.wsink) logger.info("Connected") From 2d43f6a658b5bbd79ed6ee8b254622092fd1ba2a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 12:59:01 +0200 Subject: [PATCH 067/283] Revert --- voice_recognition.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index ac2eb73..84c65b1 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -141,17 +141,9 @@ class Transcriber(commands.Cog): @commands.hybrid_command(name="transcribe") async def test(self, ctx): logger.info("Attempt transcribe") - if self.bot.voice_clients: - voice_client = self.bot.voice_clients[0] - if not voice_client.is_connected(): - logger.info("Connecting to voice") - voice_client = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) - voice_client.listen(self.wsink) - else: - voice_client = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) - - voice_client.listen(self.wsink) + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info("Connected") + vc.listen(self.wsink) @commands.command(name="stop_transcribe") async def stop(self, ctx): From 5697f057d9cf93a1e37f2dcd070ce1d4fcfc8613 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 16:37:27 +0200 Subject: [PATCH 068/283] fx --- voice_recognition.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 84c65b1..f1f6a20 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -27,6 +27,9 @@ discord.opus._load_default() # URL of the file to transcribe # You can also transcribe a local file by passing in a file path # FILE_URL = './path/to/file.mWp3 +CHANNELS = OpusDecoder.CHANNELS +SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS +SAMPLING_RATE = OpusDecoder.SAMPLING_RATE logger = logging.getLogger("discord") @@ -49,7 +52,7 @@ class Events(commands.Cog): logger.info("After %s", after) -class UserTranscript: +class WaveWriter: def __init__(self, user_id): self.username = str(user_id) @@ -60,11 +63,18 @@ class UserTranscript: self.transcript_file_present = wave.Wave_write = wave.open( self.file_name_present, "wb" ) + self.transcript_file_present.setnchannels(CHANNELS) + self.transcript_file_present.setsampwidth(SAMPLE_WIDTH) + self.transcript_file_present.setframerate(SAMPLING_RATE) self.file_name_future = location + self.username + str(self.present_file_id + 1) self.transcript_file_future = wave.Wave_write = wave.open( self.file_name_future, "wb" ) + self.file_name_future.setnchannels(CHANNELS) + self.file_name_future.setsampwidth(SAMPLE_WIDTH) + self.file_name_future.setframerate(SAMPLING_RATE) + def rotate(self): self.transcript_file_present.close() @@ -78,7 +88,10 @@ class UserTranscript: self.transcript_file_future = wave.Wave_write = wave.open( self.file_name_future, "wb" ) - # send past to transcription + self.file_name_future.setnchannels(CHANNELS) + self.file_name_future.setsampwidth(SAMPLE_WIDTH) + self.file_name_future.setframerate(SAMPLING_RATE) + # send present to transcription def cleanup(self): self.transcript_file_present.close() @@ -91,9 +104,6 @@ class SRBuffer(voice_recv.AudioSink): Best used in conjunction with a silence generating sink. (TBD) """ - CHANNELS = OpusDecoder.CHANNELS - SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS - SAMPLING_RATE = OpusDecoder.SAMPLING_RATE # on member join dodajemytypa do listy # on member disconnect - dropujemy go @@ -102,10 +112,17 @@ class SRBuffer(voice_recv.AudioSink): super().__init__() self._file: wave.Wave_write = wave.open(destination, "wb") - self._file.setnchannels(self.CHANNELS) - self._file.setsampwidth(self.SAMPLE_WIDTH) - self._file.setframerate(self.SAMPLING_RATE) + self._file.setnchannels(CHANNELS) + self._file.setsampwidth(SAMPLE_WIDTH) + self._file.setframerate(SAMPLING_RATE) self.user_list = {} + self.wavewriter = {} + + def on_user_connect(self, username): + self.wavewriter[username] = WaveWriter(username) + + def on_user_disconnect(self,username): + self.wavewriter[username].cleanup() def wants_opus(self) -> bool: return False @@ -141,7 +158,9 @@ class Transcriber(commands.Cog): @commands.hybrid_command(name="transcribe") async def test(self, ctx): logger.info("Attempt transcribe") + vc = None vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + logger.info(self.bot.voice_clients) logger.info("Connected") vc.listen(self.wsink) From 6011ab5cd402e0dade81c19ff0139bba300bffa2 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 16:39:22 +0200 Subject: [PATCH 069/283] fx --- voice_recognition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index f1f6a20..56c3553 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -159,9 +159,10 @@ class Transcriber(commands.Cog): async def test(self, ctx): logger.info("Attempt transcribe") vc = None - vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) logger.info("Connected") + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + logger.info(self.bot.voice_clients) vc.listen(self.wsink) @commands.command(name="stop_transcribe") From 8b134ddcc9f49787a4c53e93bc0d966283f66983 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 16:44:09 +0200 Subject: [PATCH 070/283] Fix --- voice_recognition.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 56c3553..234ebf2 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -159,11 +159,18 @@ class Transcriber(commands.Cog): async def test(self, ctx): logger.info("Attempt transcribe") vc = None - logger.info(self.bot.voice_clients) - logger.info("Connected") - vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) - logger.info(self.bot.voice_clients) - vc.listen(self.wsink) + if self.bot.voice_clients: + if isinstance(self.bot.voicce_clients[0], voice_recv.VoiceRecvClient): + logger.info("Already transcribing") + else: + logger.info("Already connected with other client") + logger.info(self.bot.voice_clients) + + else: + logger.info("Connected") + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + logger.info(self.bot.voice_clients) + vc.listen(self.wsink) @commands.command(name="stop_transcribe") async def stop(self, ctx): From 73b7a4bfa4ac676bfb12f05d301b03a8ff4f0ffd Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 16:45:36 +0200 Subject: [PATCH 071/283] fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 234ebf2..2ef7ca7 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -160,7 +160,7 @@ class Transcriber(commands.Cog): logger.info("Attempt transcribe") vc = None if self.bot.voice_clients: - if isinstance(self.bot.voicce_clients[0], voice_recv.VoiceRecvClient): + if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient): logger.info("Already transcribing") else: logger.info("Already connected with other client") From dd0b679bfcce63f007553178193dc90311a54ad4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 16:55:09 +0200 Subject: [PATCH 072/283] fx --- bot.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/bot.py b/bot.py index 711fac0..6924761 100644 --- a/bot.py +++ b/bot.py @@ -542,16 +542,20 @@ async def connect(ctx, arg=None): """ if ctx and arg: logger.info("Ctx and arg defined for connect") - voice_channel = client.get_channel(1060349757349974066) if client.voice_clients: - voice_client = client.voice_clients[0] - if voice_client.is_connected(): + logger.info("Already connected with other client") + else: + vc = None + if ctx.author.voice.channel: + vc = await ctx.author.voice.channel.connect() + else: + voice_channel = client.get_channel(1060349757349974066) + vc = await voice_channel.connect() + if not vc: + logger.error("Not possible to connect to voice") return - voice_channel_connection = await voice_channel.connect() - if not voice_channel_connection: - logger.error("Not possible to connect to voice") - logger.info("Connecting to voice") + logger.info("Connected to voice") async def disconnect(ctx): From 2646178a2842dcd3568dfc0200a5d7c9a041aa80 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:30:43 +0200 Subject: [PATCH 073/283] fx --- voice_recognition.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 2ef7ca7..892b321 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -93,6 +93,9 @@ class WaveWriter: self.file_name_future.setframerate(SAMPLING_RATE) # send present to transcription + def writeframes(self, pcmdata): + self.transcript_file_present.writeframes(pcmdata) + def cleanup(self): self.transcript_file_present.close() self.transcript_file_future.close() @@ -103,20 +106,13 @@ class SRBuffer(voice_recv.AudioSink): """Endpoint AudioSink that generates a wav file. Best used in conjunction with a silence generating sink. (TBD) """ - - # on member join dodajemytypa do listy # on member disconnect - dropujemy go - def __init__(self, destination): super().__init__() - - self._file: wave.Wave_write = wave.open(destination, "wb") - self._file.setnchannels(CHANNELS) - self._file.setsampwidth(SAMPLE_WIDTH) - self._file.setframerate(SAMPLING_RATE) - self.user_list = {} - self.wavewriter = {} + self.wavewriter = { + "polishhammer" : WaveWriter("polishhammer") + } def on_user_connect(self, username): self.wavewriter[username] = WaveWriter(username) @@ -130,10 +126,11 @@ class SRBuffer(voice_recv.AudioSink): def write(self, user, data) -> None: logger.info("writing to file %s", self._file) logger.info("user: %s", user) - self._file.writeframes(data.pcm) + self.wavewriter[user].writeframes(data.pcm) def cleanup(self) -> None: try: + logger.info("Cleanup for SRBuffer") self._file.close() except Exception: logger.warning("WaveSink got error closing file on cleanup", exc_info=True) From e34dc37dc13a5429ad7bfd4377d4655f0e19efd1 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:35:23 +0200 Subject: [PATCH 074/283] fx --- voice_recognition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 892b321..6344a11 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -131,7 +131,8 @@ class SRBuffer(voice_recv.AudioSink): def cleanup(self) -> None: try: logger.info("Cleanup for SRBuffer") - self._file.close() + for _, item in self.wavewriter: + item.cleanup() except Exception: logger.warning("WaveSink got error closing file on cleanup", exc_info=True) From 1a31175db93a2a7c7cd3112b592d7c79eb321f37 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:42:34 +0200 Subject: [PATCH 075/283] aaa --- voice_recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 6344a11..524ed89 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -60,7 +60,7 @@ class WaveWriter: self.file_name_past = None self.file_name_present = location + self.username + str(self.present_file_id) - self.transcript_file_present = wave.Wave_write = wave.open( + self.transcript_file_present = wave.open( self.file_name_present, "wb" ) self.transcript_file_present.setnchannels(CHANNELS) @@ -68,7 +68,7 @@ class WaveWriter: self.transcript_file_present.setframerate(SAMPLING_RATE) self.file_name_future = location + self.username + str(self.present_file_id + 1) - self.transcript_file_future = wave.Wave_write = wave.open( + self.transcript_file_future = wave.open( self.file_name_future, "wb" ) self.file_name_future.setnchannels(CHANNELS) @@ -85,7 +85,7 @@ class WaveWriter: self.transcript_file_future = ( location + self.username + str(self.present_file_id + 1) ) - self.transcript_file_future = wave.Wave_write = wave.open( + self.transcript_file_future = wave.open( self.file_name_future, "wb" ) self.file_name_future.setnchannels(CHANNELS) From 1b0695d6c2d6ce3cd8122c7fed99818090ec9a9e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:45:23 +0200 Subject: [PATCH 076/283] FX --- voice_recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 524ed89..2ea3242 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -60,7 +60,7 @@ class WaveWriter: self.file_name_past = None self.file_name_present = location + self.username + str(self.present_file_id) - self.transcript_file_present = wave.open( + self.transcript_file_present : wave.Wave_write = wave.open( self.file_name_present, "wb" ) self.transcript_file_present.setnchannels(CHANNELS) @@ -68,7 +68,7 @@ class WaveWriter: self.transcript_file_present.setframerate(SAMPLING_RATE) self.file_name_future = location + self.username + str(self.present_file_id + 1) - self.transcript_file_future = wave.open( + self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) self.file_name_future.setnchannels(CHANNELS) @@ -85,7 +85,7 @@ class WaveWriter: self.transcript_file_future = ( location + self.username + str(self.present_file_id + 1) ) - self.transcript_file_future = wave.open( + self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) self.file_name_future.setnchannels(CHANNELS) From 2f183c3afb200ba23e84eb0a98bc5d7cda8d2dfb Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:48:41 +0200 Subject: [PATCH 077/283] fx --- voice_recognition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voice_recognition.py b/voice_recognition.py index 2ea3242..b2bb29b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -110,6 +110,7 @@ class SRBuffer(voice_recv.AudioSink): # on member disconnect - dropujemy go def __init__(self, destination): super().__init__() + self.wavewriter = { "polishhammer" : WaveWriter("polishhammer") } From e1e0a3569b81e454f6b496f2969f45dbe26117cd Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:52:30 +0200 Subject: [PATCH 078/283] :S --- voice_recognition.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index b2bb29b..fbd626d 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -71,9 +71,9 @@ class WaveWriter: self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) - self.file_name_future.setnchannels(CHANNELS) - self.file_name_future.setsampwidth(SAMPLE_WIDTH) - self.file_name_future.setframerate(SAMPLING_RATE) + self.transcript_file_future.setnchannels(CHANNELS) + self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) + self.transcript_file_future.setframerate(SAMPLING_RATE) def rotate(self): @@ -88,9 +88,9 @@ class WaveWriter: self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) - self.file_name_future.setnchannels(CHANNELS) - self.file_name_future.setsampwidth(SAMPLE_WIDTH) - self.file_name_future.setframerate(SAMPLING_RATE) + self.transcript_file_future.setnchannels(CHANNELS) + self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) + self.transcript_file_future.setframerate(SAMPLING_RATE) # send present to transcription def writeframes(self, pcmdata): From ad2a1a21e28e4f175748d59fb2d7957ca21eb253 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 17:55:50 +0200 Subject: [PATCH 079/283] :) --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index fbd626d..d91ecb4 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -132,7 +132,7 @@ class SRBuffer(voice_recv.AudioSink): def cleanup(self) -> None: try: logger.info("Cleanup for SRBuffer") - for _, item in self.wavewriter: + for item in self.wavewriter.values(): item.cleanup() except Exception: logger.warning("WaveSink got error closing file on cleanup", exc_info=True) From 9052e3e505dd8b6b73e8e95629d54fbd661f6b5b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 18:00:00 +0200 Subject: [PATCH 080/283] aaaa --- voice_recognition.py | 1 - 1 file changed, 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index d91ecb4..9780772 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -125,7 +125,6 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: - logger.info("writing to file %s", self._file) logger.info("user: %s", user) self.wavewriter[user].writeframes(data.pcm) From 373ea608d9b351c15078a7d7eb5c1e49abe3db91 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 18:04:18 +0200 Subject: [PATCH 081/283] fx --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 9780772..be3c4cf 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -112,7 +112,7 @@ class SRBuffer(voice_recv.AudioSink): super().__init__() self.wavewriter = { - "polishhammer" : WaveWriter("polishhammer") + "346956223645614080" : WaveWriter("346956223645614080") } def on_user_connect(self, username): @@ -126,7 +126,7 @@ class SRBuffer(voice_recv.AudioSink): def write(self, user, data) -> None: logger.info("user: %s", user) - self.wavewriter[user].writeframes(data.pcm) + self.wavewriter[str(user.id)].writeframes(data.pcm) def cleanup(self) -> None: try: From cb8ad5ca0814bf8abcdddc131b192da2a94f2274 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 18:13:36 +0200 Subject: [PATCH 082/283] fgf --- voice_recognition.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index be3c4cf..c4462ee 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -36,22 +36,6 @@ logger = logging.getLogger("discord") location = "/home/pi/Conjurer/" -class Events(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.Cog.listener() - async def on_voice_state_update(self, user, before, after): - if before.channel is None: - logger.info("User %s connected to channel %s", user, after.channel.name) - elif after.channel is None: - logger.info("User %s disconnected from channel %s", user, before.channel.name) - else: - logger.info("User VC status changed %s", user) - logger.info("Before %s", before) - logger.info("After %s", after) - - class WaveWriter: def __init__(self, user_id): @@ -94,6 +78,7 @@ class WaveWriter: # send present to transcription def writeframes(self, pcmdata): + logger.info(self.transcript_file_present) self.transcript_file_present.writeframes(pcmdata) def cleanup(self): @@ -170,6 +155,18 @@ class Transcriber(commands.Cog): logger.info(self.bot.voice_clients) vc.listen(self.wsink) + @commands.Cog.listener() + async def on_voice_state_update(self, user, before, after): + if before.channel is None: + logger.info("User %s connected to channel %s", user, after.channel.name) + elif after.channel is None: + logger.info("User %s disconnected from channel %s", user, before.channel.name) + else: + logger.info("User VC status changed %s", user) + logger.info("Before %s", before) + logger.info("After %s", after) + + @commands.command(name="stop_transcribe") async def stop(self, ctx): await ctx.voice_client.disconnect() @@ -177,7 +174,6 @@ class Transcriber(commands.Cog): async def setup(bot): await bot.add_cog(Transcriber(bot)) - await bot.add_cog(Events(bot)) # 1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. From 0ce3710b29a7a0b544d3a45bf89ffc89058fe8e3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 18:26:18 +0200 Subject: [PATCH 083/283] Fx --- voice_recognition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voice_recognition.py b/voice_recognition.py index c4462ee..d6b9db5 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -82,6 +82,7 @@ class WaveWriter: self.transcript_file_present.writeframes(pcmdata) def cleanup(self): + logger.info("Cleanup for user %s", self.username) self.transcript_file_present.close() self.transcript_file_future.close() # send present to transcription From 49f393fc476970f2248c542f3f9d9981e4c9567a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 19:09:44 +0200 Subject: [PATCH 084/283] aa --- voice_recognition.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index d6b9db5..00eb40b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -32,8 +32,11 @@ SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS SAMPLING_RATE = OpusDecoder.SAMPLING_RATE +#rotate file after there is 0.5s between last received pcm for user. +#delete messsages after user disconnect + logger = logging.getLogger("discord") -location = "/home/pi/Conjurer/" +location = "/home/pi/Conjurer/transcripts/" class WaveWriter: @@ -43,7 +46,7 @@ class WaveWriter: self.present_file_id = 0 self.file_name_past = None - self.file_name_present = location + self.username + str(self.present_file_id) + self.file_name_present = location + self.username + "_" + str(self.present_file_id) self.transcript_file_present : wave.Wave_write = wave.open( self.file_name_present, "wb" ) @@ -51,7 +54,7 @@ class WaveWriter: self.transcript_file_present.setsampwidth(SAMPLE_WIDTH) self.transcript_file_present.setframerate(SAMPLING_RATE) - self.file_name_future = location + self.username + str(self.present_file_id + 1) + self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) @@ -66,9 +69,7 @@ class WaveWriter: self.present_file_id += 1 self.file_name_present = self.file_name_future self.transcript_file_present = self.transcript_file_future - self.transcript_file_future = ( - location + self.username + str(self.present_file_id + 1) - ) + self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) @@ -106,12 +107,15 @@ class SRBuffer(voice_recv.AudioSink): def on_user_disconnect(self,username): self.wavewriter[username].cleanup() + self.wavewriter.pop(username) + + def rotate_user(self,username): + self.wavewriter[username].rotate() def wants_opus(self) -> bool: return False def write(self, user, data) -> None: - logger.info("user: %s", user) self.wavewriter[str(user.id)].writeframes(data.pcm) def cleanup(self) -> None: @@ -160,10 +164,14 @@ class Transcriber(commands.Cog): async def on_voice_state_update(self, user, before, after): if before.channel is None: logger.info("User %s connected to channel %s", user, after.channel.name) + self.wsink.on_user_connect(user.id) elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) + self.wsink.on_user_disconnect(user.id) + elif after.self_mute or after.mute: + self.wsink.rotate_user(user.id) else: - logger.info("User VC status changed %s", user) + logger.info("User VC status changed %s", user.id) logger.info("Before %s", before) logger.info("After %s", after) From 520d995553c78ac46a84f8126aee85201aed9b6c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 19:14:20 +0200 Subject: [PATCH 085/283] Fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 00eb40b..8c0b5fd 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -110,7 +110,7 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter.pop(username) def rotate_user(self,username): - self.wavewriter[username].rotate() + self.wavewriter[str(username)].rotate() def wants_opus(self) -> bool: return False From 28db612fad11b33f1dd1652a3fdf60ee04b87294 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 19:31:01 +0200 Subject: [PATCH 086/283] ff --- voice_recognition.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 8c0b5fd..8986b1e 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -46,7 +46,7 @@ class WaveWriter: self.present_file_id = 0 self.file_name_past = None - self.file_name_present = location + self.username + "_" + str(self.present_file_id) + self.file_name_present = location + self.username + "_" + str(self.present_file_id) + ".mp3" self.transcript_file_present : wave.Wave_write = wave.open( self.file_name_present, "wb" ) @@ -54,7 +54,7 @@ class WaveWriter: self.transcript_file_present.setsampwidth(SAMPLE_WIDTH) self.transcript_file_present.setframerate(SAMPLING_RATE) - self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) @@ -69,7 +69,7 @@ class WaveWriter: self.present_file_id += 1 self.file_name_present = self.file_name_future self.transcript_file_present = self.transcript_file_future - self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" ) @@ -95,7 +95,7 @@ class SRBuffer(voice_recv.AudioSink): """ # on member join dodajemytypa do listy # on member disconnect - dropujemy go - def __init__(self, destination): + def __init__(self): super().__init__() self.wavewriter = { @@ -103,11 +103,11 @@ class SRBuffer(voice_recv.AudioSink): } def on_user_connect(self, username): - self.wavewriter[username] = WaveWriter(username) + self.wavewriter[str(username)] = WaveWriter(username) def on_user_disconnect(self,username): - self.wavewriter[username].cleanup() - self.wavewriter.pop(username) + self.wavewriter[str(username)].cleanup() + self.wavewriter.pop(str(username)) def rotate_user(self,username): self.wavewriter[str(username)].rotate() @@ -131,7 +131,7 @@ class Transcriber(commands.Cog): def __init__(self, bot): self.bot = bot self._last_member = None - self.wsink = SRBuffer(destination="/home/pi/Conjurer/wav.wav") + self.wsink = SRBuffer() self.threads = [] self.comm_queue = Queue() From ada24f505d7ae36d64b291aa220d9a93db307d89 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:11:36 +0200 Subject: [PATCH 087/283] aa --- voice_recognition.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 8986b1e..5566596 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -3,10 +3,10 @@ import wave import assemblyai as aai import discord -from discord.ext import commands, voice_recv +from discord.ext import commands, voice_recv, tasks from discord.opus import Decoder as OpusDecoder from queue import Queue, Empty - +import time # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() @@ -99,7 +99,7 @@ class SRBuffer(voice_recv.AudioSink): super().__init__() self.wavewriter = { - "346956223645614080" : WaveWriter("346956223645614080") + "346956223645614080" : [WaveWriter("346956223645614080"), time.time_ns(), False] } def on_user_connect(self, username): @@ -116,7 +116,9 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: - self.wavewriter[str(user.id)].writeframes(data.pcm) + self.wavewriter[str(user.id)][0].writeframes(data.pcm) + self.wavewriter[str(user.id)][1] = time.time_ns() + self.wavewriter[str(user.id)][2] = True def cleanup(self) -> None: try: @@ -130,7 +132,6 @@ class SRBuffer(voice_recv.AudioSink): class Transcriber(commands.Cog): def __init__(self, bot): self.bot = bot - self._last_member = None self.wsink = SRBuffer() self.threads = [] self.comm_queue = Queue() @@ -160,6 +161,14 @@ class Transcriber(commands.Cog): logger.info(self.bot.voice_clients) vc.listen(self.wsink) + @tasks.loop(seconds=5) + async def check_data(self): + for item in self.wsink.wavewriter.values(): + print(item[0]) + print(item[1]) + print(item[2]) + print(time.time_ns()) + @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): if before.channel is None: @@ -167,9 +176,11 @@ class Transcriber(commands.Cog): self.wsink.on_user_connect(user.id) elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) - self.wsink.on_user_disconnect(user.id) - elif after.self_mute or after.mute: - self.wsink.rotate_user(user.id) + operation = { + "type": "user_cleanup", + user : str(user.id) + } + self.comm_queue.put(operation) else: logger.info("User VC status changed %s", user.id) logger.info("Before %s", before) From a4dceb8651998caa93c6908e54bae917d24ebf43 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:14:48 +0200 Subject: [PATCH 088/283] as --- voice_recognition.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 5566596..6ec7d79 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -98,12 +98,10 @@ class SRBuffer(voice_recv.AudioSink): def __init__(self): super().__init__() - self.wavewriter = { - "346956223645614080" : [WaveWriter("346956223645614080"), time.time_ns(), False] - } + self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username)] = WaveWriter(username) + self.wavewriter[str(username)] = [WaveWriter(username), time.time_ns(), False] def on_user_disconnect(self,username): self.wavewriter[str(username)].cleanup() From 33ded0ffe9ca15acd154f7542cdd4834f33fca11 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:18:46 +0200 Subject: [PATCH 089/283] zzz --- voice_recognition.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 6ec7d79..7b17093 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -122,7 +122,7 @@ class SRBuffer(voice_recv.AudioSink): try: logger.info("Cleanup for SRBuffer") for item in self.wavewriter.values(): - item.cleanup() + item[0].cleanup() except Exception: logger.warning("WaveSink got error closing file on cleanup", exc_info=True) @@ -162,10 +162,10 @@ class Transcriber(commands.Cog): @tasks.loop(seconds=5) async def check_data(self): for item in self.wsink.wavewriter.values(): - print(item[0]) - print(item[1]) - print(item[2]) - print(time.time_ns()) + self.logger.info(item[0]) + self.logger.info(item[1]) + self.logger.info(item[2]) + self.logger.info(time.time_ns()) @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): From 9567cb07e26fb51c630a1e31d38b643de789dd33 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:22:05 +0200 Subject: [PATCH 090/283] fx --- voice_recognition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voice_recognition.py b/voice_recognition.py index 7b17093..6f39512 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -158,6 +158,7 @@ class Transcriber(commands.Cog): vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) vc.listen(self.wsink) + self.check_data.start() @tasks.loop(seconds=5) async def check_data(self): From 13029b25b5f1476a7cfa2e0ebbe04656834d434d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:27:52 +0200 Subject: [PATCH 091/283] :) --- voice_recognition.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 6f39512..454ad5d 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -163,10 +163,10 @@ class Transcriber(commands.Cog): @tasks.loop(seconds=5) async def check_data(self): for item in self.wsink.wavewriter.values(): - self.logger.info(item[0]) - self.logger.info(item[1]) - self.logger.info(item[2]) - self.logger.info(time.time_ns()) + logger.info(item[0]) + logger.info(item[1]) + logger.info(item[2]) + logger.info(time.time_ns()) @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): From 4a769006a856b90bbe8f11835f500d2eb902f276 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:32:18 +0200 Subject: [PATCH 092/283] fx --- voice_recognition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voice_recognition.py b/voice_recognition.py index 454ad5d..fe408e7 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -114,6 +114,7 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: + logger.info("User %s", user) self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() self.wavewriter[str(user.id)][2] = True From 2a87153e718db378f6d69e8dd45732aca9946421 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:34:26 +0200 Subject: [PATCH 093/283] fx --- voice_recognition.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index fe408e7..b5676af 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -115,9 +115,10 @@ class SRBuffer(voice_recv.AudioSink): def write(self, user, data) -> None: logger.info("User %s", user) - self.wavewriter[str(user.id)][0].writeframes(data.pcm) - self.wavewriter[str(user.id)][1] = time.time_ns() - self.wavewriter[str(user.id)][2] = True + if user: + self.wavewriter[str(user.id)][0].writeframes(data.pcm) + self.wavewriter[str(user.id)][1] = time.time_ns() + self.wavewriter[str(user.id)][2] = True def cleanup(self) -> None: try: From 529c8ba20a219eba68432b97d53bcc4deacbd2ea Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:44:36 +0200 Subject: [PATCH 094/283] addfs --- voice_recognition.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index b5676af..3141ed5 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -42,7 +42,9 @@ location = "/home/pi/Conjurer/transcripts/" class WaveWriter: def __init__(self, user_id): - self.username = str(user_id) + self.username = str(user_id.id) + logger.info("Creating Wavewriter %s", self.username) + logger.info(user_id) self.present_file_id = 0 self.file_name_past = None @@ -101,20 +103,20 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username)] = [WaveWriter(username), time.time_ns(), False] + self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, False] - def on_user_disconnect(self,username): - self.wavewriter[str(username)].cleanup() - self.wavewriter.pop(str(username)) + def on_user_disconnect(self,username.id): + self.wavewriter[str(username.id)].cleanup() + self.wavewriter.pop(str(username.id)) def rotate_user(self,username): - self.wavewriter[str(username)].rotate() + self.wavewriter[str(username.id)].rotate() def wants_opus(self) -> bool: return False def write(self, user, data) -> None: - logger.info("User %s", user) + #logger.info("User %s", user) if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() @@ -162,7 +164,7 @@ class Transcriber(commands.Cog): vc.listen(self.wsink) self.check_data.start() - @tasks.loop(seconds=5) + @tasks.loop(seconds=0.5) async def check_data(self): for item in self.wsink.wavewriter.values(): logger.info(item[0]) @@ -174,12 +176,12 @@ class Transcriber(commands.Cog): async def on_voice_state_update(self, user, before, after): if before.channel is None: logger.info("User %s connected to channel %s", user, after.channel.name) - self.wsink.on_user_connect(user.id) + self.wsink.on_user_connect(user) elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) operation = { "type": "user_cleanup", - user : str(user.id) + user : user.id } self.comm_queue.put(operation) else: From e1a38bcdcd710ea4d0aa28d98e22b5c3f4afa2cb Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 20:47:14 +0200 Subject: [PATCH 095/283] fa --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 3141ed5..51445e2 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -105,7 +105,7 @@ class SRBuffer(voice_recv.AudioSink): def on_user_connect(self, username): self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, False] - def on_user_disconnect(self,username.id): + def on_user_disconnect(self,username): self.wavewriter[str(username.id)].cleanup() self.wavewriter.pop(str(username.id)) From 48fce96250076ec67a1490d59af55a50ce0a0d57 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 21:09:05 +0200 Subject: [PATCH 096/283] Test --- voice_recognition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voice_recognition.py b/voice_recognition.py index 51445e2..9f555ad 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -192,6 +192,7 @@ class Transcriber(commands.Cog): @commands.command(name="stop_transcribe") async def stop(self, ctx): + self.check_data.stop() await ctx.voice_client.disconnect() From 80360e0dde9833d7b3ed1091004408a8ce37c874 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 21:49:03 +0200 Subject: [PATCH 097/283] Fixtime --- voice_recognition.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 9f555ad..cf5ed4b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -10,35 +10,16 @@ import time # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() -# VoiceState -# self_mute=False -# self_deaf=False -# self_stream=False -# suppress=False -# requested_to_speak_at=None -# channel=<VoiceChannel id=1285599336318632036 -# name='testing' -# rtc_region=None -# position=10 -# bitrate=64000 -# video_quality_mode=<VideoQualityMode.auto: 1> -# user_limit=0 -# category_id=1084451744496496753 -# URL of the file to transcribe -# You can also transcribe a local file by passing in a file path -# FILE_URL = './path/to/file.mWp3 CHANNELS = OpusDecoder.CHANNELS SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS SAMPLING_RATE = OpusDecoder.SAMPLING_RATE - #rotate file after there is 0.5s between last received pcm for user. #delete messsages after user disconnect logger = logging.getLogger("discord") location = "/home/pi/Conjurer/transcripts/" - class WaveWriter: def __init__(self, user_id): @@ -167,11 +148,12 @@ class Transcriber(commands.Cog): @tasks.loop(seconds=0.5) async def check_data(self): for item in self.wsink.wavewriter.values(): + if time.time_ns() - item[1] > 10000: + pass logger.info(item[0]) logger.info(item[1]) logger.info(item[2]) - logger.info(time.time_ns()) - + logger.info("Diff : %s", time.time_ns() - item[1]) @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): if before.channel is None: From 1419be48a145e3c33bc1a6c0fbbf5b3a57a3e734 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 21:57:40 +0200 Subject: [PATCH 098/283] FX --- voice_recognition.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index cf5ed4b..deea6e7 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -154,8 +154,13 @@ class Transcriber(commands.Cog): logger.info(item[1]) logger.info(item[2]) logger.info("Diff : %s", time.time_ns() - item[1]) + @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): + if user == self.bot.user: + logger.info("Ignoring self") + return + if before.channel is None: logger.info("User %s connected to channel %s", user, after.channel.name) self.wsink.on_user_connect(user) From 3c177b7538b2f0df7e2eb158243394d20cb42808 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 22:59:12 +0200 Subject: [PATCH 099/283] Fix --- voice_recognition.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index deea6e7..608d3c9 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -84,7 +84,7 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, False] + self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, True] def on_user_disconnect(self,username): self.wavewriter[str(username.id)].cleanup() @@ -101,7 +101,8 @@ class SRBuffer(voice_recv.AudioSink): if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() - self.wavewriter[str(user.id)][2] = True + self.wavewriter[str(user.id)][2] = True #no writes + self.wavewriter[str(user.id)][3] = False #ready to rotate def cleanup(self) -> None: try: @@ -137,7 +138,6 @@ class Transcriber(commands.Cog): else: logger.info("Already connected with other client") logger.info(self.bot.voice_clients) - else: logger.info("Connected") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) @@ -148,11 +148,14 @@ class Transcriber(commands.Cog): @tasks.loop(seconds=0.5) async def check_data(self): for item in self.wsink.wavewriter.values(): - if time.time_ns() - item[1] > 10000: - pass - logger.info(item[0]) - logger.info(item[1]) - logger.info(item[2]) + logger.info("Item %s", item) + if not item[2] and not item[3]: + if time.time_ns() - item[1] > 34000000: + item[3] = True + elif not item[2] and item[3]: + item.rotate() + item[3] = False + item[2] = False logger.info("Diff : %s", time.time_ns() - item[1]) @commands.Cog.listener() From 6c68b574a209d22037aa5898f082558d148f82f3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 23:01:36 +0200 Subject: [PATCH 100/283] aa --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 608d3c9..d6e385b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -153,7 +153,7 @@ class Transcriber(commands.Cog): if time.time_ns() - item[1] > 34000000: item[3] = True elif not item[2] and item[3]: - item.rotate() + item[0].rotate() item[3] = False item[2] = False logger.info("Diff : %s", time.time_ns() - item[1]) From 945ed5677bc79863b8f679ff6e3cef1d517f8861 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Sep 2024 23:25:41 +0200 Subject: [PATCH 101/283] FX --- voice_recognition.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index d6e385b..cda8a00 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -101,8 +101,8 @@ class SRBuffer(voice_recv.AudioSink): if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() - self.wavewriter[str(user.id)][2] = True #no writes - self.wavewriter[str(user.id)][3] = False #ready to rotate + self.wavewriter[str(user.id)][2] = True #still transmiting + self.wavewriter[str(user.id)][3] = True #there is data to write def cleanup(self) -> None: try: @@ -149,12 +149,13 @@ class Transcriber(commands.Cog): async def check_data(self): for item in self.wsink.wavewriter.values(): logger.info("Item %s", item) - if not item[2] and not item[3]: - if time.time_ns() - item[1] > 34000000: - item[3] = True - elif not item[2] and item[3]: - item[0].rotate() - item[3] = False + if not item[2]: + timediff = time.time_ns() - item[1] + if timediff > 50000000 and item[3]: #timeout occured data to transmit exists + logger.info("File rotation at %s", timediff) + item[3] = False #buffer rotated + item[0].rotate() + #ensure only files with minimum length are sent to transcription item[2] = False logger.info("Diff : %s", time.time_ns() - item[1]) From fad17ef9a85d8b7c63064a69ab7e1b206e0622e4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:21:49 +0200 Subject: [PATCH 102/283] test --- test_time.py | 7 ++++++ voice_recognition.py | 52 ++++++++++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 test_time.py diff --git a/test_time.py b/test_time.py new file mode 100644 index 0000000..63b1f06 --- /dev/null +++ b/test_time.py @@ -0,0 +1,7 @@ +import time +first_time = time.time_ns() + +time.sleep(1) +time_diff = time.time_ns() - first_time +print(time_diff) +#2000149433 \ No newline at end of file diff --git a/voice_recognition.py b/voice_recognition.py index cda8a00..51a3156 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -21,14 +21,14 @@ logger = logging.getLogger("discord") location = "/home/pi/Conjurer/transcripts/" class WaveWriter: - def __init__(self, user_id): - + def __init__(self, user_id, queue): + self.queue = queue + self.user = user_id self.username = str(user_id.id) logger.info("Creating Wavewriter %s", self.username) logger.info(user_id) self.present_file_id = 0 self.file_name_past = None - self.file_name_present = location + self.username + "_" + str(self.present_file_id) + ".mp3" self.transcript_file_present : wave.Wave_write = wave.open( self.file_name_present, "wb" @@ -36,7 +36,6 @@ class WaveWriter: self.transcript_file_present.setnchannels(CHANNELS) self.transcript_file_present.setsampwidth(SAMPLE_WIDTH) self.transcript_file_present.setframerate(SAMPLING_RATE) - self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" self.transcript_file_future : wave.Wave_write = wave.open( self.file_name_future, "wb" @@ -45,11 +44,13 @@ class WaveWriter: self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) - def rotate(self): self.transcript_file_present.close() self.name_file_past = self.file_name_present - self.present_file_id += 1 + if self.present_file_id > 10: + self.present_file_id = 0 + else: + self.present_file_id += 1 self.file_name_present = self.file_name_future self.transcript_file_present = self.transcript_file_future self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" @@ -59,7 +60,12 @@ class WaveWriter: self.transcript_file_future.setnchannels(CHANNELS) self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) - # send present to transcription + operation = { + "type": "user_cleanup", + "user" : self.user, + "filename": self.name_file_past + } + self.queue.put(operation) def writeframes(self, pcmdata): logger.info(self.transcript_file_present) @@ -69,7 +75,6 @@ class WaveWriter: logger.info("Cleanup for user %s", self.username) self.transcript_file_present.close() self.transcript_file_future.close() - # send present to transcription class SRBuffer(voice_recv.AudioSink): @@ -78,9 +83,9 @@ class SRBuffer(voice_recv.AudioSink): """ # on member join dodajemytypa do listy # on member disconnect - dropujemy go - def __init__(self): + def __init__(self, queue): super().__init__() - + self.queue = queue self.wavewriter = {} def on_user_connect(self, username): @@ -90,9 +95,6 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter[str(username.id)].cleanup() self.wavewriter.pop(str(username.id)) - def rotate_user(self,username): - self.wavewriter[str(username.id)].rotate() - def wants_opus(self) -> bool: return False @@ -101,8 +103,8 @@ class SRBuffer(voice_recv.AudioSink): if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() - self.wavewriter[str(user.id)][2] = True #still transmiting - self.wavewriter[str(user.id)][3] = True #there is data to write + self.wavewriter[str(user.id)][2] = True + self.wavewriter[str(user.id)][3] = True def cleanup(self) -> None: try: @@ -113,12 +115,17 @@ class SRBuffer(voice_recv.AudioSink): logger.warning("WaveSink got error closing file on cleanup", exc_info=True) +class TranscriptionOutput(commands.Cog): + def __init__(self, bot): + pass + + class Transcriber(commands.Cog): def __init__(self, bot): self.bot = bot - self.wsink = SRBuffer() self.threads = [] self.comm_queue = Queue() + self.wsink = SRBuffer(self.comm_queue) async def transcribe(file_url): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") @@ -126,7 +133,7 @@ class Transcriber(commands.Cog): transcriber = aai.Transcriber() transcript = transcriber.transcribe(file_url, config=config) for utterance in transcript.utterances: - print(f"Speaker {utterance.speaker}: {utterance.text}") + logger.info("Speaker %s : %s", utterance.speaker, utterance.text) @commands.hybrid_command(name="transcribe") async def test(self, ctx): @@ -151,14 +158,20 @@ class Transcriber(commands.Cog): logger.info("Item %s", item) if not item[2]: timediff = time.time_ns() - item[1] - if timediff > 50000000 and item[3]: #timeout occured data to transmit exists + if timediff > 3000149433 and item[3]: #timeout occured data to transmit exists logger.info("File rotation at %s", timediff) item[3] = False #buffer rotated item[0].rotate() + #temporary - will be put in separate thread + self.transcribe(item[0].file_name_past) #ensure only files with minimum length are sent to transcription item[2] = False logger.info("Diff : %s", time.time_ns() - item[1]) + @tasks.loop(seconds=1) + async def scan_queue(self): + self.comm_queue.get() + @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): if user == self.bot.user: @@ -172,7 +185,8 @@ class Transcriber(commands.Cog): logger.info("User %s disconnected from channel %s", user, before.channel.name) operation = { "type": "user_cleanup", - user : user.id + "user" : user, + "filename": None } self.comm_queue.put(operation) else: From 582ae365c732492012a9d0461e326274faeb45b8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:24:20 +0200 Subject: [PATCH 103/283] Fa --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 51a3156..d617bee 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -89,7 +89,7 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, True] + self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, True, self.queue] def on_user_disconnect(self,username): self.wavewriter[str(username.id)].cleanup() From 0a248ef3fbcab8a3584ebcd90774afeeaae850d5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:27:11 +0200 Subject: [PATCH 104/283] fix --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index d617bee..b2dd518 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -89,7 +89,7 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username.id)] = [WaveWriter(username), time.time_ns(), False, True, self.queue] + self.wavewriter[str(username.id)] = [WaveWriter(username, self.queue), time.time_ns(), False, True] def on_user_disconnect(self,username): self.wavewriter[str(username.id)].cleanup() From cc5f97f1d690a62b35d348db1e97cf964a21af27 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:30:04 +0200 Subject: [PATCH 105/283] test --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index b2dd518..d7daea6 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -127,7 +127,7 @@ class Transcriber(commands.Cog): self.comm_queue = Queue() self.wsink = SRBuffer(self.comm_queue) - async def transcribe(file_url): + async def transcribe(self,file_url): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") transcriber = aai.Transcriber() From a60e99cd6e21706853e7aaef331330de2c2a0f3c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:31:48 +0200 Subject: [PATCH 106/283] fix --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index d7daea6..fcfe220 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -163,7 +163,7 @@ class Transcriber(commands.Cog): item[3] = False #buffer rotated item[0].rotate() #temporary - will be put in separate thread - self.transcribe(item[0].file_name_past) + await self.transcribe(item[0].file_name_past) #ensure only files with minimum length are sent to transcription item[2] = False logger.info("Diff : %s", time.time_ns() - item[1]) From 6a50568d25077ef695243e75869d599ae7746e86 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:34:41 +0200 Subject: [PATCH 107/283] fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index fcfe220..9005216 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -89,7 +89,7 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username.id)] = [WaveWriter(username, self.queue), time.time_ns(), False, True] + self.wavewriter[str(username.id)] = [WaveWriter(username, self.queue), time.time_ns(), False, False] def on_user_disconnect(self,username): self.wavewriter[str(username.id)].cleanup() From 9c686ded9817cae856a2b159d409a5e8d2627e0e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:41:50 +0200 Subject: [PATCH 108/283] aaa --- voice_recognition.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index 9005216..86cd0d7 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -45,6 +45,11 @@ class WaveWriter: self.transcript_file_future.setframerate(SAMPLING_RATE) def rotate(self): + + logger.info("Before rotation") + logger.info(self.file_name_present) + logger.info(self.name_file_past) + self.transcript_file_present.close() self.name_file_past = self.file_name_present if self.present_file_id > 10: @@ -65,6 +70,10 @@ class WaveWriter: "user" : self.user, "filename": self.name_file_past } + logger.info("After rotation") + logger.info(self.file_name_present) + logger.info(self.name_file_past) + self.queue.put(operation) def writeframes(self, pcmdata): From 02e8d4effdf279df6a97d16c8920125331c62761 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:45:19 +0200 Subject: [PATCH 109/283] fix --- voice_recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 86cd0d7..b86f6ca 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -48,10 +48,10 @@ class WaveWriter: logger.info("Before rotation") logger.info(self.file_name_present) - logger.info(self.name_file_past) + logger.info(self.file_name_past) self.transcript_file_present.close() - self.name_file_past = self.file_name_present + self.file_name_past = self.file_name_present if self.present_file_id > 10: self.present_file_id = 0 else: @@ -72,7 +72,7 @@ class WaveWriter: } logger.info("After rotation") logger.info(self.file_name_present) - logger.info(self.name_file_past) + logger.info(self.file_name_past) self.queue.put(operation) From 0ed0f60b9f830f78d08b93597686c782ee39365d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 00:49:06 +0200 Subject: [PATCH 110/283] fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index b86f6ca..4e90cad 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -68,7 +68,7 @@ class WaveWriter: operation = { "type": "user_cleanup", "user" : self.user, - "filename": self.name_file_past + "filename": self.file_name_present } logger.info("After rotation") logger.info(self.file_name_present) From dbca290979310d7c6551cf6cdadebc893f7f3f2d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:16:18 +0200 Subject: [PATCH 111/283] Hack na hacku hackiem pogania --- voice_recognition.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 4e90cad..53f9361 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -77,7 +77,6 @@ class WaveWriter: self.queue.put(operation) def writeframes(self, pcmdata): - logger.info(self.transcript_file_present) self.transcript_file_present.writeframes(pcmdata) def cleanup(self): @@ -98,7 +97,7 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter = {} def on_user_connect(self, username): - self.wavewriter[str(username.id)] = [WaveWriter(username, self.queue), time.time_ns(), False, False] + self.wavewriter[str(username.id)] = [WaveWriter(username, self.queue), time.time_ns(), time.time_ns(), False, False] def on_user_disconnect(self,username): self.wavewriter[str(username.id)].cleanup() @@ -108,12 +107,12 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: - #logger.info("User %s", user) if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) - self.wavewriter[str(user.id)][1] = time.time_ns() - self.wavewriter[str(user.id)][2] = True + self.wavewriter[str(user.id)][1] = time.time_ns() #time from last write + self.wavewriter[str(user.id)][2] = time.time_ns() #time since last rotation self.wavewriter[str(user.id)][3] = True + self.wavewriter[str(user.id)][4] = True def cleanup(self) -> None: try: @@ -125,9 +124,15 @@ class SRBuffer(voice_recv.AudioSink): class TranscriptionOutput(commands.Cog): - def __init__(self, bot): - pass - + def __init__(self, bot, queue): + self.bot = bot + self.queue = queue + self.work = False + @discord.ext.tasks.loop() + def scan_loop(self): + while self.work: + time.sleep(1.0) + logger.info("Output scan loop exited succesfully") class Transcriber(commands.Cog): def __init__(self, bot): @@ -135,7 +140,7 @@ class Transcriber(commands.Cog): self.threads = [] self.comm_queue = Queue() self.wsink = SRBuffer(self.comm_queue) - + self.output_handler = TranscriptionOutput(self.bot, self.comm_queue) async def transcribe(self,file_url): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") @@ -148,6 +153,7 @@ class Transcriber(commands.Cog): async def test(self, ctx): logger.info("Attempt transcribe") vc = None + if self.bot.voice_clients: if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient): logger.info("Already transcribing") @@ -158,6 +164,9 @@ class Transcriber(commands.Cog): logger.info("Connected") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) + self.output_handler.work = True + self.output_handler.scan_loop.start() + vc.listen(self.wsink) self.check_data.start() @@ -165,16 +174,17 @@ class Transcriber(commands.Cog): async def check_data(self): for item in self.wsink.wavewriter.values(): logger.info("Item %s", item) - if not item[2]: - timediff = time.time_ns() - item[1] - if timediff > 3000149433 and item[3]: #timeout occured data to transmit exists - logger.info("File rotation at %s", timediff) - item[3] = False #buffer rotated + if not item[3]: + timediff_rotation = time.time_ns() - item[2] + timediff_write = time.time_ns() -item[1] + if timediff_rotation > 3000149433 and timediff_write > 500014943 and item[4]: + logger.info("File rotation time since last write %s", timediff_write/1e9) + logger.info("File rotation time since last rotation %s", timediff_rotation/1e9) + item[4] = False item[0].rotate() #temporary - will be put in separate thread await self.transcribe(item[0].file_name_past) - #ensure only files with minimum length are sent to transcription - item[2] = False + item[4] = False logger.info("Diff : %s", time.time_ns() - item[1]) @tasks.loop(seconds=1) @@ -206,6 +216,8 @@ class Transcriber(commands.Cog): @commands.command(name="stop_transcribe") async def stop(self, ctx): + + self.output_handler.work = False self.check_data.stop() await ctx.voice_client.disconnect() From 62bb650b3704a9dbb6825a03e8a39e43e760fd53 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:18:36 +0200 Subject: [PATCH 112/283] Fix fo hack --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 53f9361..79851f3 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -128,8 +128,8 @@ class TranscriptionOutput(commands.Cog): self.bot = bot self.queue = queue self.work = False - @discord.ext.tasks.loop() - def scan_loop(self): + @tasks.loop() + async def scan_loop(self): while self.work: time.sleep(1.0) logger.info("Output scan loop exited succesfully") From e3ad25b95ededf59a7ffb7b9a91e58ba1d775004 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:21:20 +0200 Subject: [PATCH 113/283] Fix asyncio sleep --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 79851f3..8b4ad2f 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -1,6 +1,6 @@ import logging import wave - +import asyncio import assemblyai as aai import discord from discord.ext import commands, voice_recv, tasks @@ -131,7 +131,7 @@ class TranscriptionOutput(commands.Cog): @tasks.loop() async def scan_loop(self): while self.work: - time.sleep(1.0) + asyncio.sleep(1.0) logger.info("Output scan loop exited succesfully") class Transcriber(commands.Cog): From b2e414a268563d624361b1d4fe71888490c18bb9 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:23:27 +0200 Subject: [PATCH 114/283] tst --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 8b4ad2f..06889a2 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -128,7 +128,7 @@ class TranscriptionOutput(commands.Cog): self.bot = bot self.queue = queue self.work = False - @tasks.loop() + @tasks.loop(minutes=4600) async def scan_loop(self): while self.work: asyncio.sleep(1.0) From 39865253fe237f4d3d3521dc4eafc4b41860094c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:33:52 +0200 Subject: [PATCH 115/283] tst --- voice_recognition.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 06889a2..fed0e30 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -7,6 +7,7 @@ from discord.ext import commands, voice_recv, tasks from discord.opus import Decoder as OpusDecoder from queue import Queue, Empty import time +import threading # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() @@ -20,6 +21,7 @@ SAMPLING_RATE = OpusDecoder.SAMPLING_RATE logger = logging.getLogger("discord") location = "/home/pi/Conjurer/transcripts/" + class WaveWriter: def __init__(self, user_id, queue): self.queue = queue @@ -123,24 +125,12 @@ class SRBuffer(voice_recv.AudioSink): logger.warning("WaveSink got error closing file on cleanup", exc_info=True) -class TranscriptionOutput(commands.Cog): - def __init__(self, bot, queue): - self.bot = bot - self.queue = queue - self.work = False - @tasks.loop(minutes=4600) - async def scan_loop(self): - while self.work: - asyncio.sleep(1.0) - logger.info("Output scan loop exited succesfully") - class Transcriber(commands.Cog): def __init__(self, bot): self.bot = bot self.threads = [] self.comm_queue = Queue() self.wsink = SRBuffer(self.comm_queue) - self.output_handler = TranscriptionOutput(self.bot, self.comm_queue) async def transcribe(self,file_url): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") @@ -168,7 +158,6 @@ class Transcriber(commands.Cog): self.output_handler.scan_loop.start() vc.listen(self.wsink) - self.check_data.start() @tasks.loop(seconds=0.5) async def check_data(self): @@ -221,8 +210,22 @@ class Transcriber(commands.Cog): self.check_data.stop() await ctx.voice_client.disconnect() - +async def transcribe_output_queue(): + while True: + asyncio.sleep(10.0) + logger.info("PING") async def setup(bot): + logger = logging.getLogger("discord") + logger.info("Started comms") + threads = [] + #threads.append(threading.Thread(target=flask_debug)) + threads.append(threading.Thread(target=transcribe_output_queue)) + + for worker in threads: + worker.start() + for worker in threads: + worker.join() + await bot.add_cog(Transcriber(bot)) From b78f5427789ddc7cfd5d14210007f90b80f50f3f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:36:00 +0200 Subject: [PATCH 116/283] 1 --- voice_recognition.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index fed0e30..b9655f8 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -216,15 +216,18 @@ async def transcribe_output_queue(): logger.info("PING") async def setup(bot): logger = logging.getLogger("discord") - logger.info("Started comms") + logger.info("Loading voice transcribed module phase one") threads = [] - #threads.append(threading.Thread(target=flask_debug)) threads.append(threading.Thread(target=transcribe_output_queue)) + logger.info("Loading voice transcribed phase two") for worker in threads: worker.start() + logger.info("Loading voice transcribed module phase three") + for worker in threads: worker.join() + logger.info("Loading voice transcribed module phase four") await bot.add_cog(Transcriber(bot)) From 2e135934339dc835ff6f731cd205187d4fb8a397 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:37:52 +0200 Subject: [PATCH 117/283] tst 2 --- voice_recognition.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index b9655f8..79520bb 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -210,10 +210,11 @@ class Transcriber(commands.Cog): self.check_data.stop() await ctx.voice_client.disconnect() -async def transcribe_output_queue(): +def transcribe_output_queue(): while True: - asyncio.sleep(10.0) + time.sleep(10.0) logger.info("PING") + async def setup(bot): logger = logging.getLogger("discord") logger.info("Loading voice transcribed module phase one") @@ -231,6 +232,7 @@ async def setup(bot): await bot.add_cog(Transcriber(bot)) + logger.info("Loading voice transcribed module done") # 1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. # 2. Transkrypcja to abstrakt - w zależności od tego która metoda jest włączona wysyła do odpowiedniego silnika, silniki offline odpalam na activcomie. Zaczynamy od assemblyai bo najlepiej wspiera polski. Potem zobaczymy co dalej. From b3121aaaf2f1bf6c362b2788b89e4e8f00022ff6 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 13:41:14 +0200 Subject: [PATCH 118/283] ii --- voice_recognition.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 79520bb..421a540 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -226,9 +226,6 @@ async def setup(bot): worker.start() logger.info("Loading voice transcribed module phase three") - for worker in threads: - worker.join() - logger.info("Loading voice transcribed module phase four") await bot.add_cog(Transcriber(bot)) From b025790b3d56694f1147c09b47eada924cc483a5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 14:19:16 +0200 Subject: [PATCH 119/283] Thread --- voice_recognition.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 421a540..4915763 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -126,10 +126,11 @@ class SRBuffer(voice_recv.AudioSink): class Transcriber(commands.Cog): - def __init__(self, bot): + def __init__(self, bot, worker, queue): self.bot = bot self.threads = [] - self.comm_queue = Queue() + self.comm_queue = queue + self.worker = worker self.wsink = SRBuffer(self.comm_queue) async def transcribe(self,file_url): config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") @@ -156,7 +157,7 @@ class Transcriber(commands.Cog): logger.info(self.bot.voice_clients) self.output_handler.work = True self.output_handler.scan_loop.start() - + self.worker.start() vc.listen(self.wsink) @tasks.loop(seconds=0.5) @@ -208,27 +209,22 @@ class Transcriber(commands.Cog): self.output_handler.work = False self.check_data.stop() + self.worker.stop() await ctx.voice_client.disconnect() -def transcribe_output_queue(): +def transcribe_output_queue(queue): while True: - time.sleep(10.0) + queue.get() logger.info("PING") async def setup(bot): logger = logging.getLogger("discord") logger.info("Loading voice transcribed module phase one") - threads = [] - threads.append(threading.Thread(target=transcribe_output_queue)) + worker = threading.Thread(target=transcribe_output_queue) logger.info("Loading voice transcribed phase two") - - for worker in threads: - worker.start() + queue = Queue() logger.info("Loading voice transcribed module phase three") - - - await bot.add_cog(Transcriber(bot)) - + await bot.add_cog(Transcriber(bot, worker, queue)) logger.info("Loading voice transcribed module done") # 1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. From e628b18354939c1ae36746fa75a860f1c750c7b0 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 14:27:46 +0200 Subject: [PATCH 120/283] asa --- voice_recognition.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 4915763..3cde36c 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -155,8 +155,6 @@ class Transcriber(commands.Cog): logger.info("Connected") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) - self.output_handler.work = True - self.output_handler.scan_loop.start() self.worker.start() vc.listen(self.wsink) @@ -206,8 +204,6 @@ class Transcriber(commands.Cog): @commands.command(name="stop_transcribe") async def stop(self, ctx): - - self.output_handler.work = False self.check_data.stop() self.worker.stop() await ctx.voice_client.disconnect() From 88926ec6275997453e51fc1abbc3b7dca7aa47c9 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 14:42:18 +0200 Subject: [PATCH 121/283] Fx --- voice_recognition.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 3cde36c..4ffecf3 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -112,9 +112,8 @@ class SRBuffer(voice_recv.AudioSink): if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() #time from last write - self.wavewriter[str(user.id)][2] = time.time_ns() #time since last rotation - self.wavewriter[str(user.id)][3] = True - self.wavewriter[str(user.id)][4] = True + self.wavewriter[str(user.id)][3] = True #data written in last iter + self.wavewriter[str(user.id)][4] = True #data in buffer def cleanup(self) -> None: try: @@ -168,16 +167,11 @@ class Transcriber(commands.Cog): if timediff_rotation > 3000149433 and timediff_write > 500014943 and item[4]: logger.info("File rotation time since last write %s", timediff_write/1e9) logger.info("File rotation time since last rotation %s", timediff_rotation/1e9) + item[2] = time.time_ns() item[4] = False item[0].rotate() - #temporary - will be put in separate thread await self.transcribe(item[0].file_name_past) - item[4] = False - logger.info("Diff : %s", time.time_ns() - item[1]) - - @tasks.loop(seconds=1) - async def scan_queue(self): - self.comm_queue.get() + item[3] = False @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): @@ -209,6 +203,7 @@ class Transcriber(commands.Cog): await ctx.voice_client.disconnect() def transcribe_output_queue(queue): + logger.info("Worker start") while True: queue.get() logger.info("PING") From 4184cbb687a53e243fbf0ce2ac48dd1d9119e5ca Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 15:48:00 +0200 Subject: [PATCH 122/283] Fix --- voice_recognition.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 4ffecf3..579b594 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -154,6 +154,7 @@ class Transcriber(commands.Cog): logger.info("Connected") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) + self.check_data.start() self.worker.start() vc.listen(self.wsink) @@ -199,13 +200,15 @@ class Transcriber(commands.Cog): @commands.command(name="stop_transcribe") async def stop(self, ctx): self.check_data.stop() - self.worker.stop() + self.comm_queue.put("STOP") await ctx.voice_client.disconnect() def transcribe_output_queue(queue): logger.info("Worker start") while True: - queue.get() + item = queue.get() + if item == "STOP": + break logger.info("PING") async def setup(bot): From 0278dc3a49e5411ce502f87e5e4130831f62569c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:06:05 +0200 Subject: [PATCH 123/283] Fx test --- voice_recognition.py | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 579b594..0778b90 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -21,6 +21,11 @@ SAMPLING_RATE = OpusDecoder.SAMPLING_RATE logger = logging.getLogger("discord") location = "/home/pi/Conjurer/transcripts/" +class CommunicationObject: + def __init__(self, msg_type, user, data): + self.type = msg_type + self.user = user + self.data = data class WaveWriter: def __init__(self, user_id, queue): @@ -67,11 +72,7 @@ class WaveWriter: self.transcript_file_future.setnchannels(CHANNELS) self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) - operation = { - "type": "user_cleanup", - "user" : self.user, - "filename": self.file_name_present - } + operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_present]) logger.info("After rotation") logger.info(self.file_name_present) logger.info(self.file_name_past) @@ -131,13 +132,6 @@ class Transcriber(commands.Cog): self.comm_queue = queue self.worker = worker self.wsink = SRBuffer(self.comm_queue) - async def transcribe(self,file_url): - config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") - - transcriber = aai.Transcriber() - transcript = transcriber.transcribe(file_url, config=config) - for utterance in transcript.utterances: - logger.info("Speaker %s : %s", utterance.speaker, utterance.text) @commands.hybrid_command(name="transcribe") async def test(self, ctx): @@ -171,7 +165,6 @@ class Transcriber(commands.Cog): item[2] = time.time_ns() item[4] = False item[0].rotate() - await self.transcribe(item[0].file_name_past) item[3] = False @commands.Cog.listener() @@ -185,11 +178,7 @@ class Transcriber(commands.Cog): self.wsink.on_user_connect(user) elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) - operation = { - "type": "user_cleanup", - "user" : user, - "filename": None - } + operation = CommunicationObject(msg_type="user_cleanup", user=user, data=None) self.comm_queue.put(operation) else: logger.info("User VC status changed %s", user.id) @@ -200,22 +189,34 @@ class Transcriber(commands.Cog): @commands.command(name="stop_transcribe") async def stop(self, ctx): self.check_data.stop() - self.comm_queue.put("STOP") + stop_token = CommunicationObject("STOP",None,None) + self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() def transcribe_output_queue(queue): - logger.info("Worker start") + logger.info("Transcript worker start") + config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") + transcriber = aai.Transcriber() while True: item = queue.get() - if item == "STOP": + if item.type == "STOP": + logger.info("Queue ended") break - logger.info("PING") + elif item.type == "send_file": + transcript = "" + for iter in item.data: + transcript = transcriber.transcribe(iter, config=config) + for utterance in transcript.utterances: + logger.info("%s : %s", item.user, utterance.text) + elif item.type == "user_cleanup": + logger.info("User %s disconnected - cleanup action") + logger.info("End transcript worker") async def setup(bot): logger = logging.getLogger("discord") logger.info("Loading voice transcribed module phase one") worker = threading.Thread(target=transcribe_output_queue) - logger.info("Loading voice transcribed phase two") + logger.info("Loading voice transcribed phasetwo") queue = Queue() logger.info("Loading voice transcribed module phase three") await bot.add_cog(Transcriber(bot, worker, queue)) From d027a961b7a7668d1a1bcc722b457ac20d7d03ef Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:08:43 +0200 Subject: [PATCH 124/283] fx --- voice_recognition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index 0778b90..2cf0232 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -110,6 +110,7 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: + logger.info("DAta write") if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() #time from last write @@ -193,6 +194,8 @@ class Transcriber(commands.Cog): self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() + +#this needs to be async and transcript fun awaited def transcribe_output_queue(queue): logger.info("Transcript worker start") config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") From f1d73fc0a10dce1d2877acefaaa997d9486ef44d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:14:45 +0200 Subject: [PATCH 125/283] tst --- voice_recognition.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index 2cf0232..6230618 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -150,9 +150,11 @@ class Transcriber(commands.Cog): vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) self.check_data.start() + logger.info("Start worker!~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") self.worker.start() vc.listen(self.wsink) + @tasks.loop(seconds=0.5) async def check_data(self): for item in self.wsink.wavewriter.values(): From 26c34c6fa921c737ed79ca0cd40c7a83f1ab838a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:26:01 +0200 Subject: [PATCH 126/283] asynv --- voice_recognition.py | 53 ++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 6230618..a589fbb 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -5,7 +5,6 @@ import assemblyai as aai import discord from discord.ext import commands, voice_recv, tasks from discord.opus import Decoder as OpusDecoder -from queue import Queue, Empty import time import threading # Replace with your API key @@ -127,11 +126,10 @@ class SRBuffer(voice_recv.AudioSink): class Transcriber(commands.Cog): - def __init__(self, bot, worker, queue): + def __init__(self, bot): self.bot = bot self.threads = [] - self.comm_queue = queue - self.worker = worker + self.comm_queue = asyncio.Queue() self.wsink = SRBuffer(self.comm_queue) @commands.hybrid_command(name="transcribe") @@ -151,7 +149,7 @@ class Transcriber(commands.Cog): logger.info(self.bot.voice_clients) self.check_data.start() logger.info("Start worker!~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - self.worker.start() + asyncio.run(self.transcribe_output_queue()) vc.listen(self.wsink) @@ -196,35 +194,28 @@ class Transcriber(commands.Cog): self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() - -#this needs to be async and transcript fun awaited -def transcribe_output_queue(queue): - logger.info("Transcript worker start") - config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") - transcriber = aai.Transcriber() - while True: - item = queue.get() - if item.type == "STOP": - logger.info("Queue ended") - break - elif item.type == "send_file": - transcript = "" - for iter in item.data: - transcript = transcriber.transcribe(iter, config=config) - for utterance in transcript.utterances: - logger.info("%s : %s", item.user, utterance.text) - elif item.type == "user_cleanup": - logger.info("User %s disconnected - cleanup action") - logger.info("End transcript worker") + async def transcribe_output_queue(self): + logger.info("Transcript worker start") + config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") + transcriber = aai.Transcriber() + while True: + item = self.queue.get() + if item.type == "STOP": + logger.info("Queue ended") + break + elif item.type == "send_file": + transcript = "" + for iter in item.data: + transcript = transcriber.transcribe(iter, config=config) + for utterance in transcript.utterances: + logger.info("%s : %s", item.user, utterance.text) + elif item.type == "user_cleanup": + logger.info("User %s disconnected - cleanup action") + logger.info("End transcript worker") async def setup(bot): logger = logging.getLogger("discord") - logger.info("Loading voice transcribed module phase one") - worker = threading.Thread(target=transcribe_output_queue) - logger.info("Loading voice transcribed phasetwo") - queue = Queue() - logger.info("Loading voice transcribed module phase three") - await bot.add_cog(Transcriber(bot, worker, queue)) + await bot.add_cog(Transcriber(bot)) logger.info("Loading voice transcribed module done") # 1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. From c714bdb2d7d3a89c233bec4c4285774d6ca7a19a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:30:43 +0200 Subject: [PATCH 127/283] async --- voice_recognition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index a589fbb..f6765db 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -149,7 +149,9 @@ class Transcriber(commands.Cog): logger.info(self.bot.voice_clients) self.check_data.start() logger.info("Start worker!~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - asyncio.run(self.transcribe_output_queue()) + #ev_loop = asyncio.get_event_loop() + run_loop = asyncio.get_running_loop() + run_loop.run(self.transcribe_output_queue()) vc.listen(self.wsink) From 81c477999fb363539ba1e6c8a3458f3a93794f05 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:31:10 +0200 Subject: [PATCH 128/283] fx --- voice_recognition.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index f6765db..79b124c 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -6,7 +6,6 @@ import discord from discord.ext import commands, voice_recv, tasks from discord.opus import Decoder as OpusDecoder import time -import threading # Replace with your API key aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" discord.opus._load_default() @@ -52,10 +51,6 @@ class WaveWriter: def rotate(self): - logger.info("Before rotation") - logger.info(self.file_name_present) - logger.info(self.file_name_past) - self.transcript_file_present.close() self.file_name_past = self.file_name_present if self.present_file_id > 10: @@ -72,9 +67,7 @@ class WaveWriter: self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_present]) - logger.info("After rotation") - logger.info(self.file_name_present) - logger.info(self.file_name_past) + logger.info("Item added to queue %s %s", self.queue, operation) self.queue.put(operation) @@ -109,7 +102,7 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: - logger.info("DAta write") + #logger.info("DAta write") if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() #time from last write @@ -131,6 +124,7 @@ class Transcriber(commands.Cog): self.threads = [] self.comm_queue = asyncio.Queue() self.wsink = SRBuffer(self.comm_queue) + self.worker = None @commands.hybrid_command(name="transcribe") async def test(self, ctx): @@ -151,7 +145,7 @@ class Transcriber(commands.Cog): logger.info("Start worker!~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") #ev_loop = asyncio.get_event_loop() run_loop = asyncio.get_running_loop() - run_loop.run(self.transcribe_output_queue()) + self.worker = run_loop.create_task(self.transcribe_output_queue()) vc.listen(self.wsink) @@ -182,6 +176,7 @@ class Transcriber(commands.Cog): elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) operation = CommunicationObject(msg_type="user_cleanup", user=user, data=None) + logger.info("Item added to queue %s %s", self.queue, operation) self.comm_queue.put(operation) else: logger.info("User VC status changed %s", user.id) @@ -193,6 +188,8 @@ class Transcriber(commands.Cog): async def stop(self, ctx): self.check_data.stop() stop_token = CommunicationObject("STOP",None,None) + logger.info("Item added to queue %s %s", self.queue, stop_token) + self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() @@ -202,6 +199,7 @@ class Transcriber(commands.Cog): transcriber = aai.Transcriber() while True: item = self.queue.get() + logger.info("Got %s", item) if item.type == "STOP": logger.info("Queue ended") break From 422209c3f5d201553c5caa4cfa4cce6e024027aa Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:44:13 +0200 Subject: [PATCH 129/283] fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 79b124c..a53c63c 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -176,7 +176,7 @@ class Transcriber(commands.Cog): elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) operation = CommunicationObject(msg_type="user_cleanup", user=user, data=None) - logger.info("Item added to queue %s %s", self.queue, operation) + logger.info("Item added to queue %s %s", self.comm_queue, operation) self.comm_queue.put(operation) else: logger.info("User VC status changed %s", user.id) From a1cfa1852e2eb52fad3ed8db57c52706fccc71d5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:49:41 +0200 Subject: [PATCH 130/283] Test --- voice_recognition.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index a53c63c..5e1ad23 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -107,7 +107,8 @@ class SRBuffer(voice_recv.AudioSink): self.wavewriter[str(user.id)][0].writeframes(data.pcm) self.wavewriter[str(user.id)][1] = time.time_ns() #time from last write self.wavewriter[str(user.id)][3] = True #data written in last iter - self.wavewriter[str(user.id)][4] = True #data in buffer + self.wavewriter[str(user.id)][4] = True #data in buff + time.sleep(0.001) def cleanup(self) -> None: try: @@ -188,7 +189,7 @@ class Transcriber(commands.Cog): async def stop(self, ctx): self.check_data.stop() stop_token = CommunicationObject("STOP",None,None) - logger.info("Item added to queue %s %s", self.queue, stop_token) + logger.info("Item added to queue %s %s", self.comm_queue, stop_token) self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() From 61a6d1f530403beb5c9793d2b75e5e842812f972 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 16:57:21 +0200 Subject: [PATCH 131/283] SAy --- voice_recognition.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 5e1ad23..a930968 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -144,9 +144,7 @@ class Transcriber(commands.Cog): logger.info(self.bot.voice_clients) self.check_data.start() logger.info("Start worker!~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - #ev_loop = asyncio.get_event_loop() - run_loop = asyncio.get_running_loop() - self.worker = run_loop.create_task(self.transcribe_output_queue()) + self.worker = asyncio.create_task(self.transcribe_output_queue()) vc.listen(self.wsink) From 80c69a8823c35723ccf4fb68bc56cb5fccd088e4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:02:21 +0200 Subject: [PATCH 132/283] ass --- voice_recognition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index a930968..e5b2919 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -28,6 +28,7 @@ class CommunicationObject: class WaveWriter: def __init__(self, user_id, queue): self.queue = queue + logger.info("Wavewriter queue id: %s",id(self.queue)) self.user = user_id self.username = str(user_id.id) logger.info("Creating Wavewriter %s", self.username) @@ -196,6 +197,8 @@ class Transcriber(commands.Cog): logger.info("Transcript worker start") config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") transcriber = aai.Transcriber() + logger.info("Transcriber queue id: %s",id(self.queue)) + while True: item = self.queue.get() logger.info("Got %s", item) From 590ab187d857382e3c69dc00ce3bc2dfcdd23b56 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:04:16 +0200 Subject: [PATCH 133/283] Bugfix --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index e5b2919..af50ea0 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -197,10 +197,10 @@ class Transcriber(commands.Cog): logger.info("Transcript worker start") config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") transcriber = aai.Transcriber() - logger.info("Transcriber queue id: %s",id(self.queue)) + logger.info("Transcriber queue id: %s",id(self.comm_queue)) while True: - item = self.queue.get() + item = self.comm_queue.get() logger.info("Got %s", item) if item.type == "STOP": logger.info("Queue ended") From 024c42a800e8c6424a2be9003a3b8d6b61f00aa9 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:07:37 +0200 Subject: [PATCH 134/283] fx --- voice_recognition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index af50ea0..4d48ca3 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -200,6 +200,7 @@ class Transcriber(commands.Cog): logger.info("Transcriber queue id: %s",id(self.comm_queue)) while True: + logger.info("waiting for tasks") item = self.comm_queue.get() logger.info("Got %s", item) if item.type == "STOP": @@ -213,6 +214,8 @@ class Transcriber(commands.Cog): logger.info("%s : %s", item.user, utterance.text) elif item.type == "user_cleanup": logger.info("User %s disconnected - cleanup action") + else: + logger.warn("Something went wrong with object %s", item) logger.info("End transcript worker") async def setup(bot): From 28c5f36cb09b1c1c433d3b4e5b096ef9651da196 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:12:38 +0200 Subject: [PATCH 135/283] fx --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 4d48ca3..01ba122 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -201,7 +201,7 @@ class Transcriber(commands.Cog): while True: logger.info("waiting for tasks") - item = self.comm_queue.get() + item = await self.comm_queue.get() logger.info("Got %s", item) if item.type == "STOP": logger.info("Queue ended") From 62d60218db290c22689cab98f1c389d298e25867 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:18:27 +0200 Subject: [PATCH 136/283] test --- voice_recognition.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 01ba122..15e9a14 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -50,7 +50,7 @@ class WaveWriter: self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) - def rotate(self): + async def rotate(self): self.transcript_file_present.close() self.file_name_past = self.file_name_present @@ -70,7 +70,7 @@ class WaveWriter: operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_present]) logger.info("Item added to queue %s %s", self.queue, operation) - self.queue.put(operation) + await self.queue.put(operation) def writeframes(self, pcmdata): self.transcript_file_present.writeframes(pcmdata) @@ -161,7 +161,7 @@ class Transcriber(commands.Cog): logger.info("File rotation time since last rotation %s", timediff_rotation/1e9) item[2] = time.time_ns() item[4] = False - item[0].rotate() + await item[0].rotate() item[3] = False @commands.Cog.listener() @@ -177,7 +177,7 @@ class Transcriber(commands.Cog): logger.info("User %s disconnected from channel %s", user, before.channel.name) operation = CommunicationObject(msg_type="user_cleanup", user=user, data=None) logger.info("Item added to queue %s %s", self.comm_queue, operation) - self.comm_queue.put(operation) + await self.comm_queue.put(operation) else: logger.info("User VC status changed %s", user.id) logger.info("Before %s", before) @@ -190,7 +190,7 @@ class Transcriber(commands.Cog): stop_token = CommunicationObject("STOP",None,None) logger.info("Item added to queue %s %s", self.comm_queue, stop_token) - self.comm_queue.put(stop_token) + await self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() async def transcribe_output_queue(self): From 66906c44867b424608c8cf8a14ead6d11bf61d31 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:23:08 +0200 Subject: [PATCH 137/283] fx --- voice_recognition.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index 15e9a14..fd3e77d 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -24,6 +24,8 @@ class CommunicationObject: self.type = msg_type self.user = user self.data = data + def __repr__(self): + return f"{self.type} : {self.user} : {self.data}" class WaveWriter: def __init__(self, user_id, queue): From 94fe4483261a822bde0f1e26e1ce6124f3628fd3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:25:31 +0200 Subject: [PATCH 138/283] fx --- voice_recognition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index fd3e77d..9fc33c3 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -205,16 +205,16 @@ class Transcriber(commands.Cog): logger.info("waiting for tasks") item = await self.comm_queue.get() logger.info("Got %s", item) - if item.type == "STOP": + if "STOP" in item.type: logger.info("Queue ended") break - elif item.type == "send_file": + elif "send_file" in item.type: transcript = "" for iter in item.data: transcript = transcriber.transcribe(iter, config=config) for utterance in transcript.utterances: logger.info("%s : %s", item.user, utterance.text) - elif item.type == "user_cleanup": + elif "user_cleanup" in item.type: logger.info("User %s disconnected - cleanup action") else: logger.warn("Something went wrong with object %s", item) From 1cc7c0b51c131cecc9297d5c4dafccdd9956a364 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:29:50 +0200 Subject: [PATCH 139/283] fx --- voice_recognition.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voice_recognition.py b/voice_recognition.py index 9fc33c3..50f9697 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -209,9 +209,11 @@ class Transcriber(commands.Cog): logger.info("Queue ended") break elif "send_file" in item.type: + logger.info("Sending file for transcription") transcript = "" for iter in item.data: transcript = transcriber.transcribe(iter, config=config) + logger.info("Data sent") for utterance in transcript.utterances: logger.info("%s : %s", item.user, utterance.text) elif "user_cleanup" in item.type: From 77e22e6cb70d2006ffef867ae4808814059046b8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:31:26 +0200 Subject: [PATCH 140/283] fx --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 50f9697..3abdf6b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -208,11 +208,11 @@ class Transcriber(commands.Cog): if "STOP" in item.type: logger.info("Queue ended") break - elif "send_file" in item.type: + elif "send_file" in item.ty logger.info("Sending file for transcription") transcript = "" for iter in item.data: - transcript = transcriber.transcribe(iter, config=config) + transcript = await transcriber.transcribe(iter, config=config) logger.info("Data sent") for utterance in transcript.utterances: logger.info("%s : %s", item.user, utterance.text) From c7e02c48ae93917398f15c6ddbc45eb3fa0da03b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:32:20 +0200 Subject: [PATCH 141/283] Masha wants to code --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 3abdf6b..bf2701d 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -208,7 +208,7 @@ class Transcriber(commands.Cog): if "STOP" in item.type: logger.info("Queue ended") break - elif "send_file" in item.ty + elif "send_file" in item.type: logger.info("Sending file for transcription") transcript = "" for iter in item.data: From 58617dc41045368b57ae52c4d7213e7e9819d869 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:37:17 +0200 Subject: [PATCH 142/283] tesst --- voice_recognition.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index bf2701d..5d67bee 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -197,7 +197,7 @@ class Transcriber(commands.Cog): async def transcribe_output_queue(self): logger.info("Transcript worker start") - config = aai.TranscriptionConfig(speaker_labels=True, language_code="pl") + config = aai.TranscriptionConfig(language_code="pl") transcriber = aai.Transcriber() logger.info("Transcriber queue id: %s",id(self.comm_queue)) @@ -210,12 +210,14 @@ class Transcriber(commands.Cog): break elif "send_file" in item.type: logger.info("Sending file for transcription") - transcript = "" + transcript = None for iter in item.data: - transcript = await transcriber.transcribe(iter, config=config) - logger.info("Data sent") - for utterance in transcript.utterances: - logger.info("%s : %s", item.user, utterance.text) + logger.info("Iter %s",iter) + transcript = transcriber.transcribe(iter, config=config) + logger.info("Data sent") + if transcript.error: + logger.error(transcript.error) + raise AssertionError elif "user_cleanup" in item.type: logger.info("User %s disconnected - cleanup action") else: From 8e6f562fd49997a202b160b5d3bd4c3f78f0b546 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:41:39 +0200 Subject: [PATCH 143/283] test --- voice_recognition.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 5d67bee..61a7315 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -213,7 +213,10 @@ class Transcriber(commands.Cog): transcript = None for iter in item.data: logger.info("Iter %s",iter) - transcript = transcriber.transcribe(iter, config=config) + try: + transcript = transcriber.transcribe(iter, config=config) + except Exception as e: + logger.warn("Exceptiom occured %s", e) logger.info("Data sent") if transcript.error: logger.error(transcript.error) From 71e5462617e10f70044d71185e2f7d7b37a3e0c9 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:44:01 +0200 Subject: [PATCH 144/283] Fur ktosia! --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 61a7315..523ea9c 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -69,7 +69,7 @@ class WaveWriter: self.transcript_file_future.setnchannels(CHANNELS) self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) - operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_present]) + operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_past]) logger.info("Item added to queue %s %s", self.queue, operation) await self.queue.put(operation) From e5d202ad3d2049cf7b89fc23bcba04f20ea3b4ff Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 17:53:48 +0200 Subject: [PATCH 145/283] =?UTF-8?q?Last=20=C5=82an=20for=20todej=20mejbi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voice_recognition.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 523ea9c..926b54b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -129,12 +129,17 @@ class Transcriber(commands.Cog): self.comm_queue = asyncio.Queue() self.wsink = SRBuffer(self.comm_queue) self.worker = None + self.mc = None + self.vc = None @commands.hybrid_command(name="transcribe") async def test(self, ctx): logger.info("Attempt transcribe") - vc = None - + if self.vc: + vc = self.vc #to juz powinien byc voice channel z funkcja conenct + else: + vc = None + self.mc = ctx.message.channel if self.bot.voice_clients: if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient): logger.info("Already transcribing") @@ -218,6 +223,7 @@ class Transcriber(commands.Cog): except Exception as e: logger.warn("Exceptiom occured %s", e) logger.info("Data sent") + await self.mc.send(("%s: %s", item.user, transcript.text)) if transcript.error: logger.error(transcript.error) raise AssertionError From 3398d5df929aa15960160650f98fe037faa90bce Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 18:08:45 +0200 Subject: [PATCH 146/283] test --- voice_recognition.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 926b54b..1e0248b 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -219,11 +219,16 @@ class Transcriber(commands.Cog): for iter in item.data: logger.info("Iter %s",iter) try: - transcript = transcriber.transcribe(iter, config=config) + coro = asyncio.to_thread( + transcriber.transcribe, + iter, + config=config + ) + transcript = await coro except Exception as e: logger.warn("Exceptiom occured %s", e) logger.info("Data sent") - await self.mc.send(("%s: %s", item.user, transcript.text)) + await self.mc.send(f"{item.user} : {transcript.text}") if transcript.error: logger.error(transcript.error) raise AssertionError From ef0be53a969bea295333a6a175b33714cbbb8d3f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 18:19:57 +0200 Subject: [PATCH 147/283] Fixing debug levels --- voice_recognition.py | 48 ++++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 1e0248b..339fef4 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -30,11 +30,9 @@ class CommunicationObject: class WaveWriter: def __init__(self, user_id, queue): self.queue = queue - logger.info("Wavewriter queue id: %s",id(self.queue)) self.user = user_id self.username = str(user_id.id) - logger.info("Creating Wavewriter %s", self.username) - logger.info(user_id) + #here we can add hashing function to make transcription files not possible to be connected with discord id self.present_file_id = 0 self.file_name_past = None self.file_name_present = location + self.username + "_" + str(self.present_file_id) + ".mp3" @@ -70,7 +68,6 @@ class WaveWriter: self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_past]) - logger.info("Item added to queue %s %s", self.queue, operation) await self.queue.put(operation) @@ -134,7 +131,6 @@ class Transcriber(commands.Cog): @commands.hybrid_command(name="transcribe") async def test(self, ctx): - logger.info("Attempt transcribe") if self.vc: vc = self.vc #to juz powinien byc voice channel z funkcja conenct else: @@ -142,16 +138,14 @@ class Transcriber(commands.Cog): self.mc = ctx.message.channel if self.bot.voice_clients: if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient): - logger.info("Already transcribing") + logger.debug("Already transcribing") else: - logger.info("Already connected with other client") - logger.info(self.bot.voice_clients) + logger.debug("Already connected with other client") else: - logger.info("Connected") + logger.debug("Connected") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) logger.info(self.bot.voice_clients) self.check_data.start() - logger.info("Start worker!~!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") self.worker = asyncio.create_task(self.transcribe_output_queue()) vc.listen(self.wsink) @@ -159,13 +153,12 @@ class Transcriber(commands.Cog): @tasks.loop(seconds=0.5) async def check_data(self): for item in self.wsink.wavewriter.values(): - logger.info("Item %s", item) if not item[3]: timediff_rotation = time.time_ns() - item[2] timediff_write = time.time_ns() -item[1] if timediff_rotation > 3000149433 and timediff_write > 500014943 and item[4]: - logger.info("File rotation time since last write %s", timediff_write/1e9) - logger.info("File rotation time since last rotation %s", timediff_rotation/1e9) + logger.debug("File rotation time since last write %s", timediff_write/1e9) + logger.debug("File rotation time since last rotation %s", timediff_rotation/1e9) item[2] = time.time_ns() item[4] = False await item[0].rotate() @@ -174,7 +167,7 @@ class Transcriber(commands.Cog): @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): if user == self.bot.user: - logger.info("Ignoring self") + logger.debug("Ignoring self") return if before.channel is None: @@ -183,41 +176,38 @@ class Transcriber(commands.Cog): elif after.channel is None: logger.info("User %s disconnected from channel %s", user, before.channel.name) operation = CommunicationObject(msg_type="user_cleanup", user=user, data=None) - logger.info("Item added to queue %s %s", self.comm_queue, operation) await self.comm_queue.put(operation) else: - logger.info("User VC status changed %s", user.id) - logger.info("Before %s", before) - logger.info("After %s", after) + logger.debug("User VC status changed %s", user.id) + logger.debug("Before %s", before) + logger.debug("After %s", after) @commands.command(name="stop_transcribe") async def stop(self, ctx): self.check_data.stop() stop_token = CommunicationObject("STOP",None,None) - logger.info("Item added to queue %s %s", self.comm_queue, stop_token) await self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() async def transcribe_output_queue(self): - logger.info("Transcript worker start") + logger.info("Transcript uploader start") config = aai.TranscriptionConfig(language_code="pl") transcriber = aai.Transcriber() - logger.info("Transcriber queue id: %s",id(self.comm_queue)) + logger.debug("Transcriber queue id: %s",id(self.comm_queue)) while True: - logger.info("waiting for tasks") + logger.debug("waiting for tasks") item = await self.comm_queue.get() - logger.info("Got %s", item) + logger.debug("Got %s", item) if "STOP" in item.type: - logger.info("Queue ended") + logger.debug("Queue ended") break elif "send_file" in item.type: - logger.info("Sending file for transcription") + logger.debug("Sending file for transcription") transcript = None for iter in item.data: - logger.info("Iter %s",iter) try: coro = asyncio.to_thread( transcriber.transcribe, @@ -227,7 +217,6 @@ class Transcriber(commands.Cog): transcript = await coro except Exception as e: logger.warn("Exceptiom occured %s", e) - logger.info("Data sent") await self.mc.send(f"{item.user} : {transcript.text}") if transcript.error: logger.error(transcript.error) @@ -236,12 +225,9 @@ class Transcriber(commands.Cog): logger.info("User %s disconnected - cleanup action") else: logger.warn("Something went wrong with object %s", item) - logger.info("End transcript worker") + logger.info("Transcript uploader stoped") async def setup(bot): logger = logging.getLogger("discord") await bot.add_cog(Transcriber(bot)) logger.info("Loading voice transcribed module done") - -# 1. zapisuj kwestie człowieka do pliku w którym będzie można stwierdzić kto co powiedział (callback z basicaudio + write z wavesinka). Zamykaj plik i wysyłaj do transkrypcji w momencie ciszy dłuższej niż 0.5s. Jeśli człowiek nadaje cały czas dawaj sygnał że nie można go transkrybować - i wyłaczaj zapis. -# 2. Transkrypcja to abstrakt - w zależności od tego która metoda jest włączona wysyła do odpowiedniego silnika, silniki offline odpalam na activcomie. Zaczynamy od assemblyai bo najlepiej wspiera polski. Potem zobaczymy co dalej. From aec60b217ccd237e3d219fd3d0be35ca93f5f3cf Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 18:24:40 +0200 Subject: [PATCH 148/283] Clock tests --- voice_recognition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice_recognition.py b/voice_recognition.py index 339fef4..ab1dadf 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -157,8 +157,8 @@ class Transcriber(commands.Cog): timediff_rotation = time.time_ns() - item[2] timediff_write = time.time_ns() -item[1] if timediff_rotation > 3000149433 and timediff_write > 500014943 and item[4]: - logger.debug("File rotation time since last write %s", timediff_write/1e9) - logger.debug("File rotation time since last rotation %s", timediff_rotation/1e9) + logger.info("File rotation time since last write %s %s", timediff_write, timediff_write/1e9) + logger.info("File rotation time since last rotation %s %s",timediff_rotation, timediff_rotation/1e9) item[2] = time.time_ns() item[4] = False await item[0].rotate() From b4d1a7b32189c2f8bed2e960d8612c929f420977 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 20 Sep 2024 18:28:17 +0200 Subject: [PATCH 149/283] CLock --- voice_recognition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index ab1dadf..73bdfb8 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -156,7 +156,7 @@ class Transcriber(commands.Cog): if not item[3]: timediff_rotation = time.time_ns() - item[2] timediff_write = time.time_ns() -item[1] - if timediff_rotation > 3000149433 and timediff_write > 500014943 and item[4]: + if timediff_rotation > 13000149433 and timediff_write > 500014943 and item[4]: logger.info("File rotation time since last write %s %s", timediff_write, timediff_write/1e9) logger.info("File rotation time since last rotation %s %s",timediff_rotation, timediff_rotation/1e9) item[2] = time.time_ns() From ee785bbdb83e102868019f921bd34b8fa363be7e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 23 Sep 2024 21:23:00 +0200 Subject: [PATCH 150/283] moving api key to netrc --- voice_recognition.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/voice_recognition.py b/voice_recognition.py index 73bdfb8..14e7d8a 100644 --- a/voice_recognition.py +++ b/voice_recognition.py @@ -1,13 +1,19 @@ import logging import wave import asyncio +import netrc import assemblyai as aai import discord from discord.ext import commands, voice_recv, tasks from discord.opus import Decoder as OpusDecoder import time + # Replace with your API key -aai.settings.api_key = "aa9962f0088a449a9c4ab2361e96cc08" +NETRC_FILE = "/home/pi/.netrc" +netrc_mod = netrc.netrc(NETRC_FILE) +authTokens = netrc_mod.authenticators("assemblyai") +aai.settings.api_key = authTokens[2] + discord.opus._load_default() CHANNELS = OpusDecoder.CHANNELS SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS From c33cfc27ba730f9a82ffce04abdb0d79e9ff47cc Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 3 Oct 2024 13:47:02 +0200 Subject: [PATCH 151/283] New stuff incoming --- bot_classes.py => administration_commands.py | 0 ai_commandspy | 1 + bot_commands.py => ai_functions.py | 0 bot.py | 7 +- bot_functions.py => bot_music_functions.py | 0 coach.sex.json | 1707 ----------------- install_music_bot.sh | 9 + librarian_commands.py | 0 music_commands.py | 1 + radio_commands.py | 1 + sr_example.py | 229 --- test_data_retrieval.py | 26 - voice_example.py | 47 - ...nition.py => voice_recognition_commands.py | 4 +- 14 files changed, 18 insertions(+), 2014 deletions(-) rename bot_classes.py => administration_commands.py (100%) create mode 100644 ai_commandspy rename bot_commands.py => ai_functions.py (100%) rename bot_functions.py => bot_music_functions.py (100%) delete mode 100644 coach.sex.json create mode 100644 librarian_commands.py create mode 100644 music_commands.py create mode 100644 radio_commands.py delete mode 100644 sr_example.py delete mode 100644 test_data_retrieval.py delete mode 100644 voice_example.py rename voice_recognition.py => voice_recognition_commands.py (100%) diff --git a/bot_classes.py b/administration_commands.py similarity index 100% rename from bot_classes.py rename to administration_commands.py diff --git a/ai_commandspy b/ai_commandspy new file mode 100644 index 0000000..0369fef --- /dev/null +++ b/ai_commandspy @@ -0,0 +1 @@ +#ai command cogs \ No newline at end of file diff --git a/bot_commands.py b/ai_functions.py similarity index 100% rename from bot_commands.py rename to ai_functions.py diff --git a/bot.py b/bot.py index 6924761..aabc1a7 100644 --- a/bot.py +++ b/bot.py @@ -301,13 +301,14 @@ logger.info("Done") async def on_ready(): """Metoda wywoływana przy połączeniu do serwera.""" logger.debug("%s has connected to Discord!", client.user) - await client.load_extension("voice_recognition") - logger.info("Cogs") + #TODO: load vs reload + #try: + await client.load_extension("voice_recognition_commands") logger.info(client.cogs) await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) await client.tree.sync() for com in client.commands: - logger.info(com.qualified_name) + logger.info("Command %s is awejleble", com.qualified_name) check_self.start() check_data_q.start() diff --git a/bot_functions.py b/bot_music_functions.py similarity index 100% rename from bot_functions.py rename to bot_music_functions.py diff --git a/coach.sex.json b/coach.sex.json deleted file mode 100644 index c53bf65..0000000 --- a/coach.sex.json +++ /dev/null @@ -1,1707 +0,0 @@ -{ - "recordsCount": 17, - "records": [ - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": null, - "updatedDateISO8601": null, - "expiresDateISO8601": null, - "createdDateRaw": null, - "updatedDateRaw": null, - "expiresDateRaw": null, - "audit": { - "createdDate": "2024-02-12T08:43:20+00:00", - "updatedDate": "2024-02-12T08:43:20+00:00" - }, - "nameServers": [], - "whoisServer": "whois.nic.sex", - "registrarName": null, - "status": [], - "cleanText": "Domain Name: coach.sex\nURL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n>>> This name is not available for registration.\n>>> This name subscribes to the AdultBlock+ product and is not available for registration.\n>>> Last update of WHOIS database: 2024-02-12T08:43:20Z <<<\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\nThe Service is provided so that you may look up certain information in relation to domain names that we store in our database.\nUse of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.\nThe information provided by this Service is 'as is' and we make no guarantee of it its accuracy.\nYou agree that by your use of the Service you will not use the information provided by us in a way which is:\n* inconsistent with any applicable laws,\n* inconsistent with any policy issued by us,\n* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or\n* to enable high volume, automated, electronic processes that apply to the Service.\nYou acknowledge that:\n* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,\n* we may restrict, suspend or terminate your access to the Service at any time, and\n* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.\nThis information has been prepared and published in order to represent administrative and technical management of the TLD.\nWe may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.\n", - "rawText": "", - "registrantContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "" - }, - "administrativeContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "" - }, - "technicalContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "" - }, - "billingContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "" - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "" - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": null, - "updatedDateISO8601": null, - "expiresDateISO8601": null, - "createdDateRaw": null, - "updatedDateRaw": null, - "expiresDateRaw": null, - "audit": { - "createdDate": "2021-12-08T07:33:50+00:00", - "updatedDate": "2021-12-08T07:33:50+00:00" - }, - "nameServers": [], - "whoisServer": null, - "registrarName": null, - "status": [], - "cleanText": "Domain Name: coach.sex|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> This name is not available for registration.|>>> This name subscribes to the AdultBlock+ service.|>>> Last update of WHOIS database: 2021-12-08T07:20:05Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "rawText": "Domain Name: coach.sex|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> This name is not available for registration.|>>> This name subscribes to the AdultBlock+ service.|>>> Last update of WHOIS database: 2021-12-08T07:20:05Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "registrantContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "administrativeContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "technicalContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "billingContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2021-08-13T19:12:36+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2021-08-13T19:12:36Z", - "expiresDateRaw": "2021-08-01T20:47:08Z", - "audit": { - "createdDate": "2021-09-08T05:57:18+00:00", - "updatedDate": "2021-09-08T05:57:18+00:00" - }, - "nameServers": [ - "ns1.zenbox.pl|ns2.zenbox.pl|" - ], - "whoisServer": "whois.instra.com", - "registrarName": "Instra Corporation Pty Ltd", - "status": [ - "clientHold", - "clientTransferProhibited", - "pendingDelete", - "redemptionPeriod" - ], - "cleanText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-GDREG|Registrar WHOIS Server: whois.instra.com|Registrar URL: whois.instra.com|Updated Date: 2021-08-13T19:12:36Z|Creation Date: 2018-08-01T20:47:08Z|Registry Expiry Date: 2021-08-01T20:47:08Z|Registrar: Instra Corporation Pty Ltd|Registrar IANA ID: 1376|Registrar Abuse Contact Email:|Registrar Abuse Contact Phone:|Domain Status: clientHold https:\/\/icann.org\/epp#clientHold|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Domain Status: pendingDelete https:\/\/icann.org\/epp#pendingDelete|Domain Status: redemptionPeriod https:\/\/icann.org\/epp#redemptionPeriod|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization:|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns1.zenbox.pl|Name Server: ns2.zenbox.pl|DNSSEC: unsigned|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> Last update of WHOIS database: 2021-09-08T05:56:45Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "rawText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-GDREG|Registrar WHOIS Server: whois.instra.com|Registrar URL: whois.instra.com|Updated Date: 2021-08-13T19:12:36Z|Creation Date: 2018-08-01T20:47:08Z|Registry Expiry Date: 2021-08-01T20:47:08Z|Registrar: Instra Corporation Pty Ltd|Registrar IANA ID: 1376|Registrar Abuse Contact Email:|Registrar Abuse Contact Phone:|Domain Status: clientHold https:\/\/icann.org\/epp#clientHold|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Domain Status: pendingDelete https:\/\/icann.org\/epp#pendingDelete|Domain Status: redemptionPeriod https:\/\/icann.org\/epp#redemptionPeriod|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization:|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns1.zenbox.pl|Name Server: ns2.zenbox.pl|DNSSEC: unsigned|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> Last update of WHOIS database: 2021-09-08T05:56:45Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": null, - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": null, - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": null, - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2021-08-13T19:12:36+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2021-08-13T19:12:36Z", - "expiresDateRaw": "2021-08-01T20:47:08Z", - "audit": { - "createdDate": "2021-08-22T15:19:04+00:00", - "updatedDate": "2021-08-22T15:19:04+00:00" - }, - "nameServers": [ - "ns1.zenbox.pl|ns2.zenbox.pl|" - ], - "whoisServer": "whois.instra.com", - "registrarName": "Instra Corporation Pty Ltd", - "status": [ - "clientHold", - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-GDREG|Registrar WHOIS Server: whois.instra.com|Registrar URL: whois.instra.com|Updated Date: 2021-08-13T19:12:36Z|Creation Date: 2018-08-01T20:47:08Z|Registry Expiry Date: 2021-08-01T20:47:08Z|Registrar: Instra Corporation Pty Ltd|Registrar IANA ID: 1376|Registrar Abuse Contact Email:|Registrar Abuse Contact Phone:|Domain Status: clientHold https:\/\/icann.org\/epp#clientHold|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Domain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization:|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns1.zenbox.pl|Name Server: ns2.zenbox.pl|DNSSEC: unsigned|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> Last update of WHOIS database: 2021-08-22T14:58:12Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "rawText": "", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": "", - "telephone": "", - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": "", - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": "", - "telephone": "", - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": "", - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": "", - "telephone": "", - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": "", - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "zoneContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - } - }, - { - "domainName": "coach.sex", - "domainType": "dropped", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2021-08-13T19:12:36+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2021-08-13T19:12:36Z", - "expiresDateRaw": "2021-08-01T20:47:08Z", - "audit": { - "createdDate": "2021-08-14T17:12:55+00:00", - "updatedDate": "2021-08-14T17:12:55+00:00" - }, - "nameServers": [ - "ns1.zenbox.pl", - "ns2.zenbox.pl" - ], - "whoisServer": "whois.instra.com", - "registrarName": "Instra Corporation Pty Ltd", - "status": [ - "clientHold", - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: coach.sex\nRegistrar WHOIS Server: whois.instra.com\nRegistrar URL: whois.instra.com\nUpdated Date: 2021-08-13T19:12:36Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2021-08-01T20:47:08Z\nRegistrar: Instra Corporation Pty Ltd\nRegistrar IANA ID: 1376\nDomain Status: clientHold https:\/\/icann.org\/epp#clientHold\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns1.zenbox.pl\nName Server: ns2.zenbox.pl\n", - "rawText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-GDREG\nRegistrar WHOIS Server: whois.instra.com\nRegistrar URL: whois.instra.com\nUpdated Date: 2021-08-13T19:12:36Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2021-08-01T20:47:08Z\nRegistrar: Instra Corporation Pty Ltd\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nDomain Status: clientHold https:\/\/icann.org\/epp#clientHold\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization:\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Phone Ext: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Fax Ext: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Phone Ext: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Fax Ext: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Phone Ext: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Fax Ext: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns1.zenbox.pl\nName Server: ns2.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n>>> Last update of WHOIS database: 2021-08-14T17:12:44Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nThe Service is provided so that you may look up certain information in relation to domain names that we store in our database.\n\nUse of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.\n\nThe information provided by this Service is 'as is' and we make no guarantee of it its accuracy.\n\nYou agree that by your use of the Service you will not use the information provided by us in a way which is:\n* inconsistent with any applicable laws,\n* inconsistent with any policy issued by us,\n* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or\n* to enable high volume, automated, electronic processes that apply to the Service.\n\nYou acknowledge that:\n* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,\n* we may restrict, suspend or terminate your access to the Service at any time, and\n* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.\n\nThis information has been prepared and published in order to represent administrative and technical management of the TLD.\n\nWe may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2020-09-15T20:48:28+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2020-09-15T20:48:28Z", - "expiresDateRaw": "2021-08-01T20:47:08Z", - "audit": { - "createdDate": "2021-06-17T12:11:23+00:00", - "updatedDate": "2021-06-17T12:11:23+00:00" - }, - "nameServers": [ - "ns2.zenbox.pl|ns1.zenbox.pl|" - ], - "whoisServer": "whois.instra.com", - "registrarName": "Instra Corporation Pty Ltd", - "status": [ - "clientTransferProhibited" - ], - "cleanText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-GDREG|Registrar WHOIS Server: whois.instra.com|Registrar URL: whois.instra.com|Updated Date: 2020-09-15T20:48:28Z|Creation Date: 2018-08-01T20:47:08Z|Registry Expiry Date: 2021-08-01T20:47:08Z|Registrar: Instra Corporation Pty Ltd|Registrar IANA ID: 1376|Registrar Abuse Contact Email:|Registrar Abuse Contact Phone:|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization:|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns2.zenbox.pl|Name Server: ns1.zenbox.pl|DNSSEC: unsigned|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> Last update of WHOIS database: 2021-06-17T11:55:38Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "rawText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-GDREG|Registrar WHOIS Server: whois.instra.com|Registrar URL: whois.instra.com|Updated Date: 2020-09-15T20:48:28Z|Creation Date: 2018-08-01T20:47:08Z|Registry Expiry Date: 2021-08-01T20:47:08Z|Registrar: Instra Corporation Pty Ltd|Registrar IANA ID: 1376|Registrar Abuse Contact Email:|Registrar Abuse Contact Phone:|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization:|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns2.zenbox.pl|Name Server: ns1.zenbox.pl|DNSSEC: unsigned|URL of the ICANN Whois Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/|>>> Last update of WHOIS database: 2021-06-17T11:55:38Z <<<||For more information on Whois status codes, please visit https:\/\/icann.org\/epp||The Service is provided so that you may look up certain information in relation to domain names that we store in our database.||Use of the Service is subject to our policies, in particular you should familiarise yourself with our Acceptable Use Policy and our Privacy Policy.||The information provided by this Service is 'as is' and we make no guarantee of it its accuracy.||You agree that by your use of the Service you will not use the information provided by us in a way which is:|* inconsistent with any applicable laws,|* inconsistent with any policy issued by us,|* to generate, distribute, or facilitate unsolicited mass email, promotions, advertisings or other solicitations, or|* to enable high volume, automated, electronic processes that apply to the Service.||You acknowledge that:|* a response from the Service that a domain name is 'available', does not guarantee that is able to be registered,|* we may restrict, suspend or terminate your access to the Service at any time, and|* the copying, compilation, repackaging, dissemination or other use of the information provided by the Service is not permitted, without our express written consent.||This information has been prepared and published in order to represent administrative and technical management of the TLD.||We may discontinue or amend any part or the whole of these Terms of Service from time to time at our absolute discretion.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": null, - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Phone Ext: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Fax Ext: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": null, - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Phone Ext: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Fax Ext: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": "REDACTED FOR PRIVACY", - "fax": null, - "faxExt": "REDACTED FOR PRIVACY", - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Phone Ext: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Fax Ext: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2020-09-15T20:48:28+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08.447Z", - "updatedDateRaw": "2020-09-15T20:48:28.658Z", - "expiresDateRaw": "2021-08-01T20:47:08.447Z", - "audit": { - "createdDate": "2021-03-29T02:03:03+00:00", - "updatedDate": "2021-03-29T02:03:03+00:00" - }, - "nameServers": [ - "ns1.zenbox.pl|ns2.zenbox.pl|" - ], - "whoisServer": "whois.instra.net", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientTransferProhibited" - ], - "cleanText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-AGRS|Registrar WHOIS Server: whois.instra.net|Registrar URL: www.instra.com|Updated Date: 2020-09-15T20:48:28.658Z|Creation Date: 2018-08-01T20:47:08.447Z|Registry Expiry Date: 2021-08-01T20:47:08.447Z|Registrar: Instra Corporation Pty Ltd.|Registrar IANA ID: 1376|Registrar Abuse Contact Email: support@instra.com|Registrar Abuse Contact Phone: +61.397831800|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization: |Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Billing ID: REDACTED FOR PRIVACY|Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns1.zenbox.pl|Name Server: ns2.zenbox.pl|DNSSEC: unsigned|URL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/||>>> Last update of WHOIS database: 2021-03-29T02:02:10.846Z <<<||For more information on domain status codes, please visit https:\/\/icann.org\/epp||The WHOIS information provided in this page has been redacted|in compliance with ICANN's Temporary Specification for gTLD|Registration Data.||The data in this record is provided by Uniregistry for informational|purposes only, and it does not guarantee its accuracy. Uniregistry is|authoritative for whois information in top-level domains it operates|under contract with the Internet Corporation for Assigned Names and|Numbers. Whois information from other top-level domains is provided by|a third-party under license to Uniregistry.||This service is intended only for query-based access. By using this|service, you agree that you will use any data presented only for lawful|purposes and that, under no circumstances will you use (a) data|acquired for the purpose of allowing, enabling, or otherwise supporting|the transmission by e-mail, telephone, facsimile or other|communications mechanism of mass unsolicited, commercial advertising|or solicitations to entities other than your existing customers; or|(b) this service to enable high volume, automated, electronic processes|that send queries or data to the systems of any Registrar or any|Registry except as reasonably necessary to register domain names or|modify existing domain name registrations.||Uniregistry reserves the right to modify these terms at any time. By|submitting this query, you agree to abide by this policy. All rights|reserved.", - "rawText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-AGRS|Registrar WHOIS Server: whois.instra.net|Registrar URL: www.instra.com|Updated Date: 2020-09-15T20:48:28.658Z|Creation Date: 2018-08-01T20:47:08.447Z|Registry Expiry Date: 2021-08-01T20:47:08.447Z|Registrar: Instra Corporation Pty Ltd.|Registrar IANA ID: 1376|Registrar Abuse Contact Email: support@instra.com|Registrar Abuse Contact Phone: +61.397831800|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization: |Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Billing ID: REDACTED FOR PRIVACY|Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns1.zenbox.pl|Name Server: ns2.zenbox.pl|DNSSEC: unsigned|URL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/||>>> Last update of WHOIS database: 2021-03-29T02:02:10.846Z <<<||For more information on domain status codes, please visit https:\/\/icann.org\/epp||The WHOIS information provided in this page has been redacted|in compliance with ICANN's Temporary Specification for gTLD|Registration Data.||The data in this record is provided by Uniregistry for informational|purposes only, and it does not guarantee its accuracy. Uniregistry is|authoritative for whois information in top-level domains it operates|under contract with the Internet Corporation for Assigned Names and|Numbers. Whois information from other top-level domains is provided by|a third-party under license to Uniregistry.||This service is intended only for query-based access. By using this|service, you agree that you will use any data presented only for lawful|purposes and that, under no circumstances will you use (a) data|acquired for the purpose of allowing, enabling, or otherwise supporting|the transmission by e-mail, telephone, facsimile or other|communications mechanism of mass unsolicited, commercial advertising|or solicitations to entities other than your existing customers; or|(b) this service to enable high volume, automated, electronic processes|that send queries or data to the systems of any Registrar or any|Registry except as reasonably necessary to register domain names or|modify existing domain name registrations.||Uniregistry reserves the right to modify these terms at any time. By|submitting this query, you agree to abide by this policy. All rights|reserved.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2020-09-01T18:03:20+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08.447Z", - "updatedDateRaw": "2020-09-01T18:03:20.855Z", - "expiresDateRaw": "2021-08-01T20:47:08.447Z", - "audit": { - "createdDate": "2020-09-10T13:30:33+00:00", - "updatedDate": "2020-09-10T13:30:33+00:00" - }, - "nameServers": [ - "ns2.zenbox.pl|ns1.zenbox.pl|" - ], - "whoisServer": "whois.instra.net", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-AGRS|Registrar WHOIS Server: whois.instra.net|Registrar URL: www.instra.com|Updated Date: 2020-09-01T18:03:20.855Z|Creation Date: 2018-08-01T20:47:08.447Z|Registry Expiry Date: 2021-08-01T20:47:08.447Z|Registrar: Instra Corporation Pty Ltd.|Registrar IANA ID: 1376|Registrar Abuse Contact Email: support@instra.com|Registrar Abuse Contact Phone: +61.397831800|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Domain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization: |Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Billing ID: REDACTED FOR PRIVACY|Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns2.zenbox.pl|Name Server: ns1.zenbox.pl|DNSSEC: unsigned|URL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/||>>> Last update of WHOIS database: 2020-09-10T13:29:48.077Z <<<||For more information on domain status codes, please visit https:\/\/icann.org\/epp||The WHOIS information provided in this page has been redacted|in compliance with ICANN's Temporary Specification for gTLD|Registration Data.||The data in this record is provided by Uniregistry for informational|purposes only, and it does not guarantee its accuracy. Uniregistry is|authoritative for whois information in top-level domains it operates|under contract with the Internet Corporation for Assigned Names and|Numbers. Whois information from other top-level domains is provided by|a third-party under license to Uniregistry.||This service is intended only for query-based access. By using this|service, you agree that you will use any data presented only for lawful|purposes and that, under no circumstances will you use (a) data|acquired for the purpose of allowing, enabling, or otherwise supporting|the transmission by e-mail, telephone, facsimile or other|communications mechanism of mass unsolicited, commercial advertising|or solicitations to entities other than your existing customers; or|(b) this service to enable high volume, automated, electronic processes|that send queries or data to the systems of any Registrar or any|Registry except as reasonably necessary to register domain names or|modify existing domain name registrations.||Uniregistry reserves the right to modify these terms at any time. By|submitting this query, you agree to abide by this policy. All rights|reserved.", - "rawText": "Domain Name: coach.sex|Registry Domain ID: D425500000050058539-AGRS|Registrar WHOIS Server: whois.instra.net|Registrar URL: www.instra.com|Updated Date: 2020-09-01T18:03:20.855Z|Creation Date: 2018-08-01T20:47:08.447Z|Registry Expiry Date: 2021-08-01T20:47:08.447Z|Registrar: Instra Corporation Pty Ltd.|Registrar IANA ID: 1376|Registrar Abuse Contact Email: support@instra.com|Registrar Abuse Contact Phone: +61.397831800|Domain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited|Domain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod|Registry Registrant ID: REDACTED FOR PRIVACY|Registrant Name: REDACTED FOR PRIVACY|Registrant Organization: |Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Admin ID: REDACTED FOR PRIVACY|Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Tech ID: REDACTED FOR PRIVACY|Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Registry Billing ID: REDACTED FOR PRIVACY|Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.|Name Server: ns2.zenbox.pl|Name Server: ns1.zenbox.pl|DNSSEC: unsigned|URL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/||>>> Last update of WHOIS database: 2020-09-10T13:29:48.077Z <<<||For more information on domain status codes, please visit https:\/\/icann.org\/epp||The WHOIS information provided in this page has been redacted|in compliance with ICANN's Temporary Specification for gTLD|Registration Data.||The data in this record is provided by Uniregistry for informational|purposes only, and it does not guarantee its accuracy. Uniregistry is|authoritative for whois information in top-level domains it operates|under contract with the Internet Corporation for Assigned Names and|Numbers. Whois information from other top-level domains is provided by|a third-party under license to Uniregistry.||This service is intended only for query-based access. By using this|service, you agree that you will use any data presented only for lawful|purposes and that, under no circumstances will you use (a) data|acquired for the purpose of allowing, enabling, or otherwise supporting|the transmission by e-mail, telephone, facsimile or other|communications mechanism of mass unsolicited, commercial advertising|or solicitations to entities other than your existing customers; or|(b) this service to enable high volume, automated, electronic processes|that send queries or data to the systems of any Registrar or any|Registry except as reasonably necessary to register domain names or|modify existing domain name registrations.||Uniregistry reserves the right to modify these terms at any time. By|submitting this query, you agree to abide by this policy. All rights|reserved.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2020-09-01T18:03:20+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08.447Z", - "updatedDateRaw": "2020-09-01T18:03:20.855Z", - "expiresDateRaw": "2021-08-01T20:47:08.447Z", - "audit": { - "createdDate": "2020-09-10T13:30:33+00:00", - "updatedDate": "2020-09-10T13:30:33+00:00" - }, - "nameServers": [ - "ns2.zenbox.pl", - "ns1.zenbox.pl" - ], - "whoisServer": "whois.instra.net", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2020-09-01T18:03:20.855Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2021-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization: \nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Billing ID: REDACTED FOR PRIVACY\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n\n>>> Last update of WHOIS database: 2020-09-10T13:29:48.077Z <<<\n\nFor more information on domain status codes, please visit https:\/\/icann.org\/epp\n\nThe WHOIS information provided in this page has been redacted\nin compliance with ICANN's Temporary Specification for gTLD\nRegistration Data.\n\nThe data in this record is provided by Uniregistry for informational\npurposes only, and it does not guarantee its accuracy. Uniregistry is\nauthoritative for whois information in top-level domains it operates\nunder contract with the Internet Corporation for Assigned Names and\nNumbers. Whois information from other top-level domains is provided by\na third-party under license to Uniregistry.\n\nThis service is intended only for query-based access. By using this\nservice, you agree that you will use any data presented only for lawful\npurposes and that, under no circumstances will you use (a) data\nacquired for the purpose of allowing, enabling, or otherwise supporting\nthe transmission by e-mail, telephone, facsimile or other\ncommunications mechanism of mass unsolicited, commercial advertising\nor solicitations to entities other than your existing customers; or\n(b) this service to enable high volume, automated, electronic processes\nthat send queries or data to the systems of any Registrar or any\nRegistry except as reasonably necessary to register domain names or\nmodify existing domain name registrations.\n\nUniregistry reserves the right to modify these terms at any time. By\nsubmitting this query, you agree to abide by this policy. All rights\nreserved.", - "rawText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2020-09-01T18:03:20.855Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2021-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization: \nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Billing ID: REDACTED FOR PRIVACY\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n\n>>> Last update of WHOIS database: 2020-09-10T13:29:48.077Z <<<\n\nFor more information on domain status codes, please visit https:\/\/icann.org\/epp\n\nThe WHOIS information provided in this page has been redacted\nin compliance with ICANN's Temporary Specification for gTLD\nRegistration Data.\n\nThe data in this record is provided by Uniregistry for informational\npurposes only, and it does not guarantee its accuracy. Uniregistry is\nauthoritative for whois information in top-level domains it operates\nunder contract with the Internet Corporation for Assigned Names and\nNumbers. Whois information from other top-level domains is provided by\na third-party under license to Uniregistry.\n\nThis service is intended only for query-based access. By using this\nservice, you agree that you will use any data presented only for lawful\npurposes and that, under no circumstances will you use (a) data\nacquired for the purpose of allowing, enabling, or otherwise supporting\nthe transmission by e-mail, telephone, facsimile or other\ncommunications mechanism of mass unsolicited, commercial advertising\nor solicitations to entities other than your existing customers; or\n(b) this service to enable high volume, automated, electronic processes\nthat send queries or data to the systems of any Registrar or any\nRegistry except as reasonably necessary to register domain names or\nmodify existing domain name registrations.\n\nUniregistry reserves the right to modify these terms at any time. By\nsubmitting this query, you agree to abide by this policy. All rights\nreserved.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: mazowieckie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2020-09-01T18:03:20+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08.447Z", - "updatedDateRaw": "2020-09-01T18:03:20.855Z", - "expiresDateRaw": "2021-08-01T20:47:08.447Z", - "audit": { - "createdDate": "2020-09-03T16:58:47+00:00", - "updatedDate": "2020-09-03T16:58:47+00:00" - }, - "nameServers": [ - "ns2.zenbox.pl", - "ns1.zenbox.pl" - ], - "whoisServer": "whois.instra.net", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: coach.sex\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2020-09-01T18:03:20.855Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2021-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\n", - "rawText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2020-09-01T18:03:20.855Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2021-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization: \nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Billing ID: REDACTED FOR PRIVACY\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n\n>>> Last update of WHOIS database: 2020-09-03T16:58:43.794Z <<<\n\nFor more information on domain status codes, please visit https:\/\/icann.org\/epp\n\nThe WHOIS information provided in this page has been redacted\nin compliance with ICANN's Temporary Specification for gTLD\nRegistration Data.\n\nThe data in this record is provided by Uniregistry for informational\npurposes only, and it does not guarantee its accuracy. Uniregistry is\nauthoritative for whois information in top-level domains it operates\nunder contract with the Internet Corporation for Assigned Names and\nNumbers. Whois information from other top-level domains is provided by\na third-party under license to Uniregistry.\n\nThis service is intended only for query-based access. By using this\nservice, you agree that you will use any data presented only for lawful\npurposes and that, under no circumstances will you use (a) data\nacquired for the purpose of allowing, enabling, or otherwise supporting\nthe transmission by e-mail, telephone, facsimile or other\ncommunications mechanism of mass unsolicited, commercial advertising\nor solicitations to entities other than your existing customers; or\n(b) this service to enable high volume, automated, electronic processes\nthat send queries or data to the systems of any Registrar or any\nRegistry except as reasonably necessary to register domain names or\nmodify existing domain name registrations.\n\nUniregistry reserves the right to modify these terms at any time. By\nsubmitting this query, you agree to abide by this policy. All rights\nreserved.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Billing Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "dropped", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2020-08-15T19:12:26+00:00", - "expiresDateISO8601": "2021-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08.447Z", - "updatedDateRaw": "2020-08-15T19:12:26.051Z", - "expiresDateRaw": "2021-08-01T20:47:08.447Z", - "audit": { - "createdDate": "2020-08-16T17:52:30+00:00", - "updatedDate": "2020-08-16T17:52:30+00:00" - }, - "nameServers": [ - "ns2.zenbox.pl", - "ns1.zenbox.pl" - ], - "whoisServer": "whois.instra.net", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientHold", - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: coach.sex\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2020-08-15T19:12:26.051Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2021-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientHold https:\/\/icann.org\/epp#clientHold\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\n", - "rawText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2020-08-15T19:12:26.051Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2021-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientHold https:\/\/icann.org\/epp#clientHold\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization: \nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Billing ID: REDACTED FOR PRIVACY\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n\n>>> Last update of WHOIS database: 2020-08-16T17:52:19.372Z <<<\n\nFor more information on domain status codes, please visit https:\/\/icann.org\/epp\n\nThe WHOIS information provided in this page has been redacted\nin compliance with ICANN's Temporary Specification for gTLD\nRegistration Data.\n\nThe data in this record is provided by Uniregistry for informational\npurposes only, and it does not guarantee its accuracy. Uniregistry is\nauthoritative for whois information in top-level domains it operates\nunder contract with the Internet Corporation for Assigned Names and\nNumbers. Whois information from other top-level domains is provided by\na third-party under license to Uniregistry.\n\nThis service is intended only for query-based access. By using this\nservice, you agree that you will use any data presented only for lawful\npurposes and that, under no circumstances will you use (a) data\nacquired for the purpose of allowing, enabling, or otherwise supporting\nthe transmission by e-mail, telephone, facsimile or other\ncommunications mechanism of mass unsolicited, commercial advertising\nor solicitations to entities other than your existing customers; or\n(b) this service to enable high volume, automated, electronic processes\nthat send queries or data to the systems of any Registrar or any\nRegistry except as reasonably necessary to register domain names or\nmodify existing domain name registrations.\n\nUniregistry reserves the right to modify these terms at any time. By\nsubmitting this query, you agree to abide by this policy. All rights\nreserved.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "mazowieckie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY\nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: mazowieckie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": "", - "telephoneExt": null, - "fax": "", - "faxExt": null, - "rawText": "Billing Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2019-10-29T19:13:20+00:00", - "expiresDateISO8601": "2020-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08.447Z", - "updatedDateRaw": "2019-10-29T19:13:20.489Z", - "expiresDateRaw": "2020-08-01T20:47:08.447Z", - "audit": { - "createdDate": "2020-01-13T08:00:00+00:00", - "updatedDate": "2020-01-13T08:00:00+00:00" - }, - "nameServers": [ - "ns2.zenbox.pl", - "ns1.zenbox.pl" - ], - "whoisServer": "whois.instra.net", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientTransferProhibited" - ], - "cleanText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2019-10-29T19:13:20.489Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2020-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization: \nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: podkarpackie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Billing ID: REDACTED FOR PRIVACY\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n\n>>> Last update of WHOIS database: 2020-01-13T01:43:21.865Z <<<\n\nFor more information on domain status codes, please visit https:\/\/icann.org\/epp\n\nThe WHOIS information provided in this page has been redacted\nin compliance with ICANN's Temporary Specification for gTLD\nRegistration Data.\n\nThe data in this record is provided by Uniregistry for informational\npurposes only, and it does not guarantee its accuracy. Uniregistry is\nauthoritative for whois information in top-level domains it operates\nunder contract with the Internet Corporation for Assigned Names and\nNumbers. Whois information from other top-level domains is provided by\na third-party under license to Uniregistry.\n\nThis service is intended only for query-based access. By using this\nservice, you agree that you will use any data presented only for lawful\npurposes and that, under no circumstances will you use (a) data\nacquired for the purpose of allowing, enabling, or otherwise supporting\nthe transmission by e-mail, telephone, facsimile or other\ncommunications mechanism of mass unsolicited, commercial advertising\nor solicitations to entities other than your existing customers; or\n(b) this service to enable high volume, automated, electronic processes\nthat send queries or data to the systems of any Registrar or any\nRegistry except as reasonably necessary to register domain names or\nmodify existing domain name registrations.\n\nUniregistry reserves the right to modify these terms at any time. By\nsubmitting this query, you agree to abide by this policy. All rights\nreserved.", - "rawText": "Domain Name: coach.sex\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server: whois.instra.net\nRegistrar URL: www.instra.com\nUpdated Date: 2019-10-29T19:13:20.489Z\nCreation Date: 2018-08-01T20:47:08.447Z\nRegistry Expiry Date: 2020-08-01T20:47:08.447Z\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: support@instra.com\nRegistrar Abuse Contact Phone: +61.397831800\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nRegistry Registrant ID: REDACTED FOR PRIVACY\nRegistrant Name: REDACTED FOR PRIVACY\nRegistrant Organization: \nRegistrant Street: REDACTED FOR PRIVACY\nRegistrant City: REDACTED FOR PRIVACY\nRegistrant State\/Province: podkarpackie\nRegistrant Postal Code: REDACTED FOR PRIVACY\nRegistrant Country: PL\nRegistrant Phone: REDACTED FOR PRIVACY\nRegistrant Fax: REDACTED FOR PRIVACY\nRegistrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Admin ID: REDACTED FOR PRIVACY\nAdmin Name: REDACTED FOR PRIVACY\nAdmin Organization: REDACTED FOR PRIVACY\nAdmin Street: REDACTED FOR PRIVACY\nAdmin City: REDACTED FOR PRIVACY\nAdmin State\/Province: REDACTED FOR PRIVACY\nAdmin Postal Code: REDACTED FOR PRIVACY\nAdmin Country: REDACTED FOR PRIVACY\nAdmin Phone: REDACTED FOR PRIVACY\nAdmin Fax: REDACTED FOR PRIVACY\nAdmin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Tech ID: REDACTED FOR PRIVACY\nTech Name: REDACTED FOR PRIVACY\nTech Organization: REDACTED FOR PRIVACY\nTech Street: REDACTED FOR PRIVACY\nTech City: REDACTED FOR PRIVACY\nTech State\/Province: REDACTED FOR PRIVACY\nTech Postal Code: REDACTED FOR PRIVACY\nTech Country: REDACTED FOR PRIVACY\nTech Phone: REDACTED FOR PRIVACY\nTech Fax: REDACTED FOR PRIVACY\nTech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nRegistry Billing ID: REDACTED FOR PRIVACY\nBilling Name: REDACTED FOR PRIVACY\nBilling Organization: REDACTED FOR PRIVACY\nBilling Street: REDACTED FOR PRIVACY\nBilling City: REDACTED FOR PRIVACY\nBilling State\/Province: REDACTED FOR PRIVACY\nBilling Postal Code: REDACTED FOR PRIVACY\nBilling Country: REDACTED FOR PRIVACY\nBilling Phone: REDACTED FOR PRIVACY\nBilling Fax: REDACTED FOR PRIVACY\nBilling Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\nName Server: ns2.zenbox.pl\nName Server: ns1.zenbox.pl\nDNSSEC: unsigned\nURL of the ICANN RDDS Inaccuracy Complaint Form: https:\/\/www.icann.org\/wicf\/\n\n>>> Last update of WHOIS database: 2020-01-13T01:43:21.865Z <<<\n\nFor more information on domain status codes, please visit https:\/\/icann.org\/epp\n\nThe WHOIS information provided in this page has been redacted\nin compliance with ICANN's Temporary Specification for gTLD\nRegistration Data.\n\nThe data in this record is provided by Uniregistry for informational\npurposes only, and it does not guarantee its accuracy. Uniregistry is\nauthoritative for whois information in top-level domains it operates\nunder contract with the Internet Corporation for Assigned Names and\nNumbers. Whois information from other top-level domains is provided by\na third-party under license to Uniregistry.\n\nThis service is intended only for query-based access. By using this\nservice, you agree that you will use any data presented only for lawful\npurposes and that, under no circumstances will you use (a) data\nacquired for the purpose of allowing, enabling, or otherwise supporting\nthe transmission by e-mail, telephone, facsimile or other\ncommunications mechanism of mass unsolicited, commercial advertising\nor solicitations to entities other than your existing customers; or\n(b) this service to enable high volume, automated, electronic processes\nthat send queries or data to the systems of any Registrar or any\nRegistry except as reasonably necessary to register domain names or\nmodify existing domain name registrations.\n\nUniregistry reserves the right to modify these terms at any time. By\nsubmitting this query, you agree to abide by this policy. All rights\nreserved.", - "registrantContact": { - "name": "REDACTED FOR PRIVACY", - "organization": null, - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "podkarpackie", - "postalCode": "REDACTED FOR PRIVACY", - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Registrant Name: REDACTED FOR PRIVACY|Registrant Street: REDACTED FOR PRIVACY|Registrant City: REDACTED FOR PRIVACY|Registrant State\/Province: podkarpackie|Registrant Postal Code: REDACTED FOR PRIVACY|Registrant Country: PL|Registrant Phone: REDACTED FOR PRIVACY|Registrant Fax: REDACTED FOR PRIVACY|Registrant Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "administrativeContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Admin Name: REDACTED FOR PRIVACY|Admin Organization: REDACTED FOR PRIVACY|Admin Street: REDACTED FOR PRIVACY|Admin City: REDACTED FOR PRIVACY|Admin State\/Province: REDACTED FOR PRIVACY|Admin Postal Code: REDACTED FOR PRIVACY|Admin Country: REDACTED FOR PRIVACY|Admin Phone: REDACTED FOR PRIVACY|Admin Fax: REDACTED FOR PRIVACY|Admin Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "technicalContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Tech Name: REDACTED FOR PRIVACY|Tech Organization: REDACTED FOR PRIVACY|Tech Street: REDACTED FOR PRIVACY|Tech City: REDACTED FOR PRIVACY|Tech State\/Province: REDACTED FOR PRIVACY|Tech Postal Code: REDACTED FOR PRIVACY|Tech Country: REDACTED FOR PRIVACY|Tech Phone: REDACTED FOR PRIVACY|Tech Fax: REDACTED FOR PRIVACY|Tech Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "billingContact": { - "name": "REDACTED FOR PRIVACY", - "organization": "REDACTED FOR PRIVACY", - "street": "REDACTED FOR PRIVACY", - "city": "REDACTED FOR PRIVACY", - "state": "REDACTED FOR PRIVACY", - "postalCode": "REDACTED FOR PRIVACY", - "country": "REDACTED FOR PRIVACY", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Billing Name: REDACTED FOR PRIVACY|Billing Organization: REDACTED FOR PRIVACY|Billing Street: REDACTED FOR PRIVACY|Billing City: REDACTED FOR PRIVACY|Billing State\/Province: REDACTED FOR PRIVACY|Billing Postal Code: REDACTED FOR PRIVACY|Billing Country: REDACTED FOR PRIVACY|Billing Phone: REDACTED FOR PRIVACY|Billing Fax: REDACTED FOR PRIVACY|Billing Email: Please query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name." - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2019-08-26T16:32:41+00:00", - "expiresDateISO8601": "2020-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2019-08-26T16:32:41Z", - "expiresDateRaw": "2020-08-01T20:47:08Z", - "audit": { - "createdDate": "2019-08-27T16:38:18+00:00", - "updatedDate": "2019-08-27T16:38:18+00:00" - }, - "nameServers": [ - "NS1.ZENBOX.PL", - "NS2.ZENBOX.PL" - ], - "whoisServer": "whois.nic.sex", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2019-08-26T16:32:41Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2020-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: abuse@instra.com\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2019-08-27T16:37:08Z <<<\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\n", - "rawText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2019-08-26T16:32:41Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2020-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: abuse@instra.com\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2019-08-27T16:37:08Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\n\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.", - "registrantContact": { - "name": "Registrant Country: PL", - "organization": "", - "street": "Registrant Country: PL", - "city": "", - "state": "podkarpackie", - "postalCode": "", - "country": "POLAND", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "Registrant Country: PL" - }, - "administrativeContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "technicalContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "billingContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "zoneContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - } - }, - { - "domainName": "coach.sex", - "domainType": "dropped", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2019-08-15T19:13:27+00:00", - "expiresDateISO8601": "2020-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2019-08-15T19:13:27Z", - "expiresDateRaw": "2020-08-01T20:47:08Z", - "audit": { - "createdDate": "2019-08-16T16:52:48+00:00", - "updatedDate": "2019-08-16T16:52:48+00:00" - }, - "nameServers": [ - "NS1.ZENBOX.PL", - "NS2.ZENBOX.PL" - ], - "whoisServer": "whois.nic.sex", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "clientHold", - "clientTransferProhibited", - "autoRenewPeriod" - ], - "cleanText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2019-08-15T19:13:27Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2020-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: abuse@instra.com\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: clientHold https:\/\/icann.org\/epp#clientHold\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2019-08-16T16:51:47Z <<<\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\n", - "rawText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2019-08-15T19:13:27Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2020-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email: abuse@instra.com\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: clientHold https:\/\/icann.org\/epp#clientHold\nDomain Status: clientTransferProhibited https:\/\/icann.org\/epp#clientTransferProhibited\nDomain Status: autoRenewPeriod https:\/\/icann.org\/epp#autoRenewPeriod\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2019-08-16T16:51:47Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\n\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.", - "registrantContact": { - "name": "Registrant Country: PL", - "organization": "", - "street": "Registrant Country: PL", - "city": "", - "state": "podkarpackie", - "postalCode": "", - "country": "POLAND", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "Registrant Country: PL" - }, - "administrativeContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "technicalContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "billingContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "zoneContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2018-09-30T21:15:46+00:00", - "expiresDateISO8601": "2019-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2018-09-30T21:15:46Z", - "expiresDateRaw": "2019-08-01T20:47:08Z", - "audit": { - "createdDate": "2018-10-07T07:00:00+00:00", - "updatedDate": "2018-10-07T07:00:00+00:00" - }, - "nameServers": [ - "NS1.ZENBOX.PL", - "NS2.ZENBOX.PL" - ], - "whoisServer": null, - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "ok" - ], - "cleanText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2018-09-30T21:15:46Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2019-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: ok https:\/\/icann.org\/epp#ok\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2018-10-07T14:46:15Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\n\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.", - "rawText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2018-09-30T21:15:46Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2019-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: ok https:\/\/icann.org\/epp#ok\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2018-10-07T14:46:15Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\n\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.", - "registrantContact": { - "name": "Registrant Country: PL", - "organization": null, - "street": "Registrant Country: PL", - "city": null, - "state": "podkarpackie", - "postalCode": null, - "country": "POLAND", - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": "Registrant Country: PL" - }, - "administrativeContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "technicalContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "billingContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - }, - "zoneContact": { - "name": null, - "organization": null, - "street": "", - "city": null, - "state": null, - "postalCode": null, - "country": null, - "email": null, - "telephone": null, - "telephoneExt": null, - "fax": null, - "faxExt": null, - "rawText": null - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": "2018-09-14T18:17:08+00:00", - "expiresDateISO8601": "2019-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "2018-09-14T18:17:08Z", - "expiresDateRaw": "2019-08-01T20:47:08Z", - "audit": { - "createdDate": "2018-09-15T19:18:37+00:00", - "updatedDate": "2018-09-15T19:18:37+00:00" - }, - "nameServers": [ - "NS1.ZENBOX.PL", - "NS2.ZENBOX.PL" - ], - "whoisServer": "whois.nic.sex", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "serverTransferProhibited" - ], - "cleanText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2018-09-14T18:17:08Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2019-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: serverTransferProhibited https:\/\/icann.org\/epp#serverTransferProhibited\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2018-09-15T19:17:34Z <<<\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\n", - "rawText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date: 2018-09-14T18:17:08Z\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2019-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: serverTransferProhibited https:\/\/icann.org\/epp#serverTransferProhibited\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ZENBOX.PL\nName Server: NS2.ZENBOX.PL\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2018-09-15T19:17:34Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\n\nThe Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.", - "registrantContact": { - "name": "Registrant Country: PL", - "organization": "", - "street": "Registrant Country: PL", - "city": "", - "state": "podkarpackie", - "postalCode": "", - "country": "POLAND", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "Registrant Country: PL" - }, - "administrativeContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "technicalContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "billingContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "zoneContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - } - }, - { - "domainName": "coach.sex", - "domainType": "added", - "createdDateISO8601": "2018-08-01T20:47:08+00:00", - "updatedDateISO8601": null, - "expiresDateISO8601": "2019-08-01T20:47:08+00:00", - "createdDateRaw": "2018-08-01T20:47:08Z", - "updatedDateRaw": "", - "expiresDateRaw": "2019-08-01T20:47:08Z", - "audit": { - "createdDate": "2018-08-02T18:44:19+00:00", - "updatedDate": "2018-08-02T18:44:19+00:00" - }, - "nameServers": [ - "NS1.ONLYDOMAINS.COM", - "NS2.ONLYDOMAINS.COM", - "NS3.ONLYDOMAINS.COM" - ], - "whoisServer": "whois.nic.sex", - "registrarName": "Instra Corporation Pty Ltd.", - "status": [ - "serverTransferProhibited", - "addPeriod" - ], - "cleanText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date:\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2019-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: serverTransferProhibited https:\/\/icann.org\/epp#serverTransferProhibited\nDomain Status: addPeriod https:\/\/icann.org\/epp#addPeriod\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ONLYDOMAINS.COM\nName Server: NS2.ONLYDOMAINS.COM\nName Server: NS3.ONLYDOMAINS.COM\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2018-08-02T18:43:18Z <<<\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\nPlease query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.\n", - "rawText": "Domain Name: COACH.SEX\nRegistry Domain ID: D425500000050058539-AGRS\nRegistrar WHOIS Server:\nRegistrar URL: http:\/\/www.instra.com\nUpdated Date:\nCreation Date: 2018-08-01T20:47:08Z\nRegistry Expiry Date: 2019-08-01T20:47:08Z\nRegistrar Registration Expiration Date:\nRegistrar: Instra Corporation Pty Ltd.\nRegistrar IANA ID: 1376\nRegistrar Abuse Contact Email:\nRegistrar Abuse Contact Phone:\nReseller:\nDomain Status: serverTransferProhibited https:\/\/icann.org\/epp#serverTransferProhibited\nDomain Status: addPeriod https:\/\/icann.org\/epp#addPeriod\nRegistrant Organization:\nRegistrant State\/Province: podkarpackie\nRegistrant Country: PL\nName Server: NS1.ONLYDOMAINS.COM\nName Server: NS2.ONLYDOMAINS.COM\nName Server: NS3.ONLYDOMAINS.COM\nDNSSEC: unsigned\nURL of the ICANN Whois Inaccuracy Complaint Form https:\/\/www.icann.org\/wicf\/)\n>>> Last update of WHOIS database: 2018-08-02T18:43:18Z <<<\n\nFor more information on Whois status codes, please visit https:\/\/icann.org\/epp\n\nAccess to WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the registry database. The data in this record is provided by The Registry Operator for informational purposes only, and accuracy is not guaranteed. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Registry Operator reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.\n\nPlease query the RDDS service of the Registrar of Record identified in this output for information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.", - "registrantContact": { - "name": "Registrant Country: PL", - "organization": "", - "street": "Registrant Country: PL", - "city": "", - "state": "podkarpackie", - "postalCode": "", - "country": "POLAND", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "Registrant Country: PL" - }, - "administrativeContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "technicalContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "billingContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - }, - "zoneContact": { - "name": "", - "organization": "", - "street": "", - "city": "", - "state": "", - "postalCode": "", - "country": "", - "email": "", - "telephone": "", - "telephoneExt": "", - "fax": "", - "faxExt": "", - "rawText": "" - } - } - ] -} \ No newline at end of file diff --git a/install_music_bot.sh b/install_music_bot.sh index 3d1364a..a93163a 100755 --- a/install_music_bot.sh +++ b/install_music_bot.sh @@ -19,6 +19,15 @@ touch /home/pi/Conjurer/request.playlist touch /home/pi/Conjurer/persistence.log echo "[]" > /home/pi/Conjurer/persistence.log +sudo usermod -aG pulse-access pi +sudo usermod -aG pulse-access root + +sudo usermod -aG audio pi +sudo usermod -aG audio root + + + + touch /home/pi/Conjurer/radio_log.log ./env/bin/python3 -m pip install --upgrade pip ./env/bin/python3 -m pip install -r requirements_musician.txt diff --git a/librarian_commands.py b/librarian_commands.py new file mode 100644 index 0000000..e69de29 diff --git a/music_commands.py b/music_commands.py new file mode 100644 index 0000000..0c19ac9 --- /dev/null +++ b/music_commands.py @@ -0,0 +1 @@ +import bot_music_functions \ No newline at end of file diff --git a/radio_commands.py b/radio_commands.py new file mode 100644 index 0000000..0c19ac9 --- /dev/null +++ b/radio_commands.py @@ -0,0 +1 @@ +import bot_music_functions \ No newline at end of file diff --git a/sr_example.py b/sr_example.py deleted file mode 100644 index 3c176cf..0000000 --- a/sr_example.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import annotations - -import logging - -from discord.ext.voice_recv.sinks import AudioSink - -log = logging.getLogger(__name__) - -__all__ = [ - 'SpeechRecognitionSink', -] - -try: - import discord.ext.speech_recognition as sr # type: ignore -except ImportError: - - def SpeechRecognitionSink(**kwargs) -> AudioSink: - """A stub for when the SpeechRecognition module isn't found.""" - raise RuntimeError('The SpeechRecognition module is required to use this sink.') - -else: - import time - import array - import asyncio - import audioop - - from collections import defaultdict - - from discord.ext.voice_recv.sinks import SilencePacket - - from typing import TYPE_CHECKING, TypedDict - - if TYPE_CHECKING: - from concurrent.futures import Future as CFuture - from typing import Literal, Callable, Optional, Any, Final, Protocol, Awaitable, TypeVar - - from discord import Member - - from ..opus import VoiceData - from ..types import MemberOrUser as User - - T = TypeVar('T') - - SRRecognizerMethod = Literal[ - 'sphinx', - 'google', - 'google_cloud', - 'wit', - 'azure', - 'bing', - 'lex', - 'houndify', - 'amazon', - 'assemblyai', - 'ibm', - 'tensorflow', - 'whisper', - 'vosk', - ] - - class SRStopper(Protocol): - def __call__(self, wait: bool = True, /) -> None: - ... - - SRProcessDataCB = Callable[[sr.Recognizer, sr.AudioData, User], Optional[str]] - SRTextCB = Callable[[User, str], Any] - - class _StreamData(TypedDict): - stopper: Optional[SRStopper] - recognizer: sr.Recognizer - buffer: array.array[int] - - class SpeechRecognitionSink(AudioSink): # type: ignore - def __init__( - self, - *, - process_cb: Optional[SRProcessDataCB] = None, - text_cb: Optional[SRTextCB] = None, - default_recognizer: SRRecognizerMethod = 'assemblyai', - phrase_time_limit: int = 10, - ignore_silence_packets: bool = True, - ): - super().__init__(None) - self.process_cb: Optional[SRProcessDataCB] = process_cb - self.text_cb: Optional[SRTextCB] = text_cb - self.phrase_time_limmit: int = phrase_time_limit - self.ignore_silence_packets: bool = ignore_silence_packets - - self.default_recognizer: SRRecognizerMethod = default_recognizer - self._stream_data: defaultdict[int, _StreamData] = defaultdict( - lambda: _StreamData(stopper=None, recognizer=sr.Recognizer(), buffer=array.array('B')) - ) - - def _await(self, coro: Awaitable[T]) -> CFuture[T]: - assert self.client is not None - return asyncio.run_coroutine_threadsafe(coro, self.client.loop) - - def wants_opus(self) -> bool: - return False - - def write(self, user: Optional[User], data: VoiceData) -> None: - if self.ignore_silence_packets and isinstance(data.packet, SilencePacket): - return - - if user is None: - return - - sdata = self._stream_data[user.id] - sdata['buffer'].extend(data.pcm) - - if not sdata['stopper']: - sdata['stopper'] = sdata['recognizer'].listen_in_background( - DiscordSRAudioSource(sdata['buffer']), self.background_listener(user), self.phrase_time_limmit - ) - - def background_listener(self, user: User): - process_cb = self.process_cb or self.get_default_process_callback() - text_cb = self.text_cb or self.get_default_text_callback() - - def callback(_recognizer: sr.Recognizer, _audio: sr.AudioData): - output = process_cb(_recognizer, _audio, user) - if output is not None: - text_cb(user, output) - - return callback - - def get_default_process_callback(self) -> SRProcessDataCB: - def cb(recognizer: sr.Recognizer, audio: sr.AudioData, user: Optional[User]) -> Optional[str]: - log.debug("Got %s, %s, %s", audio, audio.sample_rate, audio.sample_width) - text: Optional[str] = None - try: - # they changed recognize_google to be optionally assigned at runtime... - func = getattr(recognizer, 'recognize_' + self.default_recognizer, recognizer.recognize_google) # type: ignore - text = func(audio) # type: ignore - except sr.UnknownValueError: - log.debug("Bad speech chunk") - # self._debug_audio_chunk(audio) - - return text - - return cb - - def get_default_text_callback(self) -> SRTextCB: - def cb(user: Optional[User], text: Optional[str]) -> Any: - log.info("%s said: %s", user.display_name if user else 'Someone', text) - - return cb - - @AudioSink.listener() - def on_voice_member_disconnect(self, member: Member, ssrc: Optional[int]) -> None: - self._drop(member.id) - - def cleanup(self) -> None: - for user_id in tuple(self._stream_data.keys()): - self._drop(user_id) - - def _drop(self, user_id: int) -> None: - data = self._stream_data.pop(user_id) - - stopper = data.get('stopper') - if stopper: - stopper() - - buffer = data.get('buffer') - if buffer: - # arrays don't have a clear function - del buffer[:] - - def _debug_audio_chunk(self, audio: sr.AudioData, filename: str = 'sound.wav') -> None: - import io, wave, discord - - with io.BytesIO() as b: - with wave.open(b, 'wb') as writer: - writer.setframerate(48000) - writer.setsampwidth(2) - writer.setnchannels(2) - writer.writeframes(audio.get_wav_data()) - - b.seek(0) - f = discord.File(b, filename) - self._await(self.voice_client.channel.send(file=f)) # type: ignore - - class DiscordSRAudioSource(sr.AudioSource): - little_endian: Final[bool] = True - SAMPLE_RATE: Final[int] = 48_000 - SAMPLE_WIDTH: Final[int] = 2 - CHANNELS: Final[int] = 2 - CHUNK: Final[int] = 960 - - def __init__(self, buffer: array.array[int]): - self.buffer = buffer - self._entered: bool = False - - @property - def stream(self): - return self - - def __enter__(self): - if self._entered: - log.warning('Already entered sr audio source') - self._entered = True - return self - - def __exit__(self, *exc) -> None: - self._entered = False - if any(exc): - log.exception('Error closing sr audio source') - - def read(self, size: int) -> bytes: - # TODO: make this timeout configurable - for _ in range(10): - if len(self.buffer) < size * self.CHANNELS: - time.sleep(0.1) - else: - break - else: - if len(self.buffer) == 0: - return b'' - - chunksize = size * self.CHANNELS - audiochunk = self.buffer[:chunksize].tobytes() - del self.buffer[: min(chunksize, len(audiochunk))] - audiochunk = audioop.tomono(audiochunk, 2, 1, 1) - return audiochunk - - def close(self) -> None: - self.buffer.clear() \ No newline at end of file diff --git a/test_data_retrieval.py b/test_data_retrieval.py deleted file mode 100644 index 32dd665..0000000 --- a/test_data_retrieval.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -This Python code snippet is connecting to a MySQL database -using the `mysql.connector` library and fetching data from a table named `scimag` -Here is a breakdown of what the code is doing: -""" - -import netrc - -import mysql.connector - -NETRC_FILE = "/home/mtuszowski/.netrc" -netrc_mod = netrc.netrc(NETRC_FILE) -REMOTE_HOST_NAME = "192.168.1.192" -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) - -mydb = mysql.connector.connect( - host=REMOTE_HOST_NAME, - user=authTokens[0], - password=authTokens[2], - database="scihubdois", -) -mycursor = mydb.cursor() - -mycursor.execute("SELECT * FROM scimag WHERE Title Like 'spider%'") -for x in mycursor: - print(x) diff --git a/voice_example.py b/voice_example.py deleted file mode 100644 index 36cec22..0000000 --- a/voice_example.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- - -import discord -from discord.ext import commands, voice_recv - -discord.opus._load_default() - -bot = commands.Bot(command_prefix=commands.when_mentioned, intents=discord.Intents.all()) - -class Testing(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @commands.command() - async def test(self, ctx): - def callback(user, data: voice_recv.VoiceData): - print(f"Got packet from {user}") - - ## voice power level, how loud the user is speaking - # ext_data = packet.extension_data.get(voice_recv.ExtensionID.audio_power) - # value = int.from_bytes(ext_data, 'big') - # power = 127-(value & 127) - # print('#' * int(power * (79/128))) - ## instead of 79 you can use shutil.get_terminal_size().columns-1 - - vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) - vc.listen(voice_recv.BasicSink(callback)) - - @commands.command() - async def stop(self, ctx): - await ctx.voice_client.disconnect() - - @commands.command() - async def die(self, ctx): - ctx.voice_client.stop() - await ctx.bot.close() - -@bot.event -async def on_ready(): - print('Logged in as {0.id}/{0}'.format(bot.user)) - print('------') - await bot.add_cog(Testing(bot)) - -REMOTE_HOST_NAME = "discord" - - -bot.run("NzUxNDQxODgzNDA2MDA4NDEw.Go--wb.Nr28Oo4eYAP1p2XFVHF5uIQfcPD7s_jO2NfMCQ") diff --git a/voice_recognition.py b/voice_recognition_commands.py similarity index 100% rename from voice_recognition.py rename to voice_recognition_commands.py index 14e7d8a..3626779 100644 --- a/voice_recognition.py +++ b/voice_recognition_commands.py @@ -2,11 +2,11 @@ import logging import wave import asyncio import netrc -import assemblyai as aai +import time import discord +import assemblyai as aai from discord.ext import commands, voice_recv, tasks from discord.opus import Decoder as OpusDecoder -import time # Replace with your API key NETRC_FILE = "/home/pi/.netrc" From 6019b92d15c18e5ff52996a0a17ed773f7b0a09c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sat, 2 Nov 2024 22:30:43 +0100 Subject: [PATCH 152/283] Manual external audition --- conjurer_musician/radio_conjurer.liq | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index 77c4e42..143f3b8 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -76,15 +76,16 @@ mic = compress(threshold=0., ratio=2.,mic) mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) s = add([s, mic]) - # Apply audio processing effects s = nrj(normalize(s)) s = amplify(a, s) - - # Skip blank sections in the stream s = blank.skip(max_blank=10., s) +#Manual audition override +source_control = interactive.bool("Source control", true) +s = switch(track_sensitive=false, [(source_control,mic),({true},s)]) + # Configure logging settings log_to_stdout = true log_to_file = true @@ -102,7 +103,6 @@ emergency = single("/home/pi/RetroPie/mp3/Youtube/Dr. Peacock - Trip to Ireland radio = fallback(id="switcher2", track_sensitive=false, [s, emergency]) # Set up an interactive control for skipping tracks p = interactive.bool("Skip track", false) - def http_skip(~protocol, ~data, ~headers, uri)= radio.skip() http.response(code=200,data="Skipped") From 253a82e694f27f4fe83138ed5154841fa62bbaf3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 3 Nov 2024 14:01:28 +0100 Subject: [PATCH 153/283] Let's get this effin party started --- conjurer_musician/test_mic.liq | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 conjurer_musician/test_mic.liq diff --git a/conjurer_musician/test_mic.liq b/conjurer_musician/test_mic.liq new file mode 100644 index 0000000..54471f4 --- /dev/null +++ b/conjurer_musician/test_mic.liq @@ -0,0 +1,11 @@ +tmic = buffer(input.pulseaudio()) # Microphone +interactive.harbor(port = 9999) + +mic_gain = interactive.float("mic_volume", min=0., max=20., 6.) + +mic = amplify(mic_gain, tmic) +#mic = gate(threshold=-80., range=-120., mic) +#mic = compress(threshold=0., ratio=2.,mic) +#mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) + +output.pulseaudio(mic) From f45dc906ac89e32bf2d4de9afa8423ab144e408f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 18:03:38 +0100 Subject: [PATCH 154/283] test mic --- bot.py | 5 ++++- conjurer_musician/test_mic.liq | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index aabc1a7..302b772 100644 --- a/bot.py +++ b/bot.py @@ -348,8 +348,11 @@ async def on_message(message): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): + if message.author.id == "": + await message.channel.send("Dzien dobry panie i władco (hue hue hue)") channel = client.get_channel(1064888712565100614) - await channel.send("Słyszałem ja żem że: " + message.content) + + #await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel await client.process_commands(message) diff --git a/conjurer_musician/test_mic.liq b/conjurer_musician/test_mic.liq index 54471f4..e005ed8 100644 --- a/conjurer_musician/test_mic.liq +++ b/conjurer_musician/test_mic.liq @@ -1,4 +1,16 @@ tmic = buffer(input.pulseaudio()) # Microphone + +if tmic.is_active() then + log.info("Buffer active") +end + +if tmic.is_ready() then + log.info("Buffer ready") +end +if tmic.is_up() then + log.info("Buffer up") +end + interactive.harbor(port = 9999) mic_gain = interactive.float("mic_volume", min=0., max=20., 6.) From 4448b079e6ab2033444237e90c0b88db2cfbe290 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 19:39:40 +0100 Subject: [PATCH 155/283] tst --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 302b772..fa4e598 100644 --- a/bot.py +++ b/bot.py @@ -348,7 +348,7 @@ async def on_message(message): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): - if message.author.id == "": + if message.author.id == "346956223645614080": await message.channel.send("Dzien dobry panie i władco (hue hue hue)") channel = client.get_channel(1064888712565100614) From b82dfe38daa126d27bb5053427eb613e61724a22 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 19:41:57 +0100 Subject: [PATCH 156/283] Test --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index fa4e598..ef87b2b 100644 --- a/bot.py +++ b/bot.py @@ -348,7 +348,7 @@ async def on_message(message): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): - if message.author.id == "346956223645614080": + if message.author.id == 346956223645614080: await message.channel.send("Dzien dobry panie i władco (hue hue hue)") channel = client.get_channel(1064888712565100614) From 7197309e1dd44a5a67b348a54a86e65cfb05070e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 20:04:54 +0100 Subject: [PATCH 157/283] Test --- bot.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index ef87b2b..32ef337 100644 --- a/bot.py +++ b/bot.py @@ -215,6 +215,13 @@ REMOTE_HOST_NAME = "openai" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) openai.api_key = authTokens[2] openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) +saint_harlot_friend = openAIClient.beta.assistants.create( + name="Bane", + instructions="You are a personal horoscope assistant to Saint Harlot", + ools=[{"type":"file_search"}]) + + +saint_harlot_thread = openAIClient.beta.threads.create() REMOTE_HOST_NAME = "spotipy" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) @@ -349,10 +356,25 @@ async def on_message(message): return if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: - await message.channel.send("Dzien dobry panie i władco (hue hue hue)") - channel = client.get_channel(1064888712565100614) + message = openAIClient.beta.threads.message.create( + saint_harlot_thread.id, + role = "user", + content=message.content + ) + run = openAIClient.beta.threads.runs.create_and_poll( + thread_id = saint_harlot_thread.id, + assistant_id = saint_harlot_friend.id, + instructions = "Odpowiadaj po polsku chyba że zostaniesz przez użytkownika poinstruowany inaczej" + ) + if run.status == 'completed': + messages = openAiClient.beta.threads.messages.list(thread_id = saint_harlot_thread.id) + print(messages) + else: + print(run.status) - #await channel.send("Słyszałem ja żem że: " + message.content) + + channel = client.get_channel(1064888712565100614) + await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel await client.process_commands(message) From 46de87e874cbcf4032c7f8b6ade2ce5eff403a8d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 20:25:06 +0100 Subject: [PATCH 158/283] fix --- bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 32ef337..bd67ff9 100644 --- a/bot.py +++ b/bot.py @@ -218,7 +218,7 @@ openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) saint_harlot_friend = openAIClient.beta.assistants.create( name="Bane", instructions="You are a personal horoscope assistant to Saint Harlot", - ools=[{"type":"file_search"}]) + tools=[{"type":"file_search"}]) saint_harlot_thread = openAIClient.beta.threads.create() @@ -367,7 +367,7 @@ async def on_message(message): instructions = "Odpowiadaj po polsku chyba że zostaniesz przez użytkownika poinstruowany inaczej" ) if run.status == 'completed': - messages = openAiClient.beta.threads.messages.list(thread_id = saint_harlot_thread.id) + messages = openAIClient.beta.threads.messages.list(thread_id = saint_harlot_thread.id) print(messages) else: print(run.status) From 02cd2ba5dfdeb70c1aca8ed9ea72cd554dbb1a60 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 20:28:33 +0100 Subject: [PATCH 159/283] fx --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index bd67ff9..24ed222 100644 --- a/bot.py +++ b/bot.py @@ -218,7 +218,8 @@ openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) saint_harlot_friend = openAIClient.beta.assistants.create( name="Bane", instructions="You are a personal horoscope assistant to Saint Harlot", - tools=[{"type":"file_search"}]) + tools=[{"type":"file_search"}], + model="gpt-4o") saint_harlot_thread = openAIClient.beta.threads.create() From a8e32d6b0526c5ca9d3bd21782147f6581f550ae Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 6 Nov 2024 20:42:27 +0100 Subject: [PATCH 160/283] test --- bot.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/bot.py b/bot.py index 24ed222..92afdb4 100644 --- a/bot.py +++ b/bot.py @@ -215,15 +215,6 @@ REMOTE_HOST_NAME = "openai" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) openai.api_key = authTokens[2] openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) -saint_harlot_friend = openAIClient.beta.assistants.create( - name="Bane", - instructions="You are a personal horoscope assistant to Saint Harlot", - tools=[{"type":"file_search"}], - model="gpt-4o") - - -saint_harlot_thread = openAIClient.beta.threads.create() - REMOTE_HOST_NAME = "spotipy" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) spotify_ctrl = spotipy.Spotify( @@ -357,26 +348,12 @@ async def on_message(message): return if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: - message = openAIClient.beta.threads.message.create( - saint_harlot_thread.id, - role = "user", - content=message.content - ) - run = openAIClient.beta.threads.runs.create_and_poll( - thread_id = saint_harlot_thread.id, - assistant_id = saint_harlot_friend.id, - instructions = "Odpowiadaj po polsku chyba że zostaniesz przez użytkownika poinstruowany inaczej" - ) - if run.status == 'completed': - messages = openAIClient.beta.threads.messages.list(thread_id = saint_harlot_thread.id) - print(messages) + await message.reply("Witam witam") + return else: - print(run.status) - - - channel = client.get_channel(1064888712565100614) - await channel.send("Słyszałem ja żem że: " + message.content) - return + channel = client.get_channel(1064888712565100614) + await channel.send("Słyszałem ja żem że: " + message.content) + return channel = message.channel await client.process_commands(message) From c4e0dea7da971f78314b60cc26f044ba276e6bf5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 09:54:52 +0100 Subject: [PATCH 161/283] Refactoring step one --- ai_commandspy => ai_commands.py | 0 ai_functions.py | 26 + constants.py | 101 + music_commands.py | 156 +- music_functions.py | 247 ++ other_commands.py | 68 + bot_music_functions.py => other_functions.py | 0 radio_commands.py | 2 +- thin_client.py | 2184 ++++++++++++++++++ 9 files changed, 2782 insertions(+), 2 deletions(-) rename ai_commandspy => ai_commands.py (100%) create mode 100644 constants.py create mode 100644 music_functions.py create mode 100644 other_commands.py rename bot_music_functions.py => other_functions.py (100%) create mode 100644 thin_client.py diff --git a/ai_commandspy b/ai_commands.py similarity index 100% rename from ai_commandspy rename to ai_commands.py diff --git a/ai_functions.py b/ai_functions.py index e69de29..b281726 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -0,0 +1,26 @@ + +import tiktoken + + +def num_tokens_from_string(message, model): + """ + The function takes a string message and a model as input and returns the number of tokens in the + message according to the given model. + + :param message: A string containing the message or text from which you want to count the number of + tokens + :param model: The model parameter refers to a language model or tokenizer that can be used to + tokenize the input string. It could be a pre-trained model or a custom tokenizer + """ + tokens_per_message = 3 + tokens_per_name = 1 + chat_gpt_encoding = tiktoken.encoding_for_model(model) + + num_tokens = 0 + num_tokens += tokens_per_message + for keys, values in message.items(): + num_tokens += len(chat_gpt_encoding.encode(values)) + if keys == "role": + num_tokens += tokens_per_name + num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> + return num_tokens diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..395bb9d --- /dev/null +++ b/constants.py @@ -0,0 +1,101 @@ +import datetime +import json +from sys import platform +from typing import List, Optional, TypedDict +from platform import uname + +Music_Config = TypedDict( + "Music_Config", + { + "ctx": Optional[str], + "queue": List[str], + "requester": List[str], + }, +) + + + +# *=========================================== Predefines +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 = "" +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" + +GET_MP3 = "/mp3" +SEND_MP3 = "/update_mp3" +GET_PLAYLIST = "/get_music" +ADD_TO_PRIO_PLAYLIST = "/add_to_priority" +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" +TIME_BETWEEN_CALLS = 100000 +LAST_SPONTANEOUS_CALL = datetime.now() +HOST_ADDRESS = "192.168.1.191" +PORT_ADDRESS = 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" + + 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/RetroPie/mp3/" + SETTINGS_FILE = "/home/pi/Conjurer/settings.json" + NETRC_FILE = "/home/pi/.netrc" + LOGSTORE = "/home/pi/RetroPie/logs/" + ACCIDENT_LOG = "/home/pi/Conjurer/accident_log.json" + ENCODING = "utf-8" + GRAPHICS_PATH = "/home/pi/RetroPie/Conjurer_graphics/" + DIR_PATH_SADOX = "/home/pi/RetroPie/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) diff --git a/music_commands.py b/music_commands.py index 0c19ac9..e3438a5 100644 --- a/music_commands.py +++ b/music_commands.py @@ -1 +1,155 @@ -import bot_music_functions \ No newline at end of file +import music_functions +from constants import MUZYKA +import logging +import discord +from discord.ext import commands +from communication_subroutine import PREPPED_TRACKS +from sys import platform +import re + +class MusicModule(commands.Cog): + def __init__(self, bot, logger_name): + self.bot = bot + self.logger = logging.getLogger(logger_name) + + @commands.hybrid_command( + name="krecimy_pornola", + description="Wiadomo co, dla kogo i po co", + ) + async def krecimy_pornola(self, ctx): + """ + Download a music number from youtube. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. It allows the command to interact + with the Discord API and + """ + await ctx.send("Dej mnie chwilkę") + allowed = False + for role in ctx.author.roles: + if role.name == "Bartender": + allowed = True + if allowed: + self.logger.info("Pornol") + async with ctx.typing(): + # wyciagnij linka z kontekstu + content = ctx.message.content.split() + for item_yt in content: + if re.match("http.*", item_yt): + sciezka, files = await music_functions.get_file(ctx, "Pornol", item_yt) + if files: + self.logger.info("Pornol udany") + await ctx.send(f"Jest w tajnym archiwum pod adresem{sciezka})") + else: + await ctx.reply("Jak ładnie szefa poprosisz.") + + @commands.hybrid_command( + name="co_na_plejliscie_wariacie", + description="Wyswietl kawalki ktore sa na pocztku list radia", + guild=discord.Object(id=664789470779932693), + ) + async def co_na_plejliscie_wariacie(self,ctx): + """ + Download a music number from youtube. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. It allows the command to interact + with the Discord API and + """ + await ctx.send("Dej mnie chwilkę") + async with ctx.typing(): + self.logger.info("plejlista") + self.logger.info(PREPPED_TRACKS) + await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} (wg metadanych: {PREPPED_TRACKS['meta']}) \n- następny będzie{(PREPPED_TRACKS['next']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") + + @commands.hybrid_command( + name="dej_co_z_jutuba", + description="Podaj link do youtube - sciagnie i doda muzyke", + guild=discord.Object(id=664789470779932693), + ) + async def dej_co_z_jutuba(self,ctx): + """ + Download a music number from youtube. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. It allows the command to interact + with the Discord API and + """ + await ctx.send("Dej mnie chwilkę") + self.logger.info("Jutub") + async with ctx.typing(): + # wyciagnij linka z kontekstu + content = ctx.message.content.split() + for item_yt in content: + if re.match("http.*", item_yt): + _, files = await music_functions.get_file(ctx, "Youtube", item_yt) + if files: + for file_iter in files: + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, file_iter) + music_functions.MUSIC_FILE_LIST.update_file_list(file_iter) + MUZYKA["requester"].insert(0, ctx.author) + await ctx.send(f"Dodałem do listy {file_iter}") + self.logger.info("Jutub udany") + + @commands.hybrid_command( + name="dej_co_ze_spotifaja", + description="Podaj link do spotify - sciagnie i doda muzyke", + guild=discord.Object(id=664789470779932693), + ) + async def dej_co_ze_spotifaja(self, ctx): + """ + Get a playlist or a music number from Spotify. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + self.logger.info("Spotifaj") + await ctx.send("Dej mnie chwilkę") + async with ctx.typing(): + # wyciagnij linka z kontekstu + content = ctx.message.content.split() + for item in content: + if re.match("http.*", item): + dir_path, files = await music_functions.get_file(ctx, "Spotify", item) + if platform == "win32": + separator = "\\" + else: + separator = "/" + for file in files: + file_path = ( + dir_path + + separator + + file["artist"] + + " - " + + file["name"] + + ".mp3" + ) + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert( + 0, + dir_path + + separator + + file["artist"] + + " - " + + file["name"] + + ".mp3", + ) + MUZYKA["requester"].insert(0, ctx.author) + music_functions.MUSIC_FILE_LIST.update_file_list(file_path) + await ctx.send( + f"Dodałem do listy {dir_path}{file['artist']} - + {file['name']}.mp3" + ) + self.logger.info("Spotifaj udany") + +async def setup(bot): + logger = logging.getLogger("discord") + music_functions.MUSIC_FILE_LIST.refresh_file_list() + logger.info("Playlist generation") + await bot.add_cog(MusicModule(bot, "discord")) + logger.info("Loading music commands module done") diff --git a/music_functions.py b/music_functions.py new file mode 100644 index 0000000..223fd91 --- /dev/null +++ b/music_functions.py @@ -0,0 +1,247 @@ +import logging +import asyncio +from pathlib import Path, PurePath +from sys import platform +from spotify_dl import spotify +from spotify_dl import youtube as youtube_download +import re +import netrc +import spotipy +from spotipy.oauth2 import SpotifyClientCredentials +from constants import NETRC_FILE, MUSIC_FOLDER, FILE_SERVICE_ADDRESS, GET_MP3, SEND_MP3 +import yt_dlp +import requests +netrc_mod = netrc.netrc(NETRC_FILE) +REMOTE_HOST_NAME = "spotipy" +authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) + +logger = logging.getLogger("discord") +spotify_ctrl = spotipy.Spotify( + client_credentials_manager=SpotifyClientCredentials( + client_id=authTokens[0], + client_secret=authTokens[2], + ) +) +class MusicFileList(object): + """ + The `MusicFileList` class manages a list of music files, with the ability to refresh the list from a + file service or local directory and update the list with new items. + """ + + def __init__(self, uplogger) -> None: + self.music_file_list = [] + self.file_service_active = False + self.logger = uplogger + self.logger.info("Created Playlist organizer class") + + def refresh_file_list(self): + """ + The `refresh_file_list` function attempts to connect to a file service to retrieve a list of music + files, and if unsuccessful, it populates the list by scanning a local directory for .mp3 files. + """ + try: + self.logger.info("Attempt to connect to file service") + response = requests.get(f"{FILE_SERVICE_ADDRESS}{GET_MP3}", timeout=360) + 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"): + temp_music_file = mp3_item.as_posix() + if platform == "win32": + temp_music_file = temp_music_file.replace("/", "\\") + self.music_file_list.append(temp_music_file) + self.file_service_active = False + self.logger.error(e.strerror) + self.logger.error("Service Unavailable") + + def get_file_list(self): + """ + The `get_file_list` function returns the list of music files stored in the object. + :return: The `music_file_list` attribute is being returned. + """ + return self.music_file_list + + def update_file_list(self, item): + """ + The `update_file_list` function appends an item to a music file list and sends a POST request with + the item data to a file service address. + + :param item: The `item` parameter in the `update_file_list` method is the file that you want to add + to the `music_file_list` attribute of the class instance. It is then sent as a JSON payload in a + POST request to a file service address along with the endpoint `SEND_MP3` + """ + self.music_file_list.append(item) + post_data = {"item": str(item)} + requests.post(f"{FILE_SERVICE_ADDRESS}{SEND_MP3}", json=post_data, timeout=360) + +MUSIC_FILE_LIST = MusicFileList(logger) + + +async def get_file(ctx, source, link): + """ + Take in three parameters: "ctx" (context), + "source" (a string representing the source of the file), and "link" (a string representing the link + to the file). Execute it in asynchronouse way. And get file from spotify or youtube. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + represents the context in which a command is being executed, including information such as the + message, the channel, the server, and the user who invoked the command + :param source: The source parameter is likely a string that represents the source of the file that + the user wants to retrieve. This could be a website, a cloud storage service, or any other location + where the file is stored + :param link: The "link" parameter is likely a string that represents a URL or file path to the + location of the file that the function is trying to retrieve + """ + item_id = None + item_type = None + file_list = [] + if source == "Spotify": + dir_path = None + item_type, item_id = spotify.parse_spotify_url(link) + if item_type in ["album", "track", "playlist"] and item_id: + logger.info(item_type) + logger.info(item_id) + logger.info("Spotify link provided") + file_list = await asyncio.to_thread( + spotify.fetch_tracks, spotify_ctrl, item_type, link + ) + logger.info(file_list) + directory_name = await asyncio.to_thread( + spotify.get_item_name, spotify_ctrl, item_type, item_id + ) + logger.info(directory_name) + logger.info("Spotify scrape done") + url_data = {"urls": []} + url_dict = {} + url_dict["save_path"] = Path( + PurePath.joinpath(Path(MUSIC_FOLDER), Path(directory_name)) + ) + url_dict["save_path"].mkdir(parents=True, exist_ok=True) + url_dict["songs"] = file_list + url_data["urls"].append(url_dict.copy()) + file_name_f = youtube_download.default_filename + logger.info("YT-DL") + coro = asyncio.to_thread( + youtube_download.download_songs, + songs=url_data, + output_dir=MUSIC_FOLDER, + format_str="bestaudio/best", + skip_mp3=False, + keep_playlist_order=False, + no_overwrites=True, + remove_trailing_tracks="no", + use_sponsorblock="yes", + file_name_f=file_name_f, + multi_core=0, + proxy="", + ) + _ = await coro + logger.info("YT-DL done") + dir_path = (url_dict["save_path"].resolve()).as_posix() + if platform == "win32": + dir_path = dir_path.replace("/", "\\") + return dir_path, file_list + + await ctx.send( + "No se jaja chyba robisz, Ty byś spotifaja nie poznawszy jakby z krzaków wyskoczył i w dupę kopnął. Plejlista, kawałek albo album prosze." + ) + logger.error("Wrong link provided spotify: %s", link) + elif source == "Youtube": + link_ok = False + pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" + if re.match(pat, link) and re.match(".*youtube.*", link): + link_ok = True + + if link_ok: + dir_path = "" + file_list = [] + query = link + sponsorblock_postprocessor = [ + { + "key": "SponsorBlock", + "categories": ["skip_non_music_sections"], + }, + { + "key": "ModifyChapters", + "remove_sponsor_segments": ["music_offtopic"], + "force_keyframes": True, + }, + ] + # TODO: Make sponsorblock work + sponsorblock_postprocessor = [] + if platform == "win32": + dir_path = "G:\\Muzyka\\Youtube" + else: + dir_path = "/home/pi/RetroPie/mp3/Youtube" + ydl_opts = { + "proxy": "", + "default_search": "ytsearch", + "format": "bestaudio/best", + "postprocessors": sponsorblock_postprocessor, + "noplaylist": True, + "no_color": False, + "paths": {"home": dir_path}, + } + mp3_postprocess_opts = { + "key": "FFmpegExtractAudio", + "preferredcodec": "mp3", + "preferredquality": "192", + } + ydl_opts["postprocessors"].append(mp3_postprocess_opts.copy()) + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + try: + coro = asyncio.to_thread(ydl.download, [query]) + _ = await coro + except Exception as exce: # pylint: disable=broad-exception-caught + logger.error(exce) + logger.info( + "Failed to download %s, make sure yt_dlp is up to date", link + ) + extract = re.search("v=(...........)[&,$]*", link) + ident = extract.group(1) + for item in Path.glob(Path(dir_path), "**/*.mp3"): + file = item.as_posix() + if platform == "win32": + file = file.replace("/", "\\") + if re.match(".*" + ident + ".*", file): + file_list.append(file) + return dir_path, file_list + await ctx.send( + "No se jaja chyba robisz, Ty byś jutuba nie poznawszy jakby z krzaków wyskoczył i w dupę kopnął. Jutuba to nie tuba którą sobie można gdzieś wsadzić." + ) + logger.error("Wrong link provided youtube: %s", link) + return "", None + elif source == "Pornol": + + dir_path = "" + file_list = [] + query = link + sponsorblock_postprocessor = [ + { + "key": "SponsorBlock", + "categories": ["skip_non_music_sections"], + }, + { + "key": "ModifyChapters", + "remove_sponsor_segments": ["music_offtopic"], + "force_keyframes": True, + }, + ] + # TODO: Make sponsorblock work + sponsorblock_postprocessor = [] + dir_path = "/home/pi/RetroPie/movies/porn" + ydl_opts = { + "postprocessors": sponsorblock_postprocessor, + "paths": {"home": dir_path}, + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + try: + coro = asyncio.to_thread(ydl.download, [query]) + _ = await coro + except Exception as exce: # pylint: disable=broad-exception-caught + logger.error(exce) + logger.info( + "Failed to download %s, make sure yt_dlp is up to date", link + ) + file_list.append("wiadomo co wiadomo gdzie") + return dir_path, file_list diff --git a/other_commands.py b/other_commands.py new file mode 100644 index 0000000..ef6f6cd --- /dev/null +++ b/other_commands.py @@ -0,0 +1,68 @@ +from discord.ext import commands +import logging +import discord +from typing import Optional +from constants import DATA + +historia_fabryczki = DATA["fabryczka"] + +class OtherModule(commands.Cog): + def __init__(self, bot, logger_name): + self.bot = bot + self.logger = logging.getLogger(logger_name) + + @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*" + ) + + + @commands.hybrid_command(name="fabryczka", description="Historia fabryczki") + async def fabryczka(self,ctx): + """ + Send a general description of "fabryczkagate" to channel. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. The context object provides a way to + interact with the Discord + """ + await ctx.send(historia_fabryczki) + + + +async def setup(bot): + logger = logging.getLogger("discord") + logger.info("Playlist generation") + await bot.add_cog(OtherModule(bot, "discord")) + logger.info("Loading other module done") diff --git a/bot_music_functions.py b/other_functions.py similarity index 100% rename from bot_music_functions.py rename to other_functions.py diff --git a/radio_commands.py b/radio_commands.py index 0c19ac9..503c766 100644 --- a/radio_commands.py +++ b/radio_commands.py @@ -1 +1 @@ -import bot_music_functions \ No newline at end of file +import music_functions \ No newline at end of file diff --git a/thin_client.py b/thin_client.py new file mode 100644 index 0000000..7d141de --- /dev/null +++ b/thin_client.py @@ -0,0 +1,2184 @@ +# This Python file uses the following encoding: utf-8 +# 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 asyncio +import io +import json +import logging +import netrc + +# *=========================================== Standard Library Imports +import os +import random +import re +import shutil +import subprocess +import sys +import threading +import uuid +from datetime import datetime +from logging import handlers +from pathlib import Path +from platform import uname +from queue import Empty +from sys import platform +from typing import List, Optional, TypedDict +from constants import DATA +# *==============Imported libraries +import discord +import eyed3 +import numpy as np +import openai +import pdf2image +import PyPDF2 +import requests +import spotipy +from discord.ext import commands, tasks +from spotipy.oauth2 import SpotifyClientCredentials + +from communication_subroutine import ( + IN_COMM_Q, + OUT_COMM_Q, + QueryControl, + comm_subroutine, +) +from ai_functions import num_tokens_from_string +# *=========================== Classes definition + +# The above code is defining a TypedDict called `Music_Config` in Python. This TypedDict has three +# keys: "ctx" which is an optional string, "queue" which is a list of strings, and "requester" which +# is a list of strings. This TypedDict is used to define the structure of a dictionary that should +# have these specific keys and value types. +Music_Config = TypedDict( + "Music_Config", + { + "ctx": Optional[str], + "queue": List[str], + "requester": List[str], + }, +) + + +class MusicFileList(object): + """ + The `MusicFileList` class manages a list of music files, with the ability to refresh the list from a + file service or local directory and update the list with new items. + """ + + def __init__(self, uplogger) -> None: + self.music_file_list = [] + self.file_service_active = False + self.logger = uplogger + self.logger.info("Created Playlist organizer class") + + def refresh_file_list(self): + """ + The `refresh_file_list` function attempts to connect to a file service to retrieve a list of music + files, and if unsuccessful, it populates the list by scanning a local directory for .mp3 files. + """ + try: + self.logger.info("Attempt to connect to file service") + response = requests.get(f"{FILE_SERVICE_ADDRESS}{GET_MP3}", timeout=360) + 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"): + temp_music_file = mp3_item.as_posix() + if platform == "win32": + temp_music_file = temp_music_file.replace("/", "\\") + self.music_file_list.append(temp_music_file) + self.file_service_active = False + self.logger.error(e.strerror) + self.logger.error("Service Unavailable") + + def get_file_list(self): + """ + The `get_file_list` function returns the list of music files stored in the object. + :return: The `music_file_list` attribute is being returned. + """ + return self.music_file_list + + def update_file_list(self, item): + """ + The `update_file_list` function appends an item to a music file list and sends a POST request with + the item data to a file service address. + + :param item: The `item` parameter in the `update_file_list` method is the file that you want to add + to the `music_file_list` attribute of the class instance. It is then sent as a JSON payload in a + POST request to a file service address along with the endpoint `SEND_MP3` + """ + self.music_file_list.append(item) + post_data = {"item": str(item)} + requests.post(f"{FILE_SERVICE_ADDRESS}{SEND_MP3}", json=post_data, timeout=360) + + +# *=========================================== Predefines +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 = "" +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" + +GET_MP3 = "/mp3" +SEND_MP3 = "/update_mp3" +GET_PLAYLIST = "/get_music" +ADD_TO_PRIO_PLAYLIST = "/add_to_priority" +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" +TIME_BETWEEN_CALLS = 100000 +LAST_SPONTANEOUS_CALL = datetime.now() +HOST_ADDRESS = "192.168.1.191" +PORT_ADDRESS = 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" + + 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/RetroPie/mp3/" + SETTINGS_FILE = "/home/pi/Conjurer/settings.json" + NETRC_FILE = "/home/pi/.netrc" + LOGSTORE = "/home/pi/RetroPie/logs/" + ACCIDENT_LOG = "/home/pi/Conjurer/accident_log.json" + ENCODING = "utf-8" + GRAPHICS_PATH = "/home/pi/RetroPie/Conjurer_graphics/" + DIR_PATH_SADOX = "/home/pi/RetroPie/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 = "\\" + + +# *=========================================== Keys +netrc_mod = netrc.netrc(NETRC_FILE) +REMOTE_HOST_NAME = "discord" +authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) +TOKEN = authTokens[2] +openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) +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], + ) +) + +REMOTE_HOST_NAME = "openai" +authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) +openai.api_key = authTokens[2] + +# *=========================================== Initializations +intents = discord.Intents.default() +intents.message_content = True +intents.typing = True +intents.presences = True +intents.members = True +intents.messages = True +intents.voice_states = True +logger = logging.getLogger("discord") +logger.setLevel(logging.DEBUG) +handler = handlers.RotatingFileHandler( + filename=LOGFILE, + encoding=ENCODING, + mode="a", + maxBytes=6 * 1024 * 1024, + backupCount=6, +) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) +random.seed() +logger.debug(platform) +logger.info("Playlist generation") + +# *=========================================== Load Data files + +music_file_list = MusicFileList(logger) +music_file_list.refresh_file_list() +logger.info("Playlist generation done") + +logger.info("Loading GPT settings") +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) +logger.info("Done") + +logger.info("Loading memory file") +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) +logger.info("Done") + +logger.info("Loading music memory 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) +logger.info("Done") + +logger.info("Loading settings file") + +logger.info("Loading reactions") +word_reactions = DATA["word_reactions"] +cyclic_words = DATA["cyclic_words"] +for key in word_reactions: + word_reactions[key][2] = datetime.now() +logger.info("Done") + + +# *=========================================== Create Client +logger.info("Creating discord bot") +client = commands.Bot(intents=intents, command_prefix="$") +logger.info("Done") +logger.info("Creating flask app") +logger.info("Done") + + +# *=========================================== Define Events +@client.event +async def on_ready(): + """Metoda wywoływana przy połączeniu do serwera.""" + logger.debug("%s has connected to Discord!", client.user) + #TODO: load vs reload + #try: + await client.load_extension("voice_recognition_commands") + await client.load_extension("music_commands") + await client.load_extension("other_commands") + logger.info(client.cogs) + await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) + await client.tree.sync() + for com in client.commands: + logger.info("Command %s is awejleble", com.qualified_name) + check_self.start() + check_data_q.start() + + +@client.event +async def on_message(message): + """ + Handle incoming messages in a Discord server, perform various + checks and actions based on the content and context of the message, and respond accordingly. + + :param message: The message object that is received when a user sends a message in a Discord server + or DM. The code is checking various conditions and performing actions based on the content of the + message and the context in which it was sent. It also includes TODOs for future improvements + :return: The function `on_message` is being returned. + """ + vykidailo = False + channel = None + if message.author == client.user: + return + if isinstance(message.author, discord.Member): + for role in message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + + if ("Conjurer Śpij Słodko Aniołku" in message.content) and vykidailo: + sys.exit() + # kanal bez bota + if message.channel.id == 1095985579147141202: + return + # wentylacja + if message.channel.id == 1083804024173764739: + return + # legendy + if message.channel.id == 1084448332841230388: + return + # interrogation booth + if message.channel.id == 1111625221171052595: + return + if isinstance(message.channel, discord.DMChannel): + if message.author.id == 346956223645614080: + await message.reply("Witam witam") + return + else: + channel = client.get_channel(1064888712565100614) + await channel.send("Słyszałem ja żem że: " + message.content) + return + channel = message.channel + await client.process_commands(message) + + message.content = message.content.lower() + + tdelta = datetime.now() - MASTER_TIMEOUT + tdelta = tdelta.total_seconds() + if "opowiedz o fabryczce" in message.content: + await message.reply(DATA["fabryczka"]) + if "opowiedz mi o fabryczce" in message.content: + await message.reply(DATA["fabryczka"]) + + if tdelta > INITIAL_TIME_WAIT: + for word in word_reactions: + if re.search(r"\b" + word + r"\b", message.content): + tdelta = datetime.now() - word_reactions[word][2] + tdelta = tdelta.total_seconds() + security_clearance = word_reactions[word][4] + reaction = word_reactions[word][3] + if tdelta > word_reactions[word][1]: + # TODO: to zrobic reactiony + logger.info("Ping z procedury reakcji") + if reaction: + emoji = client.get_emoji(word_reactions[word][0]) + await message.add_reaction(emoji) + elif security_clearance and vykidailo: + await message.reply(word_reactions[word][0]) + elif not security_clearance: + await message.reply(word_reactions[word][0]) + word_reactions[word][2] = datetime.now() + + # TODO: drobne literówki, mentiony, spacja przed dwukropkiem. napraw. + kondziu_mentioned = False + for mention in message.mentions: + if mention == client.user: + kondziu_mentioned = True + + if kondziu_mentioned or "conjurer:" in message.content: + async with channel.typing(): + logger.debug("Procedura chatu") + + message.content = message.content.replace("conjurer: ", "") + if message.author.nick: + username = message.author.nick + else: + username = message.author.name + vykidailo = False + bartender = False + if kondziu_mentioned: + prompt = message.clean_content + else: + prompt = message.content + for role in message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + if role.name == "Bartender": + bartender = True + global MESSAGE_TABLE # pylint: disable=global-statement + + result, MESSAGE_TABLE = await handle_response( + prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" + ) + + if len(result) < 1500: + await message.reply(result) + else: + while len(result) > 1500: + await message.reply(result[:1500]) + result = result[1500:] + if "imaginuje sobie:" in message.content: + async with channel.typing(): + logger.info("Poczatek procedury obrazkowej") + message.content = message.content.replace("imaginuje sobie: ", "") + logger.debug("Wywolanie obrazka: %s", message.content) + try: + response = await openAIClient.images.generate( + model="dall-e-3", + prompt=message.content, + size="1024x1024", + quality="standard", + n=1, + ) + except openai.APITimeoutError as e: + # Handle timeout error, e.g. retry or log + await message.reply( + f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" + ) + except openai.APIConnectionError as e: + await message.reply( + f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" + ) + except openai.BadRequestError as e: + # Handle invalid request error, e.g. validate parameters or log + if message.author.nick: + username = message.author.nick + else: + username = message.author.name + resp, _ = await handle_response( + f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie '{message.content}'", + True, + True, + MESSAGE_TABLE, + username, + "RANDOM", + ) + await message.reply( + f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + ) + except openai.AuthenticationError as e: + # Handle authentication error, e.g. check credentials or log + await message.reply( + f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" + ) + except openai.PermissionDeniedError as e: + # Handle permission error, e.g. check scope or log + await message.reply( + ( + f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" + ) + ) + except openai.RateLimitError as e: + await message.reply( + f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" + ) + except openai.APIError as e: + # Handle API error, e.g. retry or log + await message.reply( + f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" + ) + if response: + logger.info(response) + image_url = response.data[0].url + image_desc = response.data[0].revised_prompt + logger.debug("Wynikowy obrazek pod url: %s", image_url) + response = requests.get(image_url, timeout=360) + + temp_file_name = message.content + ".png" + temp_file_name = GRAPHICS_PATH + message.content + ".png" + + with open(temp_file_name, "wb") as dalle_file: + dalle_file.write(response.content) + logger.info("Koniec procedury obrazkowej.") + fnord = discord.File( + temp_file_name, spoiler=False, description=message.content + ) + await message.reply(f"{image_desc}", file=fnord) + + +# *=========================================== Define Functions + + +async def connect(ctx, arg=None): + """ + Connect the bot to a voice channel if it is not already connected. + + :param ctx: ctx is short for context and refers to the context in which the command was invoked. It + contains information about the message, the channel, the server, and the user who invoked the + command + :param arg: The `arg` parameter is an optional argument that can be passed to the `connect` + function. It is not used in the code snippet provided, but it could potentially be used to specify a + specific voice channel to connect to + :return: If the voice client is already connected, the function will return without doing anything. + If the function successfully connects to the voice channel, it will return a voice channel + connection object. If it is not possible to connect to the voice channel, the function will log an + error message and return nothing. + """ + if ctx and arg: + logger.info("Ctx and arg defined for connect") + + if client.voice_clients: + logger.info("Already connected with other client") + else: + vc = None + if ctx.author.voice.channel: + vc = await ctx.author.voice.channel.connect() + else: + voice_channel = client.get_channel(1060349757349974066) + vc = await voice_channel.connect() + if not vc: + logger.error("Not possible to connect to voice") + return + logger.info("Connected to voice") + + +async def disconnect(ctx): + """ + Asynchronous Python function that disconnects the voice client if it is connected and + logs a message if the context is defined. + + :param ctx: ctx is short for context and refers to the context in which a command is being executed. + It contains information about the message, the user who sent the message, the channel the message + was sent in, and more. In this case, the `disconnect` function is likely being called as a command + in + """ + if ctx: + logger.info("Ctx defined for disconnect") + if client.voice_clients: + voice_client = client.voice_clients[0] + if voice_client.is_connected(): + await voice_client.disconnect() + + +async def play(ctx, zamawial=None, arg=None): + """ + Play a music file, retrieve metadata about the file, and send a + message to a Discord channel with information about the song being played. + + :param ctx: The "ctx" parameter is a context object that contains information about the current + Discord command invocation, such as the message, the channel, and the user who invoked the command. + It is passed to the function automatically by the Discord.py library + :param zamawial: The parameter `zamawial` is a variable that stores the user who requested the song + to be played. It is an optional parameter and can be None if no user requested the song + :param arg: The `arg` parameter is an optional argument that can be passed to the `play` function. + It is not used in the code provided, so its purpose is unclear without additional context + """ + # TODO: Uzupełnianie metadanych. Jeśli ich nie ma sprawdzamy nazwę (w sposób inteligentny - czyli jeśli składa się z samych cyfr i/lub słowa track to sprawdzić album w sensie folder) + # TODO: Jeśli w metadanych jest tylko "audiotrack" i "album" też po + # folderze. Jeśli są niekompletne uzupełnić dalej. + logger.info("Play procedure") + if arg: + logger.info("Arg defined for play") + + file_to_play = None + voice_client = client.voice_clients[0] + if not client.voice_clients: + voice_client = client.voice_clients[0] + await connect(ctx) + if MUZYKA["queue"]: + file_to_play = MUZYKA["queue"].pop() + zamawial = MUZYKA["requester"].pop() + logger.info("Muzyka z listy zamówień") + else: + index = random.randint(0, len(music_file_list.get_file_list()) - 1) + file_to_play = music_file_list.get_file_list()[index] + logger.info("Muzyka losowo") + + logger.info(file_to_play) + metadata = eyed3.load(file_to_play) + query = None + temp = file_to_play.split(SEPARATOR_FILE_PATH)[-1] + kawalek = f"Zagram teraz kawalek {temp} z prywatnej kolekcji Hammera." + if metadata: + if metadata.tag: + if metadata.tag.title: + query = f"Opowiedz mi o kawałku {metadata.tag.title}" + kawalek = f"Zagram teraz kawalek {metadata.tag.title}" + if metadata.tag.artist: + query += f" wykonawcy {metadata.tag.artist}" + kawalek += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + query += f" z albumu {metadata.tag.album}" + kawalek += f" z albumu {metadata.tag.album}" + kawalek += " z prywatnej kolekcji Hammera." + if zamawial: + kawalek += f"Na specjalne życzenie <@{zamawial.id}>." + await ctx.send(kawalek) + + voice_client.play( + discord.PCMVolumeTransformer( + discord.FFmpegPCMAudio(source=file_to_play), volume=0.3 + ) + ) # NIE DOTYKAC POKRĘTŁA GŁOŚNOŚCI!!!! + if query: + logger.debug("Zapytanie do openai") + logger.debug(query) + vykidailo = False + bartender = False + for role in ctx.message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + if role.name == "Bartender": + bartender = True + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + global MESSAGE_TABLE # pylint: disable=global-statement + result, MESSAGE_TABLE = await handle_response( + query, vykidailo, bartender, message_table_muzyka, username, "MUSIC" + ) + logger.debug("Obecna historia czatu: %s", message_table_muzyka) + if len(result) < 1500: + await ctx.send(result) + else: + while len(result) > 1500: + await ctx.send(result[:1500]) + result = result[1500:] + + +async def archive_channel(channel_no): + """ + The function `archive_channel` retrieves messages from a specified channel, processes them, and + saves the data in a JSON file with a timestamp in the filename. + + :param channel_no: The `archive_channel` function you provided seems to be incomplete. It looks like + you are trying to archive messages from a Discord channel into a JSON file + """ + channel = client.get_channel(channel_no) + messages = [message async for message in channel.history(limit=None)] + path_newfile = f"{LOGSTORE}channeldump_{channel_no}_{datetime.now()}.json" + new_data = [] + for message in messages: + new_data.append(message) + with open(path_newfile, "x", encoding=ENCODING) as new_file: + new_file.seek(0) + json.dump(new_data, new_file, indent=4) + + +@tasks.loop(seconds=1) +async def check_music(): + """ + This Python function continuously checks for music playback in a voice client and plays music if + conditions are met. + """ + if client.voice_clients: + voice_client = client.voice_clients[0] + if ( + voice_client + and voice_client.is_connected() + and not voice_client.is_playing() + and not voice_client.is_paused() + ): + await play(MUZYKA["ctx"], MUZYKA["requester"]) + await asyncio.sleep(2) + + +@tasks.loop(seconds=3) +async def check_data_q(): + """ + This function checks the data queue for any new entries and processes them accordingly. + """ + try: + fresh_data = IN_COMM_Q.get(block=False) + entries = [] + if fresh_data.stop: + searcher = fresh_data.author + query = fresh_data.content + l_p = 1 + for doi in fresh_data.entries: + logger.info(doi) + desc = fresh_data.entries[doi] + title = desc["Title"][0] + entries.append( + f"{l_p}. {title} pod linkiem https://www.sci-hub.se/{doi} i jest to {desc['type']}\n" + ) + l_p += 1 + message = "*Z podłogi wysuwa się winda na książki*" + if fresh_data.ctx is not None: + ctx = fresh_data.ctx + message += f" Dzielny poszukiwaczu @{searcher} w odpowiedzi na twoje zapytanie {query} nr {fresh_data.uuid}" + else: + ctx = client.get_channel(1062047571557744721) + message += f" Mam wynik dla zapytania {fresh_data.uuid} ale że Hammer coś grzebał nie pamiętam kto to chciał i co chciał" + + message += "nasza biblioteka służy Ci odpowiedzią. Przy dźwięku fanfar winda się otwiera a w środku " + if len(entries) < 1: + message += "niestety nie ma nic" + await ctx.send(message) + elif len(entries) >= 1 and len(entries) < 5: + message += " znajduje się coś:\n" + for item in entries: + message += item + await ctx.send(message) + else: + message += " znajduje się cholernie dużo:\n" + for item in entries: + message += item + if len(message) > 1500: + await ctx.send(message) + message = "" + + # Kept for sentimental reasons + # await ctx.send(f"O. A tak będzie wyglądało coś ciekawego w przyszłości: {data}") + except Empty: + pass + + +@tasks.loop(seconds=120) +async def check_self(): + """ + The function `check_data` periodically checks for conditions to send messages and manage log files + in a Discord channel. + """ + # logger.info("Heartbeat of cleanup proc") + channel = client.get_channel(1062047367337095268) + messages = [message async for message in channel.history(limit=1)] + for mess in messages: + channel = mess.channel + if os.path.getsize(LOGFILE) > 60000000: + await channel.send( + "*Conjurer porządkuje bar, wypala szklanki do czysta miotaczem płomieni ze swojej zbroi i ogólnie wygląda na zajętego....*" + ) + async with channel.typing(): + shutil.copyfile(LOGFILE, f"{LOGSTORE}discord{datetime.now()}") + logger.info("Log rollover") + handler.doRollover() + await channel.send( + "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* ...sać powieści o naszym wspaniałym barze SZEFIE!" + ) + + if os.path.getsize(MEMORY_FIVE_MUZYKA) > 3000000: + await channel.send( + "*Conjurer porządkuje bar, wypala szklanki do czysta miotaczem płomieni ze swojej zbroi i ogólnie wygląda na zajętego....*" + ) + async with channel.typing(): + path_newfile = f"{LOGSTORE}pamiec_muzyki{datetime.now()}.json" + logger.info(path_newfile) + with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: + with open(path_newfile, "x", encoding=ENCODING) as new_file: + # First we load existing data into a dict. + file_data = json.load(file_music_memory) + new_data = [] + new_data.append(file_data[0]) + new_data.extend(file_data[:-20]) + file_music_memory.truncate(0) + file_music_memory.seek(0) + # convert back to json. + new_file.seek(0) + json.dump(new_data, file_music_memory, indent=4) + json.dump(file_data, new_file, indent=4) + await channel.send( + "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* ...sać powieści o naszym wspaniałym barze SZEFIE!" + ) + if os.path.getsize(MEMORY_FIVE_SIARA) > 3000000: + path_newfile = f"{LOGSTORE}pamiec_rozmow{datetime.now()}.json" + logger.info(path_newfile) + await channel.send( + "*Conjurer porządkuje bar, ścina lekkim laserem pulsacyjnym powierzchnie baru o grubości kilku mikronów i ogólnie wygląda na zajętego....*" + ) + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file: + with open(path_newfile, "x", encoding=ENCODING) as new_file: + # First we load existing data into a dict. + file_data = json.load(file) + new_data = [] + new_data.append(file_data[0]) + new_data.extend(file_data[-20]) + file.truncate(0) + file.seek(0) + new_file.seek(0) + # convert back to json. + json.dump(new_data, file, indent=4) + json.dump(file_data, new_file, indent=4) + await channel.send( + "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* eprzyć o głupotach z klientami... Szefie... *Bierze 'ukradkowy' łyk z piersiówki*" + ) + global LAST_SPONTANEOUS_CALL + global TIME_BETWEEN_CALLS + tdelta = datetime.now() - LAST_SPONTANEOUS_CALL + tdelta = tdelta.total_seconds() + if tdelta > TIME_BETWEEN_CALLS: + logger.info("Spontaneous call") + TIME_BETWEEN_CALLS = random.randint( + 10200, 272800 + ) # temp random, set after each call + logger.debug(TIME_BETWEEN_CALLS) + LAST_SPONTANEOUS_CALL = datetime.now() + async with channel.typing(): + message = await get_random_cyclic_message() + logger.info("Odpowiedz") + logger.info(message) + await channel.send(message) + + +async def get_random_cyclic_message(): + """ + The function `get_random_cyclic_message` returns a random cyclic message from a list of cyclic + words. + :return: a random cyclic message from the list `cyclic_words`. + """ + channel_id = 1062047367337095268 + channel = client.get_channel(channel_id) + ai_check = random.randint(0, 10) + logger.info("Losowa wypowiedź") + if ai_check < 2: + logger.info("Predefiniowana") + messnum = random.randint(0, len(cyclic_words)) + logger.debug(messnum) + logger.debug(len(cyclic_words)) + mess_key = list(cyclic_words.keys())[messnum] + return cyclic_words[mess_key][0] + ai_check2 = random.randint(0, 10) + global MESSAGE_TABLE + if ai_check2 < 6: + logger.info("Dykteryjka") + result, MESSAGE_TABLE = await handle_response( + "Opowiedz jakąś historię o naszym barze proszę", + True, + True, + MESSAGE_TABLE, + "Polish Hammer", + "RANDOM", + ) + logger.info(result) + else: + logger.info("Wtracenie w dyskusje") + messages = [message async for message in channel.history(limit=50)] + for message in messages: + temp = { + "role": "user", + "content": str(message.author) + ":" + str(message.content), + } + MESSAGE_TABLE.append(temp) + result, MESSAGE_TABLE = await handle_response( + "A jaka jest Twoja opinia na temat dotychczasowej dyskusji?", + True, + True, + MESSAGE_TABLE, + "Polish Hammer", + "RANDOM", + ) + logger.info(result) + return result + + +async def handle_response( + prompt, vykidailo, bartender, history, username, request_type +): + """ + Handle responses by appending them to a history, use OpenAI to + generate a response, and then append the generated response to the history. + + :param prompt: The prompt for the OpenAI chatbot to generate a response to + :param vykidailo: It is a boolean variable that indicates whether the user invoking the function is + an administrator or not + :param bartender: The bartender parameter is a boolean value indicating whether the user making the + request is a bartender or not + :param history: A list containing the conversation history between the user and the assistant + :param username: The username of the user who initiated the conversation + :param music: The "music" parameter is a boolean value that indicates whether the conversation is + related to music or not. If it is True, the conversation history will be stored in a different file + and the response will be generated using a different model + :return: The function `handle_response` returns a tuple containing the `result` and `MESSAGE_TABLE`. + """ + # TODO: Wykrywać "Pracuję nad odpowiedzią i tego typu rzeczy" + logger.info("Wywolanie procedury openai z promptem: %s", prompt) + temp = {"role": "user", "content": username + ":" + prompt} + if vykidailo or bartender: + logger.info("Administrator coś chciał") + history.append(temp) + if request_type == "MUSIC": + with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: + # First we load existing data into a dict. + file_data = json.load(file_music_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_music_memory.seek(0) + # convert back to json. + json.dump(file_data, file_music_memory, indent=4) + elif request_type == "RANDOM": + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + else: + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + history = [] + history.append(GPT_SETTINGS[0]) + chat_gpt_config_request_size = num_tokens_from_string(GPT_SETTINGS[0], "gpt-4") + + for slowo, reakcja in word_reactions.items(): + if not reakcja[3]: + content = ( + "Kiedy słyszysz " + + slowo + + " to reagujesz lub dzieje się to " + + reakcja[0] + ) + temp = {"role": "system", "content": content} + chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4") + history.append(temp) + + final_prompt = username + ":" + prompt + logger.debug( + "Rozmiar zapytania przed dodaniem historii %s", chat_gpt_config_request_size + ) + if request_type == "MUSIC": + algorithm = "gpt-4o" + table = message_table_muzyka + token_amount = 10700 + elif request_type == "RANDOM": + algorithm = "gpt-4o" + table = MESSAGE_TABLE + token_amount = 10700 + else: + table = MESSAGE_TABLE + algorithm = "gpt-4o" + token_amount = 10700 + + prompt_gpt_request_size = num_tokens_from_string( + {"role": "user", "content": final_prompt}, "gpt-4" + ) + temptable = [] + for i in reversed(table): + temp_token = num_tokens_from_string(i, "gpt-4") + logger.debug( + "Rozmiar zapytania %s prompt %s temp %s", + chat_gpt_config_request_size, + prompt_gpt_request_size, + temp_token, + ) + if ( + chat_gpt_config_request_size + < token_amount + prompt_gpt_request_size + temp_token + ): + temptable.insert(1, i) + chat_gpt_config_request_size += temp_token + history.extend(temptable) + temp = {"role": "user", "content": final_prompt} + history.append(temp) + logger.info("Rozmiar zapytania po wyslaniu %s", chat_gpt_config_request_size) + try: + response = await openAIClient.chat.completions.create( + model=algorithm, messages=history + ) + except openai.APITimeoutError as e: + # Handle timeout error, e.g. retry or log + result = f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" + except openai.APIConnectionError as e: + result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" + except openai.BadRequestError as e: + # Handle invalid request error, e.g. validate parameters or log + resp, _ = await handle_response( + f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'", + True, + True, + MESSAGE_TABLE, + username, + "RANDOM", + ) + result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + except openai.APIResponseValidationError as e: + # Handle invalid request error, e.g. validate parameters or log + resp, _ = await handle_response( + f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'", + True, + True, + MESSAGE_TABLE, + username, + "RANDOM", + ) + result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + except openai.AuthenticationError as e: + # Handle authentication error, e.g. check credentials or log + result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" + except openai.PermissionDeniedError as e: + # Handle permission error, e.g. check scope or log + result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" + except openai.RateLimitError as e: + result = f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" + except openai.UnprocessableEntityError as e: + result = f"*Kondziu patrzy na terminal. Potem na to co każesz mu wysłać....* Ja wiem że jesteśmy w barze BDSM - ale nie da się włożyć TEGO w TO. *Za jego plecami na terminalu pojawia się:* {e}" + except openai.APIError as e: + # Handle API error, e.g. retry or log + result = f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" + + logger.info("Historia wysłana:") + logger.info(history) + await asyncio.sleep(15) + result = "" + logger.debug("Odpowiedzi") + logger.info(response) + logger.debug(response.choices) + for choice in response.choices: + result += choice.message.content + logger.info("Sformatowane odpowiedzi") + logger.info(result) + temp = {"role": "assistant", "content": result} + history.append(temp) + if request_type == "MUSIC": + with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: + # First we load existing data into a dict. + file_data = json.load(file_music_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_music_memory.seek(0) + # convert back to json. + json.dump(file_data, file_music_memory, indent=4) + elif request_type == "RANDOM": + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + else: + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + return result, MESSAGE_TABLE + + +# define an asynchronous generator +async def async_iterator_generator(range_of_iterable): + """ + Generate asynchronouse iterator. + This is an incomplete function definition for an asynchronous iterator generator that takes a range + of iterable as input. + + :param range_of_iterable: The parameter `range_of_iterable` is likely a range or iterable object + that the async iterator generator will iterate over asynchronously. It could be a list, tuple, set, + or any other iterable object. The generator will yield each item in the iterable object + asynchronously, allowing other code to run in between + """ + # normal loop + for i in range_of_iterable: + # pylint: disable=pointless-string-statement + # yield the result + yield i + """ + Alternatywna wersja przerobienia fora na asynchroniczny. + # traverse the iterable of awaitables + for item in coros: + # await and get the result from the awaitable + result = await item + # report the results + print(result) + """ + + +async def get_stats(ctx, history_limit): + """ + The `get_stats` function retrieves the message history of a specific channel and counts the + frequency of each word in the messages. + + :param ctx: The `ctx` parameter is an object that represents the context of the command being + executed. It contains information such as the message, the author, the server, and other relevant + details + :param history_limit: The `history_limit` parameter is the maximum number of messages to retrieve + from the channel history. It determines how far back in time the statistics will be calculated + :return: The function `get_stats` returns a dictionary `stats` that contains the frequency count of + words found in the messages from the specified channel. + """ + channel_id = 1062047367337095268 + async with ctx.typing(): + stats = {} + channel = client.get_channel(channel_id) + messages = [message async for message in channel.history(limit=history_limit)] + # traverse the iterable of awaitables + for message in messages: + # await and get the result from the awaitable + result = message.content + words = result.split() + for word in words: + if word in stats: + stats[word] += 1 + else: + stats[word] = 1 + for name, how_many in stats.items(): + yield name, how_many + + +async def wyszukaj(ctx, how_many=0, slowa_kluczowe=None): + """ + Take in a context object and an optional integer parameter "how_many" and perform search + operation on music library. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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 how_many: how_many is a parameter that specifies the number of search results to be returned. + It is an optional parameter with a default value of 0, which means that if no value is provided for + how_many, the function will return all search results, defaults to 0 (optional) + """ + # i sieć neuronową z możliwością wyrażenia opinii o dopasowaniu. + if slowa_kluczowe: + word_list = slowa_kluczowe + else: + word_list = ctx.message.content.split() + logger.info("Wyszukuje") + jrequest = { + "lista_slow": word_list, + "dlugosc_plejlisty": how_many, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{GET_PLAYLIST}", + json=jrequest, + timeout=360, + ) + + return_data = await coroutine + if not return_data.status_code == 200: + await ctx.send("Wołaj szefa - coś się wyjebało") + return + logger.info(return_data.json()["data"]) + return return_data.json()["data"] + + +async def remove_characters(string, character): + """ + The `remove_characters` function removes all occurrences of a specified character from a given + string. + + :param string: The string parameter is the input string from which characters will be removed + :param character: The character parameter is the character that you want to remove from the string + :return: a new string where all occurrences of the specified character have been removed. + """ + return string.replace(character, "") + + +async def max_weight(lista): + """ + Take a list of weights and return the maximum weight. + + :param lista: It seems like the parameter `lista` is a list of items, possibly representing weights. + The function name `max_weight` suggests that the function is intended to find the maximum weight + from the list. However, without more context or information about the problem, it's difficult to say + for sure what the + """ + maximum_weight = 0 + for iterator in lista: + if iterator[0] > maximum_weight: + maximum_weight = iterator[0] + return maximum_weight + + +# *=========================================== Define Commands + + +@client.hybrid_command( + name="skip_track", + description="Przeskocz kawałek w radiu", + guild=discord.Object(id=664789470779932693), +) +async def skip_track(ctx): + async with ctx.typing(): + allowed = False + for role in ctx.author.roles: + if role.name in ("Thane", "Jarl"): + allowed = True + if not allowed: + await ctx.send("Łapy precz od radia") + else: + coroutine = asyncio.to_thread( + requests.get, + f"{RADIO_HARBOR_ADDRESS}{SKIP_TRACK}", + timeout=360, + ) + result = await coroutine + logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + +@client.hybrid_command( + name="zrestartuj_radio", + description="Komenda ktora uruchamia radio ponownie jakby się zawiesiło", + guild=discord.Object(id=664789470779932693), +) +async def zrestartuj_radio(ctx): + async with ctx.typing(): + allowed = False + for role in ctx.author.roles: + if role.name in ("Thane", "Jarl"): + allowed = True + if not allowed: + await ctx.send("Łapy precz od radia") + else: + retcode = subprocess.run( + "/home/pi/Conjurer/restart_radio.sh", + shell=False, + check=False, + capture_output=True, + ) + logger.info("Wynik: %s", retcode) + + + + + + +@client.hybrid_command( + name="update_banlist", + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", + guild=discord.Object(id=664789470779932693), +) +async def update_banlist(ctx): + """ + Update a banlist in a Discord guild from preprovided list in channel. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + async with ctx.channel.typing(): + allowed = False + for role in ctx.author.roles: + if role.name == "Jarl": + allowed = True + + if ctx.channel.id != 1102190666827702342: + # trunk-ignore(codespell/misspelled) + await ctx.send("Nie wydaje mnie sie") + elif not allowed: + await ctx.send("Idź bo Cię zdziele") + else: + tobancunter = 0 + async with ctx.typing(): + # 1. get list of users posted on channel for banning + channel = ctx.channel + messages = [message async for message in channel.history(limit=None)] + ids_to_ban = [] + for message in messages: + lines = message.content.split("\n") + for line in lines: + match = re.search( + "\\d{18}", + line, + # trunk-ignore(codespell/misspelled) + ) # poprawilem backslash na podwojny bo linter sie czepial + if match: + ids_to_ban.append(match.group()) + tobancunter += 1 + # 2. get list of already banned users for all servers Conjurer + # has rights to + for guild in client.guilds: + bancunter = 0 + # trunk-ignore(codespell/misspelled) + logger.info("Serwer: %s", guild) + current_banlist = [] + permission = True + try: + async for entry in guild.bans(limit=None): + current_banlist.append(entry.user.id) + bancunter += 1 + except BaseException: # pylint: disable=broad-exception-caught + permission = False + if permission: + # 3. compare lists ban only users on list 2 and not on + # list 1 + cunter_counter = 0 + bad_id_cunter_counter = 0 + ban_list = np.setdiff1d(ids_to_ban, current_banlist) + for ident in ban_list: + try: + tmp_user = await client.fetch_user(int(ident)) + await guild.ban( + tmp_user, + reason="Automatyczna lista banów", + delete_message_seconds=0, + ) + cunter_counter += 1 + except discord.NotFound: + logger.info( + "Znaleziono uzyszkodnika ktory juz nie ma konta" + ) + bad_id_cunter_counter += 1 + # 4. return message success/failure - how many banned + await ctx.send( + "Obecnie na serwerze {guild} jest {bancunter} zbanowanych użytkowników. Znaleziono na tym kanale {tobancunter} id użytkowników do zbanowania. Zbanowano {cunter_counter} użytkowników. Na liście jest {bad_id_cunter_counter} użytkowników którzy skasowali już konto" + ) + else: + await ctx.send( + f"Nie mam na serwerze {guild} uprawnień do banowania. :()" + ) + await ctx.send("Zrobione tak czy inaczej") + + +@client.hybrid_command( + nsfw=True, + name="get_image_sadox", + description="Wyświetla losową stronę z losowego komiksu FanSadox. Bardzo NSFW.", + guild=discord.Object(id=664789470779932693), +) +async def get_image_sadox(ctx): + """ + Take in a context parameter and retrieve an image related from fansadox collection. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. In this case, it is likely being used + to determine + """ + logger.info("Get sadox") + channel = ctx.message.channel + async with channel.typing(): + # select random file + res = [] + # Iterate directory + for path in os.listdir(DIR_PATH_SADOX): + # check if current path is a file + if os.path.isfile(os.path.join(DIR_PATH_SADOX, path)): + res.append(path) + filename = res[random.randrange(0, len(res) - 1)] + # select random page + file = open(DIR_PATH_SADOX + filename, "rb") + readpdf = PyPDF2.PdfReader(file) + totalpages = len(readpdf.pages) + page = random.randrange(1, totalpages) + # convert page to image + image = pdf2image.convert_from_path( + DIR_PATH_SADOX + filename, first_page=page, last_page=page + ) + byte_io_stream = io.BytesIO() + image[0].save(byte_io_stream, "JPEG") + byte_io_stream.seek(0) + byte_io_stream.name = "image.jpg" + file = discord.File(byte_io_stream) + await ctx.send(file=file) + logger.info("Get sadox completed") + + +@client.hybrid_command( + name="graj_muzyko", + description="Włącza muzykę na kanale #nocna-zmiana.", + guild=discord.Object(id=664789470779932693), +) +async def graj_muzyko(ctx): + """ + Async function named "graj_muzyko" starts music playback on channel "Nocna Zmiana". + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + + logger.info("Press play on tape") + async with ctx.typing(): + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["ctx"] = ctx + await connect(ctx=ctx) + await play(ctx=ctx) + logger.info("Press play on tape completed") + check_music.start() + + +@client.hybrid_command( + name="radio_hardkor", + description="Włącza radio Conjurer na kanale #nocna-zmiana.", + guild=discord.Object(id=664789470779932693), +) +async def radio_hardkor(ctx): + """ + Plays the radio hardkor stream in the voice channel. + + Args: + ctx: The context object representing the invocation context. + + Returns: + None + """ + logger.info("Press play on radio hardkor") + async with ctx.typing(): + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["ctx"] = ctx + if not client.voice_clients: + await connect(ctx=ctx) + voice_client = client.voice_clients[0] + voice_client.play( + discord.PCMVolumeTransformer( + discord.FFmpegPCMAudio("http://95.175.16.246:666/mp3-stream"), + volume=0.3, + ) + ) + logger.info("Press play on radio completed") + else: + logger.error("Already playing") + await asyncio.sleep(12) + check_music.start() + + +@client.hybrid_command( + name="cisza", + description="Wyłącza muzykę i czyści plejliste.", + guild=discord.Object(id=664789470779932693), +) +async def cisza(ctx): + """ + Stop playback of the music and clear the queue. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + logger.info("Stop") + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["ctx"] = None + voice_client = client.voice_clients[0] + check_music.stop() + if voice_client.is_connected(): + await disconnect(ctx=ctx) + while MUZYKA["queue"]: + MUZYKA["queue"].pop() + MUZYKA["requester"].pop() + logger.info("Stop completed") + + +@client.hybrid_command( + name="dalej", + description="Przerzuca na następny kawałek", + guild=discord.Object(id=664789470779932693), +) +async def dalej(ctx): + """ + Play next track in queue. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + represents the context in which a command is being executed, including information such as the + message, the channel, the server, and the user who invoked the command. The context object is used + to access and manipulate this + """ + if ctx: + logger.info("Ctx defined for dalej") + logger.info("Next") + voice_client = client.voice_clients[0] + if voice_client.is_connected(): + voice_client.stop() + logger.info("Next completed") + + +@client.hybrid_command( + name="daj_mi_chwile", + description="Pauzuje", + guild=discord.Object(id=664789470779932693), +) +async def daj_mi_chwile(ctx): + """ + Pause music playback. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in most + Discord.py commands and is + """ + if ctx: + logger.info("Ctx defined for pause") + + logger.info("Pause") + voice_client = client.voice_clients[0] + if voice_client.is_connected(): + voice_client.pause() + logger.info("Pause completed") + + +@client.hybrid_command( + name="graj_dalej", + description="Odpauzowuje", + guild=discord.Object(id=664789470779932693), +) +async def graj_dalej(ctx): + """ + Unpause music and continue playback. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + if ctx: + logger.info("Ctx defined for graj_dalej") + logger.info("Unpause") + voice_client = client.voice_clients[0] + if voice_client.is_paused(): + voice_client.resume() + logger.info("Unpause completed") + + +@client.hybrid_command( + name="zagraj_mi_kawalek", + description="Wyszukuje w bibliotece muzycznej Hammera kawałek który ma zostać zagrany", + guild=discord.Object(id=664789470779932693), +) +async def zagraj_mi_kawalek(ctx): + """ + Play a song or music piece in response to a command + triggered by a user in a Discord chat context. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + async with ctx.typing(): + search_time_glob = datetime.now() + logger.info("Zaczynam szukać timestamp %s", datetime.now()) + file = await wyszukaj(ctx=ctx) + logger.info( + "Koniec szukania(timestamp %s zajęło %s", + datetime.now(), + datetime.now() - search_time_glob, + ) + + if file: + for plik in file: + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, plik[1]) + MUZYKA["requester"].insert(0, ctx.author) + metadata = eyed3.load(plik[1]) + reply = "Dodałem do playlisty {plik[1]} z prywatnej kolekcji Hammera." + if metadata.tag.title: + reply = f"Dodałem do playlisty kawalek {metadata.tag.title}" + if metadata.tag.artist: + reply += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + reply += f" z albumu {metadata.tag.album}" + reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę." + await ctx.send(reply) + else: + await ctx.send( + "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" + ) + + +@client.hybrid_command( + name="dodaj_do_ulubionych", + description="Dodaje do listy ulubionych w radiu", + guild=discord.Object(id=664789470779932693), +) +async def dodaj_do_ulubionych(ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + logger.info("Zaczynam szukać timestamp %s", datetime.now()) + + dlugosc_playlisty = ctx.message.content.split()[1] + word_list = ctx.message.content.split() + jrequest = { + "lista_slow": word_list, + "dlugosc_plejlisty": dlugosc_playlisty, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{ADD_TO_PRIO_PLAYLIST}", + json=jrequest, + timeout=360, + ) + result = await coroutine + logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + +@client.hybrid_command( + name="ja_chciol", + description="Dodaje do listy ulubionych w radiu", + guild=discord.Object(id=664789470779932693), +) +async def request_radio(ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + logger.info("Zaczynam szukać timestamp %s", datetime.now()) + + word_list = ctx.message.content.split() + jrequest = { + "lista_slow": word_list, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{REQUEST_MUSIC}", + json=jrequest, + timeout=360, + ) + result = await coroutine + logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + +@client.hybrid_command( + name="stworz_audycje", + description="Dodaje do listy ulubionych w radiu", + guild=discord.Object(id=664789470779932693), +) +async def stworz_audycje(ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + logger.info("Zaczynam szukać timestamp %s", datetime.now()) + + dlugosc_playlisty = ctx.message.content.split()[1] + word_list = ctx.message.content.split() + jrequest = { + "lista_slow": word_list, + "dlugosc_plejlisty": dlugosc_playlisty, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{CREATE_PRIO_PLAYLIST}", + json=jrequest, + timeout=360, + ) + result = await coroutine + logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + +@client.hybrid_command( + name="wyczysc_ulubione", + description="Czysci liste ulubionych w radiu", + guild=discord.Object(id=664789470779932693), +) +async def wyczysc_ulubione(ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + coroutine = asyncio.to_thread( + requests.get, + f"{FILE_SERVICE_ADDRESS}{CLEAR_PRIO}", + timeout=360, + ) + result = await coroutine + logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + +@client.hybrid_command( + name="zrob_mi_plejliste", + description="Generuje playliste - param1 - dlugosc, potem slowa wyszukiwania", + guild=discord.Object(id=664789470779932693), +) +async def zrob_mi_plejliste(ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + reply = "Wygenerowana plejlista:\n" + async with ctx.typing(): + logger.info("Zaczynam szukać timestamp %s", datetime.now()) + search_time_glob = datetime.now() + + dlugosc_playlisty = ctx.message.content.split()[1] + file = await wyszukaj(ctx=ctx, how_many=dlugosc_playlisty) + logger.info( + "Koniec szukania(timestamp %s zajęło %s", + datetime.now(), + datetime.now() - search_time_glob, + ) + index = 1 + if file: + for plik in file: + if plik[1]: + logger.info(plik) + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, plik[1]) + MUZYKA["requester"].insert(0, ctx.author) + metadata = eyed3.load(plik[1]) + if metadata and metadata.tag: + if metadata.tag.title: + reply += f"{index} {metadata.tag.title}" + else: + reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera." + + if metadata.tag.artist: + reply += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + reply += f" z albumu {metadata.tag.album}" + if metadata.tag.title: + reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę.\n" + else: + reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera.\n" + + index += 1 + if len(reply) > 1800: + await ctx.send(reply) + reply = "" + else: + await ctx.send( + "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" + ) + await ctx.send(reply) + logger.info("Plejlista zrobiona") + + + + + +@client.hybrid_command( + name="chata_hammera", + description="Czas od ostatniego incydentu w AbsinthHammerTimeSpaceContinuum", +) +async def chata_hammera(ctx): + """ + Send measured time from last incident in Hammer Fortress to the chat. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file: + # First we load existing data into a dict. + file_data = json.load(new_file) + opis = file_data[-1][0] + czas = datetime.strptime(file_data[-1][1], "%Y-%m-%d %H:%M:%S.%f") + await ctx.send( + f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Od ostatniego incydentu w chacie Hammera minęło {datetime.now() - czas}. Incydent to: {opis}" + ) + + +@client.hybrid_command( + name="historia_incydentow_u_hammera", + description="Wyswietla liste incydentów które miały miejsce w AbsinthHammerTimeSpaceContinuum.", +) +async def historia_incydentow_u_hammera(ctx): + """ + Send a list of incidents in Hammer Fortress to the chat. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file_accidents: + # First we load existing data into a dict. + file_data = json.load(new_file_accidents) + return_data = "Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Historia zarejestrownych incydentów u Hammera:\n" + index = 1 + for iter_data in file_data: + opis = iter_data[0] + czas = datetime.strptime(iter_data[1], "%Y-%m-%d %H:%M:%S.%f") + add_data = f"{index}. Opis: {opis} Data: {czas}" + return_data = return_data + add_data + "\n" + index += 1 + await ctx.send(return_data) + + +@client.hybrid_command( + name="reset_the_clock", + description="Resetowanie zegara - dostępne wyłącznie dla Hammera", + guild=discord.Object(id=664789470779932693), +) +async def reset_the_clock(ctx): + """ + Reset the clock on "accidents" in Hammer Fortress adding a new one. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + hammer = False + for role in ctx.message.author.roles: + if role.name == "Bartender": + hammer = True + if hammer: + with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file_accidents: + # First we load existing data into a dict. + file_data = json.load(new_file_accidents) + czas = datetime.strptime(file_data[-1][1], "%Y-%m-%d %H:%M:%S.%f") + opis = ctx.message.content[16:] + accident = accident = [opis, f"{datetime.now()}"] + file_data.append(accident) + new_file_accidents.seek(0) + # convert back to json. + json.dump(file_data, new_file_accidents, indent=4) + await ctx.send( + f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n RESET THE CLOCK. Od ostatniego incydentu w chacie Hammera minęło {czas}. No ale teraz się odpierdoliło: {opis}" + ) + else: + await ctx.send( + f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Gdzie z łapami do zegara? {client.get_user(346956223645614080).mention} coś albo się komuś stało albo zaraz się stanie jak będzie zegar tykał...." + ) + + +@client.hybrid_command( + name="zagraj_muzyke_mego_ludu", + description="Gra muzyke wyszukana na bazie tematow konwersacji na kanale NZ", + guild=discord.Object(id=664789470779932693), +) +async def zagraj_muzyke_mego_ludu(ctx): + """ + Play completeley random playlist based on messaging history. + + :param ctx: ctx stands for "context" and is typically used in Discord.py commands to represent the + context in which the command was invoked. This includes information such as the message, the + channel, the author, and other relevant details. In this case, it is being passed as a parameter to + the `get_image + """ + stats = {} + if ctx: + logger.info("Zagraj muzyke mego ludu: wywolane") + stat_iter = get_stats(ctx, MUZYKA_MOJEGO_LUDU_HISTORIA) + async for name, how_many in stat_iter: + if len(name) > 3: + stats[name] = how_many + + sorted_stats = sorted(stats.items(), key=lambda x: x[1]) + logger.info("Zagraj muzyke mego ludu:zebrano statystyki") + key_words = [] + for stat in sorted_stats: + key_words.append(stat[0]) + logger.info(sorted_stats[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE]) + logger.info(key_words[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE]) + logger.info(sorted_stats[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) + logger.info(key_words[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) + + final_key_words = key_words[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE] + final_key_words.extend(key_words[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) + reply = "Wygenerowana plejlista:\n" + async with ctx.typing(): + logger.info( + "Zagraj muzyke mego ludu:Zaczynam szukać timestamp %s", datetime.now() + ) + search_time_glob = datetime.now() + file = await wyszukaj( + ctx=ctx, + how_many=MUZYKA_MOJEGO_LUDU_PLAJLISTA, + slowa_kluczowe=final_key_words, + ) + logger.info("Zagraj muzyke mego ludu:wygenerowano plejliste") + logger.info(file) + logger.info( + "Zagraj muzyke mego ludu: Koniec szukania(timestamp %s zajęło %s", + datetime.now(), + datetime.now() - search_time_glob, + ) + index = 1 + if file: + for plik in file: + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, plik[1]) + MUZYKA["requester"].insert(0, ctx.author) + metadata = eyed3.load(plik[1]) + if metadata and metadata.tag: + if metadata.tag.title: + reply += f"{index} {metadata.tag.title}" + else: + reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera." + + if metadata.tag.artist: + reply += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + reply += f" z albumu {metadata.tag.album}" + if metadata.tag.title: + reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę.\n" + else: + reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera.\n" + + index += 1 + if len(reply) > 1800: + await ctx.send(reply) + reply = "" + else: + await ctx.send( + "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" + ) + await ctx.send(reply) + logger.info("Plejlista zrobiona") + + +@client.hybrid_command( + name="parametry_muzyki_mego_ludu", + description="Konfiguruje komende zagraj muzyke mojego ludu", + guild=discord.Object(id=664789470779932693), +) +async def parametry_muzyki_mego_ludu( + ctx, ile_historii=1500, ile_slow_kluczowych=15, jak_dluga_plejlista=30 +): + """ + The function `parametry_muzyki_mego_ludu` sets global variables for the number of history entries, + number of keywords, and length of playlist for a music application. + + :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating + Discord bots. It represents the context of the command being executed, including information about + the message, the server, and the user who invoked the command + :param ile_historii: The parameter "ile_historii" represents the number of history items for the + music playlist, defaults to 1500 (optional) + :param ile_slow_kluczowych: The parameter "ile_slow_kluczowych" represents the number of keywords or + key phrases related to music that you want to include in your analysis or search, defaults to 30 + (optional) + :param jak_dluga_plejlista: The parameter "jak_dluga_plejlista" determines the length of the + playlist. It specifies how many songs should be included in the playlist, defaults to 30 (optional) + """ + + if ctx: + async with ctx.typing(): + # if ':' in ile_historii: + # _, _, ile_historii = ile_historii.partition(':') + # if ':' in ile_slow_kluczowych: + # _, _, ile_slow_kluczowych = ile_slow_kluczowych.partition(':') + # if ':' in jak_dluga_plejlista: + # _, _, jak_dluga_plejlista = jak_dluga_plejlista.partition(':') + try: + global MUZYKA_MOJEGO_LUDU_HISTORIA # pylint: disable=global-statement + global MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE # pylint: disable=global-statement + global MUZYKA_MOJEGO_LUDU_PLAJLISTA # pylint: disable=global-statement + logger.info( + "Długość historii wyszukiwania: %s\n Ilość słów kluczowych: %s\n Długość plejlisty do wygenerowanie: %s", + MUZYKA_MOJEGO_LUDU_HISTORIA, + MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE, + MUZYKA_MOJEGO_LUDU_PLAJLISTA, + ) + ctx.send( + f"Dotychczasowe wartośći:\n Długość historii wyszukiwania: {MUZYKA_MOJEGO_LUDU_HISTORIA}\n Ilość słów kluczowych: {MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE}\n Długość plejlisty do wygenerowanie:{MUZYKA_MOJEGO_LUDU_PLAJLISTA}" + ) + MUZYKA_MOJEGO_LUDU_HISTORIA = ile_historii + MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE = ile_slow_kluczowych + MUZYKA_MOJEGO_LUDU_PLAJLISTA = jak_dluga_plejlista + logger.info( + "Długość historii wyszukiwania: %s\n Ilość słów kluczowych: %s\n Długość plejlisty do wygenerowanie: %s", + MUZYKA_MOJEGO_LUDU_HISTORIA, + MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE, + MUZYKA_MOJEGO_LUDU_PLAJLISTA, + ) + ctx.send( + f"Zmienione wartośći:\n Długość historii wyszukiwania: {MUZYKA_MOJEGO_LUDU_HISTORIA}\n Ilość słów kluczowych: {MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE}\n Długość plejlisty do wygenerowanie:{MUZYKA_MOJEGO_LUDU_PLAJLISTA}" + ) + except discord.ext.commands.errors.BadArgument as exce: + ctx.send("Spierdalaj") + logger.info(exce) + + +@client.hybrid_command( + name="wyszukaj_linki_do_dokumentow", + description="Szuka linkow doi w bazie crossref i podaje linki do scihuba", + guild=discord.Object(id=664789470779932693), +) +async def wyszukaj_linki_do_dokumentow(ctx): + """ + The function `wyszukaj_linki_do_dokumentow` searches for links to documents in a crossref database and provides links to scihub. + + :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating Discord bots. + It represents the context of the command being executed, including information about the message, the server, + and the user who invoked the command. + """ + query = ctx.message.content + query_uuid = uuid.uuid4() + # TODO: TESTING ONLY!! + # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') + ctx.message.content = ctx.message.content.replace( + "$wyszukaj_linki_do_dokumentow", "" + ) + + json_query = { + "UUID": str(query_uuid), + "query": str(query), + "page": 1, + "deep_search": False, + } + coroutine = asyncio.to_thread( + requests.post, + f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", + json=json_query, + timeout=360, + ) + await ctx.send( + "*Conjurer powoli notuje podane przez Ciebie. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + + " wrzuca twój liścik do środka* To teraz trza poczekać kilka godzin. Biblioteka to 3/4 stacji." + + " A przecież pół stacji to browar, motelik dla zabaw cielesnych i ten Bar. Więc 12/8 stacji teraz będzie ciężko pracować" + ) + query_response = await coroutine + if not query_response.status_code == 200: + await ctx.send( + "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." + + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" + ) + return + + query, query_uuid, queue_size = ( + query_response.json()["data"][0], + query_response.json()["data"][1], + query_response.json()["data"][2], + ) + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + query_object = QueryControl(username, query_uuid, query, ctx) + OUT_COMM_Q.put(query_object) + await ctx.send( + f"No dobra poszło. Zapamiętaj proszę {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce." + + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni." + ) + + +@client.hybrid_command( + name="glebokie_gardlo", + description="Przygotowuje drinka o nazwie głębokie gardło", + guild=discord.Object(id=664789470779932693), +) +async def wyszukaj_linki_do_dokumentow_deep(ctx): + """ + The function `wyszukaj_linki_do_dokumentow_deep` performs a deep search for links to documents in + a crossref database and provides links to scihub. + + :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating Discord bots. + It represents the context of the command being executed, including information about the message, the server, + and the user who invoked the command. + """ + # TODO: Implement deep search logic here + query = ctx.message.content.replace("$glebokie_gardlo", "") + allowed = False + for role in ctx.message.author.roles: + if role.name == "Bartender": + allowed = True + if role.name == "Scribe": + allowed = True + if role.name == "Thane": + allowed = True + if not allowed: + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + vykidailo = False + bartender = False + prompt = "Przygotuj mi drinka o nazwie Głębokie Gardło, inspirowanego tym sławnym filmem oraz skandalem Watergate" + for role in ctx.message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + if role.name == "Bartender": + bartender = True + global MESSAGE_TABLE # pylint: disable=global-statement + + result, MESSAGE_TABLE = await handle_response( + prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" + ) + if len(result) < 1500: + await ctx.send(result) + else: + while len(result) > 1500: + await ctx.send(result[:1500]) + result = result[1500:] + return + + query_uuid = uuid.uuid4() + # TODO: TESTING ONLY!! + # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') + json_query = { + "UUID": str(query_uuid), + "query": str(query), + "page": 1, + "deep_search": True, + } + coroutine = asyncio.to_thread( + requests.post, + f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", + json=json_query, + timeout=360, + ) + await ctx.send( + "*Conjurer mruga okiem i zamiast drinka wyjmuje dysk usb z terminala. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + + " wrzuca ten dysk do środk* To teraz trza poczekać kilka godzin. TO będzie głębokie wyszukanie" + + " Rozgląda się dookoła i stawia przed tobą mętnego drinka o zapachu mięty, gwiezdnego pyłu i oleju silnikowego" + ) + query_response = await coroutine + if not query_response.status_code == 200: + await ctx.send( + "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." + + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" + ) + return + + query, query_uuid, queue_size = ( + query_response.json()["data"][0], + query_response.json()["data"][1], + query_response.json()["data"][2], + ) + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + query_object = QueryControl(username, query_uuid, query, ctx) + OUT_COMM_Q.put(query_object) + await ctx.send( + f"Wypij wypi {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce niestety kolejka obowiazuje zawsze." + + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzin - ale mogą być też dni. Głębokie zabawy trwają dłużej. *Znowu mruga*" + ) + + + +# *================================== Run +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,))) + 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() From ffeaa9437bd6201404e46c8b9f13c7330f0795b2 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 10:23:04 +0100 Subject: [PATCH 162/283] Deploy script for after refactor --- deploy.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/deploy.sh b/deploy.sh index f934f51..9a97750 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,9 +1,20 @@ #!/bin/bash git pull cd .. -cp ./conjurer/bot.py ./Conjurer/ +cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done cp ./conjurer/settings.json ./Conjurer/ cp ./conjurer/system_gpt_settings.json ./Conjurer/ -cp ./conjurer/install.sh ./Conjurer/ -cp ./conjurer/requirements.txt ./Conjurer/requirements.txt +cp ./conjurer/communication_subroutine.py ./Conjurer/ +cp ./conjurer/voice_recognition_commands.py ./Conjurer/ +cp ./conjurer/ai_commands.py ./Conjurer/ +cp ./conjurer/ai_functions.py ./Conjurer/ +cp ./conjurer/constants.py ./Conjurer/ +cp ./conjurer/librarian_commands.py ./Conjurer/ +cp ./conjurer/music_commands.py ./Conjurer/ +cp ./conjurer/music_functions.py ./Conjurer/ +cp ./conjurer/other_commands.py ./Conjurer/ +cp ./conjurer/other_functions.py ./Conjurer/ +cp ./conjurer/radio_commands.py ./Conjurer/ +cp ./conjurer/thin_client.py ./Conjurer/ + sudo systemctl restart conjurer.service From c6f57bc2aa61e7378389518054d1ded77a7bd360 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 10:26:11 +0100 Subject: [PATCH 163/283] Fix and upgrade --- deploy.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 9a97750..f8b3cbf 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,9 @@ #!/bin/bash git pull -cd .. +cd /home/pi/conjurer || exit +git pull +cp ./deploy.sh ./home/pi/ +cd /home/pi/conjurer || exit cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done cp ./conjurer/settings.json ./Conjurer/ cp ./conjurer/system_gpt_settings.json ./Conjurer/ From 2147de594dac8d7a2232328bb924871c8523a9f7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 10:30:56 +0100 Subject: [PATCH 164/283] bg fx --- deploy.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index f8b3cbf..d5197b6 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,9 +1,8 @@ #!/bin/bash -git pull cd /home/pi/conjurer || exit git pull cp ./deploy.sh ./home/pi/ -cd /home/pi/conjurer || exit +cd /home/pi || exit cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done cp ./conjurer/settings.json ./Conjurer/ cp ./conjurer/system_gpt_settings.json ./Conjurer/ From f81763a865fac5139364f1c7b1094b9bb2d53e0a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 10:32:49 +0100 Subject: [PATCH 165/283] fx --- deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index d5197b6..3793d1e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,7 +1,7 @@ #!/bin/bash cd /home/pi/conjurer || exit git pull -cp ./deploy.sh ./home/pi/ +cp ./deploy.sh /home/pi/ cd /home/pi || exit cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done cp ./conjurer/settings.json ./Conjurer/ From 1b4746aa9d10f7f01bd75848fc7ac6b2b926b994 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 11:02:58 +0100 Subject: [PATCH 166/283] fx --- constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.py b/constants.py index 395bb9d..3c03841 100644 --- a/constants.py +++ b/constants.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime import json from sys import platform from typing import List, Optional, TypedDict From 28fdfa0982ead42e0b3aa9793bb2623be3dfa9a1 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 7 Nov 2024 11:07:19 +0100 Subject: [PATCH 167/283] fx --- thin_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/thin_client.py b/thin_client.py index 7d141de..395a21c 100644 --- a/thin_client.py +++ b/thin_client.py @@ -205,7 +205,6 @@ netrc_mod = netrc.netrc(NETRC_FILE) REMOTE_HOST_NAME = "discord" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) TOKEN = authTokens[2] -openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) REMOTE_HOST_NAME = "spotipy" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) spotify_ctrl = spotipy.Spotify( @@ -218,6 +217,9 @@ spotify_ctrl = spotipy.Spotify( REMOTE_HOST_NAME = "openai" authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) openai.api_key = authTokens[2] +openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) + + # *=========================================== Initializations intents = discord.Intents.default() From 9a0f869839be698301da07f8d1bc9644d362229a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 15:03:44 +0100 Subject: [PATCH 168/283] Main refactoring --- administration_commands.py | 226 ++++ ai_commands.py | 235 +++- ai_functions.py | 268 +++++ communication_subroutine.py | 75 +- constants.py | 42 +- deploy_music.sh | 2 +- install_main_bot.sh | 11 +- librarian_commands.py | 285 +++++ music_commands.py | 582 ++++++++- music_functions.py | 76 +- other_commands.py | 137 ++- other_functions.py | 88 ++ radio_commands.py | 202 +++- requirements_bot copy.txt | 21 + requirements_bot.txt | 5 +- stream_test.py | 55 +- test.py | 20 + test_time.py | 3 +- thin_client.py | 2128 +-------------------------------- voice_recognition_commands.py | 168 ++- 20 files changed, 2349 insertions(+), 2280 deletions(-) create mode 100644 requirements_bot copy.txt create mode 100644 test.py diff --git a/administration_commands.py b/administration_commands.py index e69de29..3e2e42c 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -0,0 +1,226 @@ +import json +import logging +import os +import random +import re +import shutil +from datetime import datetime + +import discord +import numpy as np +from discord.ext import commands, tasks + +from ai_functions import get_random_cyclic_message +from constants import ENCODING, LOGFILE, LOGSTORE, MEMORY_FIVE_MUZYKA, MEMORY_FIVE_SIARA + + +class AdministrationModule(commands.Cog): + def __init__(self, bot, logger_name): + self.bot = bot + self.logger = logging.getLogger(logger_name) + + @commands.hybrid_command( + name="update_banlist", + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", + guild=discord.Object(id=664789470779932693), + ) + async def update_banlist(self, ctx): + """ + Update a banlist in a Discord guild from preprovided list in channel. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + async with ctx.channel.typing(): + allowed = False + for role in ctx.author.roles: + if role.name == "Jarl": + allowed = True + + if ctx.channel.id != 1102190666827702342: + # trunk-ignore(codespell/misspelled) + await ctx.send("Nie wydaje mnie sie") + elif not allowed: + await ctx.send("Idź bo Cię zdziele") + else: + tobancunter = 0 + async with ctx.typing(): + # 1. get list of users posted on channel for banning + channel = ctx.channel + messages = [ + message async for message in channel.history(limit=None) + ] + ids_to_ban = [] + for message in messages: + lines = message.content.split("\n") + for line in lines: + match = re.search( + "\\d{18}", + line, + # trunk-ignore(codespell/misspelled) + ) # poprawilem backslash na podwojny bo linter sie czepial + if match: + ids_to_ban.append(match.group()) + tobancunter += 1 + # 2. get list of already banned users for all servers Conjurer + # has rights to + for guild in self.bot.guilds: + bancunter = 0 + # trunk-ignore(codespell/misspelled) + self.logger.info("Serwer: %s", guild) + current_banlist = [] + permission = True + try: + async for entry in guild.bans(limit=None): + current_banlist.append(entry.user.id) + bancunter += 1 + except BaseException: # pylint: disable=broad-exception-caught + permission = False + if permission: + # 3. compare lists ban only users on list 2 and not on + # list 1 + cunter_counter = 0 + bad_id_cunter_counter = 0 + ban_list = np.setdiff1d(ids_to_ban, current_banlist) + for ident in ban_list: + try: + tmp_user = await self.bot.fetch_user(int(ident)) + await guild.ban( + tmp_user, + reason="Automatyczna lista banów", + delete_message_seconds=0, + ) + cunter_counter += 1 + except discord.NotFound: + self.logger.info( + "Znaleziono uzyszkodnika ktory juz nie ma konta" + ) + bad_id_cunter_counter += 1 + # 4. return message success/failure - how many banned + await ctx.send( + "Obecnie na serwerze {guild} jest {bancunter} zbanowanych użytkowników. Znaleziono na tym kanale {tobancunter} id użytkowników do zbanowania. Zbanowano {cunter_counter} użytkowników. Na liście jest {bad_id_cunter_counter} użytkowników którzy skasowali już konto" + ) + else: + await ctx.send( + f"Nie mam na serwerze {guild} uprawnień do banowania. :()" + ) + await ctx.send("Zrobione tak czy inaczej") + + async def archive_channel(self, channel_no): + """ + The function `archive_channel` retrieves messages from a specified channel, processes them, and + saves the data in a JSON file with a timestamp in the filename. + + :param channel_no: The `archive_channel` function you provided seems to be incomplete. It looks like + you are trying to archive messages from a Discord channel into a JSON file + """ + channel = self.bot.get_channel(channel_no) + messages = [message async for message in channel.history(limit=None)] + path_newfile = f"{LOGSTORE}channeldump_{channel_no}_{datetime.now()}.json" + new_data = [] + for message in messages: + new_data.append(message) + with open(path_newfile, "x", encoding=ENCODING) as new_file: + new_file.seek(0) + json.dump(new_data, new_file, indent=4) + + @tasks.loop(seconds=120) + async def check_self(self): + """ + The function `check_data` periodically checks for conditions to send messages and manage log files + in a Discord channel. + """ + # logger.info("Heartbeat of cleanup proc") + channel = self.bot.get_channel(1062047367337095268) + messages = [message async for message in channel.history(limit=1)] + for mess in messages: + channel = mess.channel + if os.path.getsize(LOGFILE) > 60000000: + await channel.send( + "*Conjurer porządkuje bar, wypala szklanki do czysta miotaczem płomieni ze swojej zbroi i ogólnie wygląda na zajętego....*" + ) + async with channel.typing(): + shutil.copyfile(LOGFILE, f"{LOGSTORE}discord{datetime.now()}") + self.logger.info("Log rollover") + handlers = self.logger.handlers + for handler in handlers: + if isinstance(handler, logging.handlers.RotatingFileHandler): + handler.doRollover() + await channel.send( + "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* ...sać powieści o naszym wspaniałym barze SZEFIE!" + ) + + if os.path.getsize(MEMORY_FIVE_MUZYKA) > 3000000: + await channel.send( + "*Conjurer porządkuje bar, wypala szklanki do czysta miotaczem płomieni ze swojej zbroi i ogólnie wygląda na zajętego....*" + ) + async with channel.typing(): + path_newfile = f"{LOGSTORE}pamiec_muzyki{datetime.now()}.json" + self.logger.info(path_newfile) + with open( + MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING + ) as file_music_memory: + with open(path_newfile, "x", encoding=ENCODING) as new_file: + # First we load existing data into a dict. + file_data = json.load(file_music_memory) + new_data = [] + new_data.append(file_data[0]) + new_data.extend(file_data[:-20]) + file_music_memory.truncate(0) + file_music_memory.seek(0) + # convert back to json. + new_file.seek(0) + json.dump(new_data, file_music_memory, indent=4) + json.dump(file_data, new_file, indent=4) + await channel.send( + "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* ...sać powieści o naszym wspaniałym barze SZEFIE!" + ) + if os.path.getsize(MEMORY_FIVE_SIARA) > 3000000: + path_newfile = f"{LOGSTORE}pamiec_rozmow{datetime.now()}.json" + self.logger.info(path_newfile) + await channel.send( + "*Conjurer porządkuje bar, ścina lekkim laserem pulsacyjnym powierzchnie baru o grubości kilku mikronów i ogólnie wygląda na zajętego....*" + ) + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file: + with open(path_newfile, "x", encoding=ENCODING) as new_file: + # First we load existing data into a dict. + file_data = json.load(file) + new_data = [] + new_data.append(file_data[0]) + new_data.extend(file_data[-20]) + file.truncate(0) + file.seek(0) + new_file.seek(0) + # convert back to json. + json.dump(new_data, file, indent=4) + json.dump(file_data, new_file, indent=4) + await channel.send( + "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* eprzyć o głupotach z klientami... Szefie... *Bierze 'ukradkowy' łyk z piersiówki*" + ) + global LAST_SPONTANEOUS_CALL + global TIME_BETWEEN_CALLS + tdelta = datetime.now() - LAST_SPONTANEOUS_CALL + tdelta = tdelta.total_seconds() + if tdelta > TIME_BETWEEN_CALLS: + self.logger.info("Spontaneous call") + # trunk-ignore(bandit/B311) + TIME_BETWEEN_CALLS = random.randint( + 10200, 272800 + ) # temp random, set after each call + self.logger.debug(TIME_BETWEEN_CALLS) + LAST_SPONTANEOUS_CALL = datetime.now() + async with channel.typing(): + message = await get_random_cyclic_message() + self.logger.info("Odpowiedz") + self.logger.info(message) + await channel.send(message) + + +async def setup(bot): + logger = logging.getLogger("discord") + am = AdministrationModule(bot, "discord") + await bot.add_cog(am) + am.check_self.start() + logger.info("Loading administration commands module done") diff --git a/ai_commands.py b/ai_commands.py index 0369fef..b68aeeb 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -1 +1,234 @@ -#ai command cogs \ No newline at end of file +# ai command cogs +import logging +import re +import sys +from datetime import datetime + +import discord +import openai +import requests +from discord.ext import commands + +from ai_functions import handle_response +from constants import ( + DATA, + GRAPHICS_PATH, + INITIAL_TIME_WAIT, + MASTER_TIMEOUT, + OPENAICLIENT, +) + + +class Events(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.logger = logging.getLogger("discord") + + @commands.Cog.listener() + async def on_ready(self): + print("Ready!") + print("Logged in as ---->", self.bot.user) + print("ID:", self.bot.user.id) + + @commands.Cog.listener() + async def on_message(self, message): + """ + Handle incoming messages in a Discord server, perform various + checks and actions based on the content and context of the message, and respond accordingly. + + :param message: The message object that is received when a user sends a message in a Discord server + or DM. The code is checking various conditions and performing actions based on the content of the + message and the context in which it was sent. It also includes TODOs for future improvements + :return: The function `on_message` is being returned. + """ + vykidailo = False + channel = None + if message.author == self.bot.user: + return + if isinstance(message.author, discord.Member): + for role in message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + + if ("Conjurer Śpij Słodko Aniołku" in message.content) and vykidailo: + sys.exit() + # kanal bez bota + if message.channel.id == 1095985579147141202: + return + # wentylacja + if message.channel.id == 1083804024173764739: + return + # legendy + if message.channel.id == 1084448332841230388: + return + # interrogation booth + if message.channel.id == 1111625221171052595: + return + if isinstance(message.channel, discord.DMChannel): + if message.author.id == 346956223645614080: + await message.reply("Witam witam") + return + else: + channel = self.bot.get_channel(1064888712565100614) + await channel.send("Słyszałem ja żem że: " + message.content) + return + channel = message.channel + await self.bot.process_commands(message) + + message.content = message.content.lower() + + tdelta = datetime.now() - MASTER_TIMEOUT + tdelta = tdelta.total_seconds() + if "opowiedz o fabryczce" in message.content: + await message.reply(DATA["fabryczka"]) + if "opowiedz mi o fabryczce" in message.content: + await message.reply(DATA["fabryczka"]) + + if tdelta > INITIAL_TIME_WAIT: + for word in self.word_reactions: + if re.search(r"\b" + word + r"\b", message.content): + tdelta = datetime.now() - self.word_reactions[word][2] + tdelta = tdelta.total_seconds() + security_clearance = self.word_reactions[word][4] + reaction = self.word_reactions[word][3] + if tdelta > self.word_reactions[word][1]: + # TODO: to zrobic reactiony + self.logger.info("Ping z procedury reakcji") + if reaction: + emoji = self.bot.get_emoji(self.word_reactions[word][0]) + await message.add_reaction(emoji) + elif security_clearance and vykidailo: + await message.reply(self.word_reactions[word][0]) + elif not security_clearance: + await message.reply(self.word_reactions[word][0]) + self.word_reactions[word][2] = datetime.now() + + # TODO: drobne literówki, mentiony, spacja przed dwukropkiem. napraw. + kondziu_mentioned = False + for mention in message.mentions: + if mention == self.bot.user: + kondziu_mentioned = True + + if kondziu_mentioned or "conjurer:" in message.content: + async with channel.typing(): + self.logger.debug("Procedura chatu") + + message.content = message.content.replace("conjurer: ", "") + if message.author.nick: + username = message.author.nick + else: + username = message.author.name + vykidailo = False + bartender = False + if kondziu_mentioned: + prompt = message.clean_content + else: + prompt = message.content + for role in message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + if role.name == "Bartender": + bartender = True + global MESSAGE_TABLE # pylint: disable=global-statement + + result, MESSAGE_TABLE = await handle_response( + prompt, + vykidailo, + bartender, + MESSAGE_TABLE, + username, + "CONVERSATION", + ) + + if len(result) < 1500: + await message.reply(result) + else: + while len(result) > 1500: + await message.reply(result[:1500]) + result = result[1500:] + if "imaginuje sobie:" in message.content: + async with channel.typing(): + self.logger.info("Poczatek procedury obrazkowej") + message.content = message.content.replace("imaginuje sobie: ", "") + self.logger.debug("Wywolanie obrazka: %s", message.content) + try: + response = await OPENAICLIENT.images.generate( + model="dall-e-3", + prompt=message.content, + size="1024x1024", + quality="standard", + n=1, + ) + except openai.APITimeoutError as e: + # Handle timeout error, e.g. retry or log + await message.reply( + f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" + ) + except openai.APIConnectionError as e: + await message.reply( + f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" + ) + except openai.BadRequestError as e: + # Handle invalid request error, e.g. validate parameters or log + if message.author.nick: + username = message.author.nick + else: + username = message.author.name + resp, _ = await handle_response( + f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie '{message.content}'", + True, + True, + MESSAGE_TABLE, + username, + "RANDOM", + ) + await message.reply( + f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + ) + except openai.AuthenticationError as e: + # Handle authentication error, e.g. check credentials or log + await message.reply( + f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" + ) + except openai.PermissionDeniedError as e: + # Handle permission error, e.g. check scope or log + await message.reply( + ( + f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" + ) + ) + except openai.RateLimitError as e: + await message.reply( + f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" + ) + except openai.APIError as e: + # Handle API error, e.g. retry or log + await message.reply( + f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" + ) + if response: + self.logger.info(response) + image_url = response.data[0].url + image_desc = response.data[0].revised_prompt + self.logger.debug("Wynikowy obrazek pod url: %s", image_url) + response = requests.get(image_url, timeout=360) + + temp_file_name = message.content + ".png" + temp_file_name = GRAPHICS_PATH + message.content + ".png" + + with open(temp_file_name, "wb") as dalle_file: + dalle_file.write(response.content) + self.logger.info("Koniec procedury obrazkowej.") + fnord = discord.File( + temp_file_name, spoiler=False, description=message.content + ) + await message.reply(f"{image_desc}", file=fnord) + + +# *=========================================== Define Functions + + +async def setup(bot): + logger = logging.getLogger("discord") + await bot.add_cog(Events(bot)) + logger.info("Loading ai events module done") diff --git a/ai_functions.py b/ai_functions.py index b281726..0e738d7 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -1,6 +1,23 @@ +import asyncio +import json +import logging +import random +import openai import tiktoken +from constants import ( + CYCLIC_WORDS, + ENCODING, + GPT_SETTINGS, + MEMORY_FIVE_MUZYKA, + MEMORY_FIVE_SIARA, + MESSAGE_TABLE, + MESSAGE_TABLE_MUZYKA, + OPENAICLIENT, + WORD_REACTIONS, +) + def num_tokens_from_string(message, model): """ @@ -24,3 +41,254 @@ def num_tokens_from_string(message, model): num_tokens += tokens_per_name num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> return num_tokens + + +async def handle_response( + prompt, vykidailo, bartender, history, username, request_type +): + """ + Handle responses by appending them to a history, use OpenAI to + generate a response, and then append the generated response to the history. + + :param prompt: The prompt for the OpenAI chatbot to generate a response to + :param vykidailo: It is a boolean variable that indicates whether the user invoking the function is + an administrator or not + :param bartender: The bartender parameter is a boolean value indicating whether the user making the + request is a bartender or not + :param history: A list containing the conversation history between the user and the assistant + :param username: The username of the user who initiated the conversation + :param music: The "music" parameter is a boolean value that indicates whether the conversation is + related to music or not. If it is True, the conversation history will be stored in a different file + and the response will be generated using a different model + :return: The function `handle_response` returns a tuple containing the `result` and `MESSAGE_TABLE`. + """ + # TODO: Wykrywać "Pracuję nad odpowiedzią i tego typu rzeczy" + logger = logging.getLogger("discord") + logger.info("Wywolanie procedury openai z promptem: %s", prompt) + temp = {"role": "user", "content": username + ":" + prompt} + if vykidailo or bartender: + logger.info("Administrator coś chciał") + history.append(temp) + if request_type == "MUSIC": + with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: + # First we load existing data into a dict. + file_data = json.load(file_music_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_music_memory.seek(0) + # convert back to json. + json.dump(file_data, file_music_memory, indent=4) + elif request_type == "RANDOM": + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + else: + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + history = [] + history.append(GPT_SETTINGS[0]) + chat_gpt_config_request_size = num_tokens_from_string(GPT_SETTINGS[0], "gpt-4") + + for slowo, reakcja in WORD_REACTIONS.items(): + if not reakcja[3]: + content = ( + "Kiedy słyszysz " + + slowo + + " to reagujesz lub dzieje się to " + + reakcja[0] + ) + temp = {"role": "system", "content": content} + chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4") + history.append(temp) + + final_prompt = username + ":" + prompt + logger.debug( + "Rozmiar zapytania przed dodaniem historii %s", chat_gpt_config_request_size + ) + if request_type == "MUSIC": + algorithm = "gpt-4o" + table = MESSAGE_TABLE_MUZYKA + token_amount = 10700 + elif request_type == "RANDOM": + algorithm = "gpt-4o" + table = MESSAGE_TABLE + token_amount = 10700 + else: + table = MESSAGE_TABLE + algorithm = "gpt-4o" + token_amount = 10700 + + prompt_gpt_request_size = num_tokens_from_string( + {"role": "user", "content": final_prompt}, "gpt-4" + ) + temptable = [] + for i in reversed(table): + temp_token = num_tokens_from_string(i, "gpt-4") + logger.debug( + "Rozmiar zapytania %s prompt %s temp %s", + chat_gpt_config_request_size, + prompt_gpt_request_size, + temp_token, + ) + if ( + chat_gpt_config_request_size + < token_amount + prompt_gpt_request_size + temp_token + ): + temptable.insert(1, i) + chat_gpt_config_request_size += temp_token + history.extend(temptable) + temp = {"role": "user", "content": final_prompt} + history.append(temp) + logger.info("Rozmiar zapytania po wyslaniu %s", chat_gpt_config_request_size) + try: + response = await OPENAICLIENT.chat.completions.create( + model=algorithm, messages=history + ) + except openai.APITimeoutError as e: + # Handle timeout error, e.g. retry or log + result = f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" + except openai.APIConnectionError as e: + result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" + except openai.BadRequestError as e: + # Handle invalid request error, e.g. validate parameters or log + resp, _ = await handle_response( + f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'", + True, + True, + MESSAGE_TABLE, + username, + "RANDOM", + ) + result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + except openai.APIResponseValidationError as e: + # Handle invalid request error, e.g. validate parameters or log + resp, _ = await handle_response( + f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'", + True, + True, + MESSAGE_TABLE, + username, + "RANDOM", + ) + result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + except openai.AuthenticationError as e: + # Handle authentication error, e.g. check credentials or log + result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" + except openai.PermissionDeniedError as e: + # Handle permission error, e.g. check scope or log + result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" + except openai.RateLimitError as e: + result = f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" + except openai.UnprocessableEntityError as e: + result = f"*Kondziu patrzy na terminal. Potem na to co każesz mu wysłać....* Ja wiem że jesteśmy w barze BDSM - ale nie da się włożyć TEGO w TO. *Za jego plecami na terminalu pojawia się:* {e}" + except openai.APIError as e: + # Handle API error, e.g. retry or log + result = f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" + + logger.info("Historia wysłana:") + logger.info(history) + await asyncio.sleep(15) + result = "" + logger.debug("Odpowiedzi") + logger.info(response) + logger.debug(response.choices) + for choice in response.choices: + result += choice.message.content + logger.info("Sformatowane odpowiedzi") + logger.info(result) + temp = {"role": "assistant", "content": result} + history.append(temp) + if request_type == "MUSIC": + with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: + # First we load existing data into a dict. + file_data = json.load(file_music_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_music_memory.seek(0) + # convert back to json. + json.dump(file_data, file_music_memory, indent=4) + elif request_type == "RANDOM": + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + else: + with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: + # First we load existing data into a dict. + file_data = json.load(file_memory) + # Join new_data with file_data inside emp_details + file_data.append(temp) + file_memory.seek(0) + # convert back to json. + json.dump(file_data, file_memory, indent=4) + return result, MESSAGE_TABLE + + +async def get_random_cyclic_message(client): + """ + The function `get_random_cyclic_message` returns a random cyclic message from a list of cyclic + words. + :return: a random cyclic message from the list `cyclic_words`. + """ + logger = logging.getLogger("discord") + channel_id = 1062047367337095268 + channel = client.get_channel(channel_id) + # trunk-ignore(bandit/B311) + ai_check = random.randint(0, 10) + logger.info("Losowa wypowiedź") + if ai_check < 2: + logger.info("Predefiniowana") + # trunk-ignore(bandit/B311) + messnum = random.randint(0, len(CYCLIC_WORDS)) + logger.debug(messnum) + logger.debug(len(CYCLIC_WORDS)) + mess_key = list(CYCLIC_WORDS.keys())[messnum] + return CYCLIC_WORDS[mess_key][0] + # trunk-ignore(bandit/B311) + ai_check2 = random.randint(0, 10) + global MESSAGE_TABLE + if ai_check2 < 6: + logger.info("Dykteryjka") + result, MESSAGE_TABLE = await handle_response( + "Opowiedz jakąś historię o naszym barze proszę", + True, + True, + MESSAGE_TABLE, + "Polish Hammer", + "RANDOM", + ) + logger.info(result) + else: + logger.info("Wtracenie w dyskusje") + messages = [message async for message in channel.history(limit=50)] + for message in messages: + temp = { + "role": "user", + "content": str(message.author) + ":" + str(message.content), + } + MESSAGE_TABLE.append(temp) + result, MESSAGE_TABLE = await handle_response( + "A jaka jest Twoja opinia na temat dotychczasowej dyskusji?", + True, + True, + MESSAGE_TABLE, + "Polish Hammer", + "RANDOM", + ) + logger.info(result) + return result diff --git a/communication_subroutine.py b/communication_subroutine.py index bc44fff..41c8e17 100644 --- a/communication_subroutine.py +++ b/communication_subroutine.py @@ -1,11 +1,11 @@ -import logging -import threading import json -import time +import logging import re +import threading +import time +from queue import Empty, Queue from urllib import request as urequest -from queue import Queue, Empty from flask import Flask, jsonify, request from waitress import serve @@ -14,28 +14,30 @@ PORT_ADDRESS = 5000 ICECAST_ADDRESS = "http://192.168.1.15:8000" OUT_COMM_Q = Queue() IN_COMM_Q = Queue() -SRCHTITLE = re.compile(br'StreamTitle=\\*(?P<title>[^;]*);').search +SRCHTITLE = re.compile(rb"StreamTitle=\\*(?P<title>[^;]*);").search awaiting_q = [] incoming_q = Queue() app = Flask(__name__) PREPPED_TRACKS = { - "requests" : "", - "hit" : "", + "requests": "", + "hit": "", "all": "", - "priority":"", - "jingles":"", - "now_playing":"", + "priority": "", + "jingles": "", + "now_playing": "", "next": "", - "meta" : "" + "meta": "", } logger = logging.getLogger("discord") + class QueryControl: """ This class `QueryControl` is used to manage queries with information about the author, UUID, content, logger, context, and replies. """ + def __init__(self, query_author, query_uuid, query_content, ctx) -> None: self.author = query_author self.uuid = query_uuid @@ -48,6 +50,7 @@ class QueryControl: ) self.replies = [] + @app.route("/prepped_tracks", methods=["POST"]) def log_radio_tracks(): app.logger = logging.getLogger("discord") @@ -59,7 +62,14 @@ def log_radio_tracks(): metadata = id3(ICECAST_ADDRESS) PREPPED_TRACKS["now_playing"] = PREPPED_TRACKS["next"] if metadata: - PREPPED_TRACKS["meta"] = metadata["name"] + " - " + metadata["title"] + "(" + metadata["genre"] + ")" + PREPPED_TRACKS["meta"] = ( + metadata["name"] + + " - " + + metadata["title"] + + "(" + + metadata["genre"] + + ")" + ) else: PREPPED_TRACKS["meta"] = "Nie znaju" PREPPED_TRACKS[record[0]] = record[1] @@ -83,6 +93,7 @@ def answer_external_command(): incoming_q.put(record) return jsonify("SUCCESS") + @app.route("/conjurer", methods=["GET"]) def check_alive(): """ @@ -94,6 +105,7 @@ def check_alive(): """ return jsonify("ALIVE") + def flask_debug(): """ The `flask_debug` function starts a Flask application in debug mode without using the reloader. @@ -116,6 +128,7 @@ def waitress_run(): logger.info("Attempt waitress") serve(app, host=HOST_ADDRESS, port=PORT_ADDRESS) + def scan_queue(): """ The function `scan_queue` reads data from a queue, logs it, and appends it to another queue. @@ -131,6 +144,7 @@ def scan_queue(): logger.info(data) awaiting_q.append(data) + def scan_incoming(): """ The `scan_incoming` function continuously checks for incoming data, processes it, and logs when data @@ -162,26 +176,31 @@ def scan_incoming(): except Empty: time.sleep(1) -def get_stream_title(tag:bytes) -> str: - title = '' + +def get_stream_title(tag: bytes) -> str: + title = "" if m := SRCHTITLE(tag): - #decode, strip, unescape and remove surrounding quotes (may not even be the same type of quote) - title = m.group('title').decode('utf-8').strip().replace('\\', '')[1:-1] + # decode, strip, unescape and remove surrounding quotes (may not even be the same type of quote) + title = m.group("title").decode("utf-8").strip().replace("\\", "")[1:-1] return title -def id3(url:str) -> dict: - request = urequest.Request(url, headers={'Icy-MetaData': 1}) + +def id3(url: str) -> dict: + request = urequest.Request(url, headers={"Icy-MetaData": 1}) with urequest.urlopen(request) as resp: - metaint = int(resp.headers.get('icy-metaint', '-1')) - if metaint<0: + metaint = int(resp.headers.get("icy-metaint", "-1")) + if metaint < 0: return False - resp.read(metaint) #this isn't seekable so, arbitrarily read to the point we want + resp.read( + metaint + ) # this isn't seekable so, arbitrarily read to the point we want tagdata = dict( - site_url = resp.headers.get('icy-url' ) , - name = resp.headers.get('icy-name' ).title(), - genre = resp.headers.get('icy-genre').title(), - title = get_stream_title(resp.read(255)) ) + site_url=resp.headers.get("icy-url"), + name=resp.headers.get("icy-name").title(), + genre=resp.headers.get("icy-genre").title(), + title=get_stream_title(resp.read(255)), + ) return tagdata @@ -193,20 +212,20 @@ def comm_subroutine(): object that is used for logging messages at various levels (e.g., debug, info, warning, error). In the provided code snippet, the logger is used to log messages at the "info" level """ - #logger.setLevel(logging.DEBUG) + # logger.setLevel(logging.DEBUG) logger = logging.getLogger("discord") logger.info("Started comms") threads = [] - #threads.append(threading.Thread(target=flask_debug)) + # 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)) - for worker in threads: worker.start() for worker in threads: worker.join() + if __name__ == "__main__": comm_subroutine() diff --git a/constants.py b/constants.py index 3c03841..267a3b3 100644 --- a/constants.py +++ b/constants.py @@ -1,8 +1,13 @@ -from datetime import datetime import json +import netrc +from datetime import datetime +from platform import uname from sys import platform from typing import List, Optional, TypedDict -from platform import uname + +import openai +import spotipy +from spotipy.oauth2 import SpotifyClientCredentials Music_Config = TypedDict( "Music_Config", @@ -14,7 +19,6 @@ Music_Config = TypedDict( ) - # *=========================================== Predefines MASTER_TIMEOUT = datetime.now() INITIAL_TIME_WAIT = 500 @@ -99,3 +103,35 @@ elif platform == "win32": 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], + ) +) + +WORD_REACTIONS = DATA["word_reactions"] +CYCLIC_WORDS = DATA["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) + +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) diff --git a/deploy_music.sh b/deploy_music.sh index 59873cb..7914d31 100755 --- a/deploy_music.sh +++ b/deploy_music.sh @@ -1,5 +1,5 @@ #!/bin/bash -cd ./conjurer +cd ./conjurer || exit git pull cd .. cp ./conjurer/conjurer_musician/conjurer_musician.py ./Conjurer diff --git a/install_main_bot.sh b/install_main_bot.sh index 4eb0f03..ac58dcb 100755 --- a/install_main_bot.sh +++ b/install_main_bot.sh @@ -1,11 +1,12 @@ #!/bin/bash sudo apt-get install python3-dev -cd /home/pi +cd /home/pi || exit mdkir Conjurer -cd Conjurer -python3 -m venv /home/pi/Conjurer/env +cd Conjurer ||exit +python3 -m venv /home/pi/Conjurer/.venv cp /home/pi/conjurer/requirements_bot.txt /home/pi/Conjurer/ -source ./env/bin/activate +# trunk-ignore(shellcheck/SC1091) +source /home/pi/Conjurer/.venv/bin/activate ./env/bin/python3 -m pip install --upgrade pip ./env/bin/python3 -m pip install -r requirements_bot.txt sed -i -e 's/os.rename/shutil.copy/g' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py @@ -14,7 +15,7 @@ sed -i '1i\import logging' ./env/lib/python3.11/site-packages/spotify_dl/spotify sed -i '21i\ logger = logging.getLogger("discord")' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py sed -i '22i\ logger.info("Playlist")' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py - +#add sed changing signal registration to catch exception in spotify dl init deactivate sudo cp ./conjurer.service /etc/systemd/system/ sudo systemctl daemon-reload diff --git a/librarian_commands.py b/librarian_commands.py index e69de29..1210331 100644 --- a/librarian_commands.py +++ b/librarian_commands.py @@ -0,0 +1,285 @@ +import asyncio +import io +import logging +import os +import random +import uuid +from queue import Empty + +import discord +import pdf2image +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 + + +class DataModule(commands.Cog): + def __init__(self, bot, logger_name): + self.bot = bot + self.logger = logging.getLogger(logger_name) + + @commands.hybrid_command( + nsfw=True, + name="get_image_sadox", + description="Wyświetla losową stronę z losowego komiksu FanSadox. Bardzo NSFW.", + guild=discord.Object(id=664789470779932693), + ) + async def get_image_sadox(self, ctx): + """ + Take in a context parameter and retrieve an image related from fansadox collection. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. In this case, it is likely being used + to determine + """ + self.logger.info("Get sadox") + channel = ctx.message.channel + async with channel.typing(): + # select random file + res = [] + # Iterate directory + for path in os.listdir(DIR_PATH_SADOX): + # check if current path is a file + if os.path.isfile(os.path.join(DIR_PATH_SADOX, path)): + res.append(path) + # trunk-ignore(bandit/B311) + filename = res[random.randrange(0, len(res) - 1)] + # select random page + file = open(DIR_PATH_SADOX + filename, "rb") + readpdf = PyPDF2.PdfReader(file) + totalpages = len(readpdf.pages) + # trunk-ignore(bandit/B311) + page = random.randrange(1, totalpages) + # convert page to image + image = pdf2image.convert_from_path( + DIR_PATH_SADOX + filename, first_page=page, last_page=page + ) + byte_io_stream = io.BytesIO() + image[0].save(byte_io_stream, "JPEG") + byte_io_stream.seek(0) + byte_io_stream.name = "image.jpg" + file = discord.File(byte_io_stream) + await ctx.send(file=file) + self.logger.info("Get sadox completed") + + # TODO: LIBRARIAN + @tasks.loop(seconds=3) + async def check_data_q(self): + """ + This function checks the data queue for any new entries and processes them accordingly. + """ + try: + fresh_data = IN_COMM_Q.get(block=False) + entries = [] + if fresh_data.stop: + searcher = fresh_data.author + query = fresh_data.content + l_p = 1 + for doi in fresh_data.entries: + self.logger.info(doi) + desc = fresh_data.entries[doi] + title = desc["Title"][0] + entries.append( + f"{l_p}. {title} pod linkiem https://www.sci-hub.se/{doi} i jest to {desc['type']}\n" + ) + l_p += 1 + message = "*Z podłogi wysuwa się winda na książki*" + if fresh_data.ctx is not None: + ctx = fresh_data.ctx + message += f" Dzielny poszukiwaczu @{searcher} w odpowiedzi na twoje zapytanie {query} nr {fresh_data.uuid}" + else: + ctx = self.bot.get_channel(1062047571557744721) + message += f" Mam wynik dla zapytania {fresh_data.uuid} ale że Hammer coś grzebał nie pamiętam kto to chciał i co chciał" + + message += "nasza biblioteka służy Ci odpowiedzią. Przy dźwięku fanfar winda się otwiera a w środku " + if len(entries) < 1: + message += "niestety nie ma nic" + await ctx.send(message) + elif len(entries) >= 1 and len(entries) < 5: + message += " znajduje się coś:\n" + for item in entries: + message += item + await ctx.send(message) + else: + message += " znajduje się cholernie dużo:\n" + for item in entries: + message += item + if len(message) > 1500: + await ctx.send(message) + message = "" + + # Kept for sentimental reasons + # await ctx.send(f"O. A tak będzie wyglądało coś ciekawego w przyszłości: {data}") + except Empty: + pass + + # TODO: LIBRARIAN + @commands.hybrid_command( + name="wyszukaj_linki_do_dokumentow", + description="Szuka linkow doi w bazie crossref i podaje linki do scihuba", + guild=discord.Object(id=664789470779932693), + ) + async def wyszukaj_linki_do_dokumentow(self, ctx): + """ + The function `wyszukaj_linki_do_dokumentow` searches for links to documents in a crossref database and provides links to scihub. + + :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating Discord bots. + It represents the context of the command being executed, including information about the message, the server, + and the user who invoked the command. + """ + query = ctx.message.content + query_uuid = uuid.uuid4() + # TODO: TESTING ONLY!! + # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') + ctx.message.content = ctx.message.content.replace( + "$wyszukaj_linki_do_dokumentow", "" + ) + + json_query = { + "UUID": str(query_uuid), + "query": str(query), + "page": 1, + "deep_search": False, + } + coroutine = asyncio.to_thread( + requests.post, + f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", + json=json_query, + timeout=360, + ) + await ctx.send( + "*Conjurer powoli notuje podane przez Ciebie. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + + " wrzuca twój liścik do środka* To teraz trza poczekać kilka godzin. Biblioteka to 3/4 stacji." + + " A przecież pół stacji to browar, motelik dla zabaw cielesnych i ten Bar. Więc 12/8 stacji teraz będzie ciężko pracować" + ) + query_response = await coroutine + if not query_response.status_code == 200: + await ctx.send( + "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." + + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" + ) + return + + query, query_uuid, queue_size = ( + query_response.json()["data"][0], + query_response.json()["data"][1], + query_response.json()["data"][2], + ) + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + query_object = QueryControl(username, query_uuid, query, ctx) + OUT_COMM_Q.put(query_object) + await ctx.send( + f"No dobra poszło. Zapamiętaj proszę {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce." + + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni." + ) + + # TODO: LIBRARIAN + @commands.hybrid_command( + name="glebokie_gardlo", + description="Przygotowuje drinka o nazwie głębokie gardło", + guild=discord.Object(id=664789470779932693), + ) + async def wyszukaj_linki_do_dokumentow_deep(ctx): + """ + The function `wyszukaj_linki_do_dokumentow_deep` performs a deep search for links to documents in + a crossref database and provides links to scihub. + + :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating Discord bots. + It represents the context of the command being executed, including information about the message, the server, + and the user who invoked the command. + """ + # TODO: Implement deep search logic here + query = ctx.message.content.replace("$glebokie_gardlo", "") + allowed = False + for role in ctx.message.author.roles: + if role.name == "Bartender": + allowed = True + if role.name == "Scribe": + allowed = True + if role.name == "Thane": + allowed = True + if not allowed: + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + vykidailo = False + bartender = False + prompt = "Przygotuj mi drinka o nazwie Głębokie Gardło, inspirowanego tym sławnym filmem oraz skandalem Watergate" + for role in ctx.message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + if role.name == "Bartender": + bartender = True + global MESSAGE_TABLE # pylint: disable=global-statement + + result, MESSAGE_TABLE = await handle_response( + prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" + ) + if len(result) < 1500: + await ctx.send(result) + else: + while len(result) > 1500: + await ctx.send(result[:1500]) + result = result[1500:] + return + + query_uuid = uuid.uuid4() + # TODO: TESTING ONLY!! + # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') + json_query = { + "UUID": str(query_uuid), + "query": str(query), + "page": 1, + "deep_search": True, + } + coroutine = asyncio.to_thread( + requests.post, + f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", + json=json_query, + timeout=360, + ) + await ctx.send( + "*Conjurer mruga okiem i zamiast drinka wyjmuje dysk usb z terminala. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" + + " wrzuca ten dysk do środk* To teraz trza poczekać kilka godzin. TO będzie głębokie wyszukanie" + + " Rozgląda się dookoła i stawia przed tobą mętnego drinka o zapachu mięty, gwiezdnego pyłu i oleju silnikowego" + ) + query_response = await coroutine + if not query_response.status_code == 200: + await ctx.send( + "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." + + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" + ) + return + + query, query_uuid, queue_size = ( + query_response.json()["data"][0], + query_response.json()["data"][1], + query_response.json()["data"][2], + ) + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + query_object = QueryControl(username, query_uuid, query, ctx) + OUT_COMM_Q.put(query_object) + await ctx.send( + f"Wypij wypi {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce niestety kolejka obowiazuje zawsze." + + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzin - ale mogą być też dni. Głębokie zabawy trwają dłużej. *Znowu mruga*" + ) + + +async def setup(bot): + logger = logging.getLogger("discord") + dm = DataModule(bot, "discord") + dm.check_data_q.start() + await bot.add_cog(dm) + logger.info("Loading data sharing commands module done") diff --git a/music_commands.py b/music_commands.py index e3438a5..4a1b7f1 100644 --- a/music_commands.py +++ b/music_commands.py @@ -1,11 +1,20 @@ -import music_functions -from constants import MUZYKA +import asyncio import logging -import discord -from discord.ext import commands -from communication_subroutine import PREPPED_TRACKS -from sys import platform +import random import re +from datetime import datetime +from sys import platform + +import discord +import eyed3 +from discord.ext import commands, tasks + +import music_functions +from ai_functions import handle_response +from communication_subroutine import PREPPED_TRACKS +from constants import MESSAGE_TABLE_MUZYKA, MUZYKA, SEPARATOR_FILE_PATH +from other_functions import get_stats + class MusicModule(commands.Cog): def __init__(self, bot, logger_name): @@ -37,10 +46,14 @@ class MusicModule(commands.Cog): content = ctx.message.content.split() for item_yt in content: if re.match("http.*", item_yt): - sciezka, files = await music_functions.get_file(ctx, "Pornol", item_yt) + sciezka, files = await music_functions.get_file( + ctx, "Pornol", item_yt + ) if files: self.logger.info("Pornol udany") - await ctx.send(f"Jest w tajnym archiwum pod adresem{sciezka})") + await ctx.send( + f"Jest w tajnym archiwum pod adresem{sciezka})" + ) else: await ctx.reply("Jak ładnie szefa poprosisz.") @@ -49,7 +62,7 @@ class MusicModule(commands.Cog): description="Wyswietl kawalki ktore sa na pocztku list radia", guild=discord.Object(id=664789470779932693), ) - async def co_na_plejliscie_wariacie(self,ctx): + async def co_na_plejliscie_wariacie(self, ctx): """ Download a music number from youtube. @@ -62,14 +75,33 @@ class MusicModule(commands.Cog): async with ctx.typing(): self.logger.info("plejlista") self.logger.info(PREPPED_TRACKS) - await ctx.send(f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} (wg metadanych: {PREPPED_TRACKS['meta']}) \n- następny będzie{(PREPPED_TRACKS['next']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}") + await ctx.send( + f"Didżej Hammer i jego sztuczna inteligencja (||he he||) prezentują: \n- obecnie grany jest {(PREPPED_TRACKS['now_playing']).rstrip()} (wg metadanych: {PREPPED_TRACKS['meta']}) \n- następny będzie{(PREPPED_TRACKS['next']).rstrip()} \n- na liście priorytetowej {(PREPPED_TRACKS['priority']).rstrip()} \n- na liście hitów obecnie czeka kawałek {(PREPPED_TRACKS['hit']).rstrip()} \n- na liście wszystkich kawałków {(PREPPED_TRACKS['all']).rstrip()} \n- najświeższym zamówieniem od słuchaczy jest {(PREPPED_TRACKS['requests']).rstrip()} \nZ kolei jingiel to: {(PREPPED_TRACKS['jingles']).rstrip()}" + ) + + @tasks.loop(seconds=1) + async def check_music(self): + """ + This Python function continuously checks for music playback in a voice client and plays music if + conditions are met. + """ + if self.bot.voice_clients: + voice_client = self.bot.voice_clients[0] + if ( + voice_client + and voice_client.is_connected() + and not voice_client.is_playing() + and not voice_client.is_paused() + ): + await self.play(MUZYKA["ctx"], MUZYKA["requester"]) + await asyncio.sleep(2) @commands.hybrid_command( name="dej_co_z_jutuba", description="Podaj link do youtube - sciagnie i doda muzyke", guild=discord.Object(id=664789470779932693), ) - async def dej_co_z_jutuba(self,ctx): + async def dej_co_z_jutuba(self, ctx): """ Download a music number from youtube. @@ -116,7 +148,9 @@ class MusicModule(commands.Cog): content = ctx.message.content.split() for item in content: if re.match("http.*", item): - dir_path, files = await music_functions.get_file(ctx, "Spotify", item) + dir_path, files = await music_functions.get_file( + ctx, "Spotify", item + ) if platform == "win32": separator = "\\" else: @@ -147,6 +181,530 @@ class MusicModule(commands.Cog): ) self.logger.info("Spotifaj udany") + # MUSIC BOT PART + @commands.hybrid_command( + name="cisza", + description="Wyłącza muzykę i czyści plejliste.", + guild=discord.Object(id=664789470779932693), + ) + async def cisza(self, ctx): + """ + Stop playback of the music and clear the queue. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + self.logger.info("Stop") + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["ctx"] = None + voice_client = self.bot.voice_clients[0] + self.check_music.stop() + if voice_client.is_connected(): + await self.disconnect(ctx=ctx) + while MUZYKA["queue"]: + MUZYKA["queue"].pop() + MUZYKA["requester"].pop() + self.logger.info("Stop completed") + + @commands.hybrid_command( + name="dalej", + description="Przerzuca na następny kawałek", + guild=discord.Object(id=664789470779932693), + ) + async def dalej(self, ctx): + """ + Play next track in queue. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + represents the context in which a command is being executed, including information such as the + message, the channel, the server, and the user who invoked the command. The context object is used + to access and manipulate this + """ + if ctx: + self.logger.info("Ctx defined for dalej") + self.logger.info("Next") + voice_client = self.bot.voice_clients[0] + if voice_client.is_connected(): + voice_client.stop() + self.logger.info("Next completed") + + # TODO: MUSIC + @commands.hybrid_command( + name="daj_mi_chwile", + description="Pauzuje", + guild=discord.Object(id=664789470779932693), + ) + async def daj_mi_chwile(self, ctx): + """ + Pause music playback. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in most + Discord.py commands and is + """ + if ctx: + self.logger.info("Ctx defined for pause") + + self.logger.info("Pause") + voice_client = self.bot.voice_clients[0] + if voice_client.is_connected(): + voice_client.pause() + self.logger.info("Pause completed") + + # TODO: MUSIC + @commands.hybrid_command( + name="graj_dalej", + description="Odpauzowuje", + guild=discord.Object(id=664789470779932693), + ) + async def graj_dalej(self, ctx): + """ + Unpause music and continue playback. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + if ctx: + self.logger.info("Ctx defined for graj_dalej") + self.logger.info("Unpause") + voice_client = self.bot.voice_clients[0] + if voice_client.is_paused(): + voice_client.resume() + self.logger.info("Unpause completed") + + # TODO: MUSIC + @commands.hybrid_command( + name="zagraj_mi_kawalek", + description="Wyszukuje w bibliotece muzycznej Hammera kawałek który ma zostać zagrany", + guild=discord.Object(id=664789470779932693), + ) + async def zagraj_mi_kawalek(self, ctx): + """ + Play a song or music piece in response to a command + triggered by a user in a Discord chat context. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + async with ctx.typing(): + search_time_glob = datetime.now() + self.logger.info("Zaczynam szukać timestamp %s", datetime.now()) + file = await self.wyszukaj(ctx=ctx) + self.logger.info( + "Koniec szukania(timestamp %s zajęło %s", + datetime.now(), + datetime.now() - search_time_glob, + ) + + if file: + for plik in file: + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, plik[1]) + MUZYKA["requester"].insert(0, ctx.author) + metadata = eyed3.load(plik[1]) + reply = ( + "Dodałem do playlisty {plik[1]} z prywatnej kolekcji Hammera." + ) + if metadata.tag.title: + reply = f"Dodałem do playlisty kawalek {metadata.tag.title}" + if metadata.tag.artist: + reply += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + reply += f" z albumu {metadata.tag.album}" + reply += ( + " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę." + ) + await ctx.send(reply) + else: + await ctx.send( + "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" + ) + + # TODO: MUSIC + @commands.hybrid_command( + name="zrob_mi_plejliste", + description="Generuje playliste - param1 - dlugosc, potem slowa wyszukiwania", + guild=discord.Object(id=664789470779932693), + ) + async def zrob_mi_plejliste(self, ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + reply = "Wygenerowana plejlista:\n" + async with ctx.typing(): + self.logger.info("Zaczynam szukać timestamp %s", datetime.now()) + search_time_glob = datetime.now() + + dlugosc_playlisty = ctx.message.content.split()[1] + file = await music_functions.wyszukaj(ctx=ctx, how_many=dlugosc_playlisty) + self.logger.info( + "Koniec szukania(timestamp %s zajęło %s", + datetime.now(), + datetime.now() - search_time_glob, + ) + index = 1 + if file: + for plik in file: + if plik[1]: + self.logger.info(plik) + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, plik[1]) + MUZYKA["requester"].insert(0, ctx.author) + metadata = eyed3.load(plik[1]) + if metadata and metadata.tag: + if metadata.tag.title: + reply += f"{index} {metadata.tag.title}" + else: + reply += ( + f"{index}. {plik[1]} z prywatnej kolekcji Hammera." + ) + + if metadata.tag.artist: + reply += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + reply += f" z albumu {metadata.tag.album}" + if metadata.tag.title: + reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę.\n" + else: + reply += ( + f"{index}. {plik[1]} z prywatnej kolekcji Hammera.\n" + ) + + index += 1 + if len(reply) > 1800: + await ctx.send(reply) + reply = "" + else: + await ctx.send( + "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" + ) + await ctx.send(reply) + self.logger.info("Plejlista zrobiona") + + # TODO: MUSIC + @commands.hybrid_command( + name="zagraj_muzyke_mego_ludu", + description="Gra muzyke wyszukana na bazie tematow konwersacji na kanale NZ", + guild=discord.Object(id=664789470779932693), + ) + async def zagraj_muzyke_mego_ludu(self, ctx): + """ + Play completeley random playlist based on messaging history. + + :param ctx: ctx stands for "context" and is typically used in Discord.py commands to represent the + context in which the command was invoked. This includes information such as the message, the + channel, the author, and other relevant details. In this case, it is being passed as a parameter to + the `get_image + """ + stats = {} + if ctx: + self.logger.info("Zagraj muzyke mego ludu: wywolane") + stat_iter = get_stats(self.bot, ctx, MUZYKA_MOJEGO_LUDU_HISTORIA) + async for name, how_many in stat_iter: + if len(name) > 3: + stats[name] = how_many + + sorted_stats = sorted(stats.items(), key=lambda x: x[1]) + self.logger.info("Zagraj muzyke mego ludu:zebrano statystyki") + key_words = [] + for stat in sorted_stats: + key_words.append(stat[0]) + self.logger.info(sorted_stats[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE]) + self.logger.info(key_words[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE]) + self.logger.info(sorted_stats[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) + self.logger.info(key_words[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) + + final_key_words = key_words[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE] + final_key_words.extend(key_words[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) + reply = "Wygenerowana plejlista:\n" + async with ctx.typing(): + self.logger.info( + "Zagraj muzyke mego ludu:Zaczynam szukać timestamp %s", datetime.now() + ) + search_time_glob = datetime.now() + file = await music_functions.wyszukaj( + ctx=ctx, + how_many=MUZYKA_MOJEGO_LUDU_PLAJLISTA, + slowa_kluczowe=final_key_words, + ) + self.logger.info("Zagraj muzyke mego ludu:wygenerowano plejliste") + self.logger.info(file) + self.logger.info( + "Zagraj muzyke mego ludu: Koniec szukania(timestamp %s zajęło %s", + datetime.now(), + datetime.now() - search_time_glob, + ) + index = 1 + if file: + for plik in file: + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, plik[1]) + MUZYKA["requester"].insert(0, ctx.author) + metadata = eyed3.load(plik[1]) + if metadata and metadata.tag: + if metadata.tag.title: + reply += f"{index} {metadata.tag.title}" + else: + reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera." + + if metadata.tag.artist: + reply += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + reply += f" z albumu {metadata.tag.album}" + if metadata.tag.title: + reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę.\n" + else: + reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera.\n" + + index += 1 + if len(reply) > 1800: + await ctx.send(reply) + reply = "" + else: + await ctx.send( + "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" + ) + await ctx.send(reply) + self.logger.info("Plejlista zrobiona") + + # TODO: MUSIC + @commands.hybrid_command( + name="parametry_muzyki_mego_ludu", + description="Konfiguruje komende zagraj muzyke mojego ludu", + guild=discord.Object(id=664789470779932693), + ) + async def parametry_muzyki_mego_ludu( + self, ctx, ile_historii=1500, ile_slow_kluczowych=15, jak_dluga_plejlista=30 + ): + """ + The function `parametry_muzyki_mego_ludu` sets global variables for the number of history entries, + number of keywords, and length of playlist for a music application. + + :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating + Discord bots. It represents the context of the command being executed, including information about + the message, the server, and the user who invoked the command + :param ile_historii: The parameter "ile_historii" represents the number of history items for the + music playlist, defaults to 1500 (optional) + :param ile_slow_kluczowych: The parameter "ile_slow_kluczowych" represents the number of keywords or + key phrases related to music that you want to include in your analysis or search, defaults to 30 + (optional) + :param jak_dluga_plejlista: The parameter "jak_dluga_plejlista" determines the length of the + playlist. It specifies how many songs should be included in the playlist, defaults to 30 (optional) + """ + + if ctx: + async with ctx.typing(): + # if ':' in ile_historii: + # _, _, ile_historii = ile_historii.partition(':') + # if ':' in ile_slow_kluczowych: + # _, _, ile_slow_kluczowych = ile_slow_kluczowych.partition(':') + # if ':' in jak_dluga_plejlista: + # _, _, jak_dluga_plejlista = jak_dluga_plejlista.partition(':') + try: + global MUZYKA_MOJEGO_LUDU_HISTORIA # pylint: disable=global-statement + global MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE # pylint: disable=global-statement + global MUZYKA_MOJEGO_LUDU_PLAJLISTA # pylint: disable=global-statement + self.logger.info( + "Długość historii wyszukiwania: %s\n Ilość słów kluczowych: %s\n Długość plejlisty do wygenerowanie: %s", + MUZYKA_MOJEGO_LUDU_HISTORIA, + MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE, + MUZYKA_MOJEGO_LUDU_PLAJLISTA, + ) + ctx.send( + f"Dotychczasowe wartośći:\n Długość historii wyszukiwania: {MUZYKA_MOJEGO_LUDU_HISTORIA}\n Ilość słów kluczowych: {MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE}\n Długość plejlisty do wygenerowanie:{MUZYKA_MOJEGO_LUDU_PLAJLISTA}" + ) + MUZYKA_MOJEGO_LUDU_HISTORIA = ile_historii + MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE = ile_slow_kluczowych + MUZYKA_MOJEGO_LUDU_PLAJLISTA = jak_dluga_plejlista + self.logger.info( + "Długość historii wyszukiwania: %s\n Ilość słów kluczowych: %s\n Długość plejlisty do wygenerowanie: %s", + MUZYKA_MOJEGO_LUDU_HISTORIA, + MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE, + MUZYKA_MOJEGO_LUDU_PLAJLISTA, + ) + self.ctx.send( + f"Zmienione wartośći:\n Długość historii wyszukiwania: {MUZYKA_MOJEGO_LUDU_HISTORIA}\n Ilość słów kluczowych: {MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE}\n Długość plejlisty do wygenerowanie:{MUZYKA_MOJEGO_LUDU_PLAJLISTA}" + ) + except discord.ext.commands.errors.BadArgument as exce: + ctx.send("Spierdalaj") + self.logger.info(exce) + + @commands.hybrid_command( + name="graj_muzyko", + description="Włącza muzykę na kanale #nocna-zmiana.", + guild=discord.Object(id=664789470779932693), + ) + async def graj_muzyko(self, ctx): + """ + Async function named "graj_muzyko" starts music playback on channel "Nocna Zmiana". + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + + self.logger.info("Press play on tape") + async with ctx.typing(): + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["ctx"] = ctx + await self.connect(ctx=ctx) + await self.play(ctx=ctx) + self.logger.info("Press play on tape completed") + self.check_music.start() + + async def connect(self, ctx, arg=None): + """ + Connect the bot to a voice channel if it is not already connected. + + :param ctx: ctx is short for context and refers to the context in which the command was invoked. It + contains information about the message, the channel, the server, and the user who invoked the + command + :param arg: The `arg` parameter is an optional argument that can be passed to the `connect` + function. It is not used in the code snippet provided, but it could potentially be used to specify a + specific voice channel to connect to + :return: If the voice client is already connected, the function will return without doing anything. + If the function successfully connects to the voice channel, it will return a voice channel + connection object. If it is not possible to connect to the voice channel, the function will log an + error message and return nothing. + """ + if ctx and arg: + self.logger.info("Ctx and arg defined for connect") + + if self.bot.voice_clients: + self.logger.info("Already connected with other client") + else: + vc = None + if ctx.author.voice.channel: + vc = await ctx.author.voice.channel.connect() + else: + voice_channel = self.bot.get_channel(1060349757349974066) + vc = await voice_channel.connect() + if not vc: + self.logger.error("Not possible to connect to voice") + return + self.logger.info("Connected to voice") + + async def disconnect(self, ctx): + """ + Asynchronous Python function that disconnects the voice client if it is connected and + logs a message if the context is defined. + + :param ctx: ctx is short for context and refers to the context in which a command is being executed. + It contains information about the message, the user who sent the message, the channel the message + was sent in, and more. In this case, the `disconnect` function is likely being called as a command + in + """ + if ctx: + self.logger.info("Ctx defined for disconnect") + if self.bot.voice_clients: + voice_client = self.bot.voice_clients[0] + if voice_client.is_connected(): + await voice_client.disconnect() + + async def play(self, ctx, zamawial=None, arg=None): + """ + Play a music file, retrieve metadata about the file, and send a + message to a Discord channel with information about the song being played. + + :param ctx: The "ctx" parameter is a context object that contains information about the current + Discord command invocation, such as the message, the channel, and the user who invoked the command. + It is passed to the function automatically by the Discord.py library + :param zamawial: The parameter `zamawial` is a variable that stores the user who requested the song + to be played. It is an optional parameter and can be None if no user requested the song + :param arg: The `arg` parameter is an optional argument that can be passed to the `play` function. + It is not used in the code provided, so its purpose is unclear without additional context + """ + # TODO: Uzupełnianie metadanych. Jeśli ich nie ma sprawdzamy nazwę (w sposób inteligentny - czyli jeśli składa się z samych cyfr i/lub słowa track to sprawdzić album w sensie folder) + # TODO: Jeśli w metadanych jest tylko "audiotrack" i "album" też po + # folderze. Jeśli są niekompletne uzupełnić dalej. + self.logger.info("Play procedure") + if arg: + self.logger.info("Arg defined for play") + + file_to_play = None + voice_client = self.bot.voice_clients[0] + if not self.bot.voice_clients: + voice_client = self.bot.voice_clients[0] + await self.connect(ctx) + if MUZYKA["queue"]: + file_to_play = MUZYKA["queue"].pop() + zamawial = MUZYKA["requester"].pop() + self.logger.info("Muzyka z listy zamówień") + else: + # trunk-ignore(bandit/B311) + index = random.randint( + 0, len(music_functions.MUSIC_FILE_LIST.get_file_list()) - 1 + ) + file_to_play = music_functions.MUSIC_FILE_LIST.get_file_list()[index] + self.logger.info("Muzyka losowo") + + self.logger.info(file_to_play) + metadata = eyed3.load(file_to_play) + query = None + temp = file_to_play.split(SEPARATOR_FILE_PATH)[-1] + kawalek = f"Zagram teraz kawalek {temp} z prywatnej kolekcji Hammera." + if metadata: + if metadata.tag: + if metadata.tag.title: + query = f"Opowiedz mi o kawałku {metadata.tag.title}" + kawalek = f"Zagram teraz kawalek {metadata.tag.title}" + if metadata.tag.artist: + query += f" wykonawcy {metadata.tag.artist}" + kawalek += f" wykonawcy {metadata.tag.artist}" + if metadata.tag.album: + query += f" z albumu {metadata.tag.album}" + kawalek += f" z albumu {metadata.tag.album}" + kawalek += " z prywatnej kolekcji Hammera." + if zamawial: + kawalek += f"Na specjalne życzenie <@{zamawial.id}>." + await ctx.send(kawalek) + + voice_client.play( + discord.PCMVolumeTransformer( + discord.FFmpegPCMAudio(source=file_to_play), volume=0.3 + ) + ) # NIE DOTYKAC POKRĘTŁA GŁOŚNOŚCI!!!! + if query: + self.logger.debug("Zapytanie do openai") + self.logger.debug(query) + vykidailo = False + bartender = False + for role in ctx.message.author.roles: + if role.name == "Vykidailo": + vykidailo = True + if role.name == "Bartender": + bartender = True + if ctx.message.author.nick: + username = ctx.message.author.nick + else: + username = ctx.message.author.name + global MESSAGE_TABLE_MUZYKA # pylint: disable=global-statement + result, MESSAGE_TABLE_MUZYKA = await handle_response( + query, vykidailo, bartender, MESSAGE_TABLE_MUZYKA, username, "MUSIC" + ) + self.logger.debug("Obecna historia czatu: %s", MESSAGE_TABLE_MUZYKA) + if len(result) < 1500: + await ctx.send(result) + else: + while len(result) > 1500: + await ctx.send(result[:1500]) + result = result[1500:] + + async def setup(bot): logger = logging.getLogger("discord") music_functions.MUSIC_FILE_LIST.refresh_file_list() diff --git a/music_functions.py b/music_functions.py index 223fd91..bb005c5 100644 --- a/music_functions.py +++ b/music_functions.py @@ -1,27 +1,27 @@ -import logging import asyncio +import logging +import re +import uuid from pathlib import Path, PurePath from sys import platform + +import requests + +import yt_dlp +from constants import ( + FILE_SERVICE_ADDRESS, + GET_MP3, + GET_PLAYLIST, + MUSIC_FOLDER, + SEND_MP3, + SPOTIFY_CTRL, +) from spotify_dl import spotify from spotify_dl import youtube as youtube_download -import re -import netrc -import spotipy -from spotipy.oauth2 import SpotifyClientCredentials -from constants import NETRC_FILE, MUSIC_FOLDER, FILE_SERVICE_ADDRESS, GET_MP3, SEND_MP3 -import yt_dlp -import requests -netrc_mod = netrc.netrc(NETRC_FILE) -REMOTE_HOST_NAME = "spotipy" -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) logger = logging.getLogger("discord") -spotify_ctrl = spotipy.Spotify( - client_credentials_manager=SpotifyClientCredentials( - client_id=authTokens[0], - client_secret=authTokens[2], - ) -) + + class MusicFileList(object): """ The `MusicFileList` class manages a list of music files, with the ability to refresh the list from a @@ -74,6 +74,7 @@ class MusicFileList(object): post_data = {"item": str(item)} requests.post(f"{FILE_SERVICE_ADDRESS}{SEND_MP3}", json=post_data, timeout=360) + MUSIC_FILE_LIST = MusicFileList(logger) @@ -103,11 +104,11 @@ async def get_file(ctx, source, link): logger.info(item_id) logger.info("Spotify link provided") file_list = await asyncio.to_thread( - spotify.fetch_tracks, spotify_ctrl, item_type, link + spotify.fetch_tracks, SPOTIFY_CTRL, item_type, link ) logger.info(file_list) directory_name = await asyncio.to_thread( - spotify.get_item_name, spotify_ctrl, item_type, item_id + spotify.get_item_name, SPOTIFY_CTRL, item_type, item_id ) logger.info(directory_name) logger.info("Spotify scrape done") @@ -245,3 +246,40 @@ async def get_file(ctx, source, link): ) file_list.append("wiadomo co wiadomo gdzie") return dir_path, file_list + + +async def search_music(ctx, how_many=0, slowa_kluczowe=None): + """ + Take in a context object and an optional integer parameter "how_many" and perform search + operation on music library. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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 how_many: how_many is a parameter that specifies the number of search results to be returned. + It is an optional parameter with a default value of 0, which means that if no value is provided for + how_many, the function will return all search results, defaults to 0 (optional) + """ + # i sieć neuronową z możliwością wyrażenia opinii o dopasowaniu. + if slowa_kluczowe: + word_list = slowa_kluczowe + else: + word_list = ctx.message.content.split() + logger.info("Wyszukuje") + jrequest = { + "lista_slow": word_list, + "dlugosc_plejlisty": how_many, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{GET_PLAYLIST}", + json=jrequest, + timeout=360, + ) + return_data = await coroutine + if not return_data.status_code == 200: + await ctx.send("Wołaj szefa - coś się wyjebało") + return + logger.info(return_data.json()["data"]) + return return_data.json()["data"] diff --git a/other_commands.py b/other_commands.py index ef6f6cd..3dbab50 100644 --- a/other_commands.py +++ b/other_commands.py @@ -1,11 +1,17 @@ -from discord.ext import commands +import asyncio +import json import logging -import discord +from datetime import datetime from typing import Optional -from constants import DATA + +import discord +from discord.ext import commands + +from constants import ACCIDENT_LOG, DATA, ENCODING historia_fabryczki = DATA["fabryczka"] + class OtherModule(commands.Cog): def __init__(self, bot, logger_name): self.bot = bot @@ -46,9 +52,8 @@ class OtherModule(commands.Cog): "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*" ) - @commands.hybrid_command(name="fabryczka", description="Historia fabryczki") - async def fabryczka(self,ctx): + async def fabryczka(self, ctx): """ Send a general description of "fabryczkagate" to channel. @@ -59,10 +64,128 @@ class OtherModule(commands.Cog): """ await ctx.send(historia_fabryczki) + @commands.hybrid_command( + name="reset_the_clock", + description="Resetowanie zegara - dostępne wyłącznie dla Hammera", + guild=discord.Object(id=664789470779932693), + ) + async def reset_the_clock(self, ctx): + """ + Reset the clock on "accidents" in Hammer Fortress adding a new one. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + hammer = False + for role in ctx.message.author.roles: + if role.name == "Bartender": + hammer = True + if hammer: + with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file_accidents: + # First we load existing data into a dict. + file_data = json.load(new_file_accidents) + czas = datetime.strptime(file_data[-1][1], "%Y-%m-%d %H:%M:%S.%f") + opis = ctx.message.content[16:] + accident = accident = [opis, f"{datetime.now()}"] + file_data.append(accident) + new_file_accidents.seek(0) + # convert back to json. + json.dump(file_data, new_file_accidents, indent=4) + await ctx.send( + f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n RESET THE CLOCK. Od ostatniego incydentu w chacie Hammera minęło {czas}. No ale teraz się odpierdoliło: {opis}" + ) + else: + await ctx.send( + f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Gdzie z łapami do zegara? {self.bot.get_user(346956223645614080).mention} coś albo się komuś stało albo zaraz się stanie jak będzie zegar tykał...." + ) + + @commands.hybrid_command( + name="chata_hammera", + description="Czas od ostatniego incydentu w AbsinthHammerTimeSpaceContinuum", + ) + async def chata_hammera(ctx): + """ + Send measured time from last incident in Hammer Fortress to the chat. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file: + # First we load existing data into a dict. + file_data = json.load(new_file) + opis = file_data[-1][0] + czas = datetime.strptime(file_data[-1][1], "%Y-%m-%d %H:%M:%S.%f") + await ctx.send( + f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Od ostatniego incydentu w chacie Hammera minęło {datetime.now() - czas}. Incydent to: {opis}" + ) + + @commands.hybrid_command( + name="historia_incydentow_u_hammera", + description="Wyswietla liste incydentów które miały miejsce w AbsinthHammerTimeSpaceContinuum.", + ) + async def historia_incydentow_u_hammera(ctx): + """ + Send a list of incidents in Hammer Fortress to the chat. + + :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It + contains information about the context in which the command was invoked, such as the message, the + channel, the server, and the user who invoked the command. This information can be used to perform + various actions, such + """ + with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file_accidents: + # First we load existing data into a dict. + file_data = json.load(new_file_accidents) + return_data = "Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Historia zarejestrownych incydentów u Hammera:\n" + index = 1 + for iter_data in file_data: + opis = iter_data[0] + czas = datetime.strptime(iter_data[1], "%Y-%m-%d %H:%M:%S.%f") + add_data = f"{index}. Opis: {opis} Data: {czas}" + return_data = return_data + add_data + "\n" + index += 1 + await ctx.send(return_data) + + # TODO: RADIO + @commands.hybrid_command( + name="radio_hardkor", + description="Włącza radio Conjurer na kanale #nocna-zmiana.", + guild=discord.Object(id=664789470779932693), + ) + async def radio_hardkor(self, ctx): + """ + Plays the radio hardkor stream in the voice channel. + + Args: + ctx: The context object representing the invocation context. + + Returns: + None + """ + self.logger.info("Press play on radio hardkor") + async with ctx.typing(): + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["ctx"] = ctx + if not self.bot.voice_clients: + await self.connect(ctx=ctx) + voice_client = self.bot.voice_clients[0] + voice_client.play( + discord.PCMVolumeTransformer( + discord.FFmpegPCMAudio("http://95.175.16.246:666/mp3-stream"), + volume=0.3, + ) + ) + self.logger.info("Press play on radio completed") + else: + self.logger.error("Already playing") + await asyncio.sleep(12) + self.check_music.start() async def setup(bot): logger = logging.getLogger("discord") - logger.info("Playlist generation") await bot.add_cog(OtherModule(bot, "discord")) - logger.info("Loading other module done") + logger.info("Loading kitchen sink module done") diff --git a/other_functions.py b/other_functions.py index e69de29..58a69fc 100644 --- a/other_functions.py +++ b/other_functions.py @@ -0,0 +1,88 @@ +# define an asynchronous generator +async def async_iterator_generator(range_of_iterable): + """ + Generate asynchronouse iterator. + This is an incomplete function definition for an asynchronous iterator generator that takes a range + of iterable as input. + + :param range_of_iterable: The parameter `range_of_iterable` is likely a range or iterable object + that the async iterator generator will iterate over asynchronously. It could be a list, tuple, set, + or any other iterable object. The generator will yield each item in the iterable object + asynchronously, allowing other code to run in between + """ + # normal loop + for i in range_of_iterable: + # pylint: disable=pointless-string-statement + # yield the result + yield i + """ + Alternatywna wersja przerobienia fora na asynchroniczny. + # traverse the iterable of awaitables + for item in coros: + # await and get the result from the awaitable + result = await item + # report the results + print(result) + """ + + +# TODO: OTHER +async def remove_characters(string, character): + """ + The `remove_characters` function removes all occurrences of a specified character from a given + string. + + :param string: The string parameter is the input string from which characters will be removed + :param character: The character parameter is the character that you want to remove from the string + :return: a new string where all occurrences of the specified character have been removed. + """ + return string.replace(character, "") + + +# TODO: OTHER +async def max_weight(lista): + """ + Take a list of weights and return the maximum weight. + + :param lista: It seems like the parameter `lista` is a list of items, possibly representing weights. + The function name `max_weight` suggests that the function is intended to find the maximum weight + from the list. However, without more context or information about the problem, it's difficult to say + for sure what the + """ + maximum_weight = 0 + for iterator in lista: + if iterator[0] > maximum_weight: + maximum_weight = iterator[0] + return maximum_weight + + +async def get_stats(client, ctx, history_limit): + """ + The `get_stats` function retrieves the message history of a specific channel and counts the + frequency of each word in the messages. + + :param ctx: The `ctx` parameter is an object that represents the context of the command being + executed. It contains information such as the message, the author, the server, and other relevant + details + :param history_limit: The `history_limit` parameter is the maximum number of messages to retrieve + from the channel history. It determines how far back in time the statistics will be calculated + :return: The function `get_stats` returns a dictionary `stats` that contains the frequency count of + words found in the messages from the specified channel. + """ + channel_id = 1062047367337095268 + async with ctx.typing(): + stats = {} + channel = client.get_channel(channel_id) + messages = [message async for message in channel.history(limit=history_limit)] + # traverse the iterable of awaitables + for message in messages: + # await and get the result from the awaitable + result = message.content + words = result.split() + for word in words: + if word in stats: + stats[word] += 1 + else: + stats[word] = 1 + for name, how_many in stats.items(): + yield name, how_many diff --git a/radio_commands.py b/radio_commands.py index 503c766..53234a1 100644 --- a/radio_commands.py +++ b/radio_commands.py @@ -1 +1,201 @@ -import music_functions \ No newline at end of file +import logging +from discord.ext import commands +import discord +# trunk-ignore(bandit/B404) +import subprocess +import requests +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 + +class RadioModule(commands.Cog): + def __init__(self, bot, logger_name): + self.bot = bot + self.logger = logging.getLogger(logger_name) + + + @commands.hybrid_command( + name="skip_track", + description="Przeskocz kawałek w radiu", + guild=discord.Object(id=664789470779932693), + ) + async def skip_track(self, ctx): + async with ctx.typing(): + allowed = False + for role in ctx.author.roles: + if role.name in ("Thane", "Jarl"): + allowed = True + if not allowed: + await ctx.send("Łapy precz od radia") + else: + coroutine = asyncio.to_thread( + requests.get, + f"{RADIO_HARBOR_ADDRESS}{SKIP_TRACK}", + timeout=360, + ) + result = await coroutine + self.logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + + @commands.hybrid_command( + name="zrestartuj_radio", + description="Komenda ktora uruchamia radio ponownie jakby się zawiesiło", + guild=discord.Object(id=664789470779932693), + ) + async def zrestartuj_radio(self, ctx): + async with ctx.typing(): + allowed = False + for role in ctx.author.roles: + if role.name in ("Thane", "Jarl"): + allowed = True + if not allowed: + await ctx.send("Łapy precz od radia") + else: + retcode = subprocess.run( + "/home/pi/Conjurer/restart_radio.sh", + # trunk-ignore(bandit/B603) + shell=False, + check=False, + capture_output=True, + ) + self.logger.info("Wynik: %s", retcode) + + @commands.hybrid_command( + name="dodaj_do_ulubionych", + description="Dodaje do listy ulubionych w radiu", + guild=discord.Object(id=664789470779932693), + ) + async def dodaj_do_ulubionych(self, ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + self.logger.info("Zaczynam szukać timestamp %s", datetime.now()) + + dlugosc_playlisty = ctx.message.content.split()[1] + word_list = ctx.message.content.split() + jrequest = { + "lista_slow": word_list, + "dlugosc_plejlisty": dlugosc_playlisty, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{ADD_TO_PRIO_PLAYLIST}", + json=jrequest, + timeout=360, + ) + result = await coroutine + self.logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + + @commands.hybrid_command( + name="ja_chciol", + description="Dodaje do listy ulubionych w radiu", + guild=discord.Object(id=664789470779932693), + ) + async def request_radio(self, ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + self.logger.info("Zaczynam szukać timestamp %s", datetime.now()) + + word_list = ctx.message.content.split() + jrequest = { + "lista_slow": word_list, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{REQUEST_MUSIC}", + json=jrequest, + timeout=360, + ) + result = await coroutine + self.logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + + @commands.hybrid_command( + name="stworz_audycje", + description="Dodaje do listy ulubionych w radiu", + guild=discord.Object(id=664789470779932693), + ) + async def stworz_audycje(self, ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + self.logger.info("Zaczynam szukać timestamp %s", datetime.now()) + + dlugosc_playlisty = ctx.message.content.split()[1] + word_list = ctx.message.content.split() + jrequest = { + "lista_slow": word_list, + "dlugosc_plejlisty": dlugosc_playlisty, + "UUID": str(uuid.uuid4()), + } + coroutine = asyncio.to_thread( + requests.post, + f"{FILE_SERVICE_ADDRESS}{CREATE_PRIO_PLAYLIST}", + json=jrequest, + timeout=360, + ) + result = await coroutine + self.logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + + @commands.hybrid_command( + name="wyczysc_ulubione", + description="Czysci liste ulubionych w radiu", + guild=discord.Object(id=664789470779932693), + ) + async def wyczysc_ulubione(self, ctx): + """ + Generate a playlist in queue. First word in this command shall be int defining length of the playlist. + Rest of the line are search terms. + + :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all + Discord.py commands + """ + async with ctx.typing(): + coroutine = asyncio.to_thread( + requests.get, + f"{FILE_SERVICE_ADDRESS}{CLEAR_PRIO}", + timeout=360, + ) + result = await coroutine + self.logger.info("Done %s", result) + await ctx.send("Zrobione szefie!") + + +async def setup(bot): + logger = logging.getLogger("discord") + await bot.add_cog(RadioModule(bot, "discord")) + logger.info("Loading raadio commands module done") diff --git a/requirements_bot copy.txt b/requirements_bot copy.txt new file mode 100644 index 0000000..149dc14 --- /dev/null +++ b/requirements_bot copy.txt @@ -0,0 +1,21 @@ +setuptools +discord +yaach +t_dlp +spotify_dl +spotipy +openai +eyed3 +numpy +pdf2image +PyPDF2 +requests +spotipy +tiktoken +PyNaCl +flask[async] +waitress +clickupython +assemblyai[extras] +SpeechRecognition +git+https://github.com/imayhaveborkedit/discord-ext-voice-recvO \ No newline at end of file diff --git a/requirements_bot.txt b/requirements_bot.txt index dd2fcd4..2831a78 100644 --- a/requirements_bot.txt +++ b/requirements_bot.txt @@ -1,3 +1,4 @@ +setuptools discord yt_dlp spotify_dl @@ -11,9 +12,9 @@ requests spotipy tiktoken PyNaCl -flask +flask[async] waitress clickupython assemblyai[extras] SpeechRecognition -git+https://github.com/imayhaveborkedit/discord-ext-voice-recv \ No newline at end of file +git+https://github.com/imayhaveborkedit/discord-ext-voice-recvO \ No newline at end of file diff --git a/stream_test.py b/stream_test.py index 82a410e..7b4411d 100644 --- a/stream_test.py +++ b/stream_test.py @@ -6,57 +6,62 @@ # # Note: Some macOS users might need to use `pip3` instead of `pip`. -import pyaudio 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_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." + "This function is called when a new transcript has been received." - if not transcript.text: - return + if not transcript.text: + return + + if isinstance(transcript, aai.RealtimeFinalTranscript): + print(transcript.text, end="\r\n") + else: + print(transcript.text, end="\r") - 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." + "This function is called when the connection has been closed." + + print("An error occured:", error) - print("An error occured:", error) def on_close(): - "This function is called when the connection has been closed." + "This function is called when the connection has been closed." + + print("Closing Session") - 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 + 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)) + print(pa.get_device_info_by_index(i)) # Start the connection -#transcriber.connect() +# transcriber.connect() # Open a microphone stream -#microphone_stream = aai.extras.MicrophoneStream() +# microphone_stream = aai.extras.MicrophoneStream() # Press CTRL+C to abort -#transcriber.stream(microphone_stream) +# transcriber.stream(microphone_stream) -#transcriber.close() +# transcriber.close() diff --git a/test.py b/test.py new file mode 100644 index 0000000..357bb3f --- /dev/null +++ b/test.py @@ -0,0 +1,20 @@ +import logging +from logging import handlers + +logger = logging.getLogger("discord") +logger.setLevel(logging.DEBUG) +handler = handlers.RotatingFileHandler( + filename="test.log", + encoding="utf-8", + mode="a", + maxBytes=6 * 1024 * 1024, + backupCount=6, +) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) + + +logger2 = logging.getLogger("discord") +for item in logger2.handlers: + print(item) diff --git a/test_time.py b/test_time.py index 63b1f06..0c71465 100644 --- a/test_time.py +++ b/test_time.py @@ -1,7 +1,8 @@ import time + first_time = time.time_ns() time.sleep(1) time_diff = time.time_ns() - first_time print(time_diff) -#2000149433 \ No newline at end of file +# 2000149433 diff --git a/thin_client.py b/thin_client.py index 395a21c..aed2484 100644 --- a/thin_client.py +++ b/thin_client.py @@ -5,221 +5,19 @@ """ Module of a python bot named Conjurer - used to work on BDSM discord servers. """ -import asyncio -import io -import json import logging -import netrc # *=========================================== Standard Library Imports -import os import random -import re -import shutil -import subprocess -import sys import threading -import uuid -from datetime import datetime from logging import handlers -from pathlib import Path -from platform import uname -from queue import Empty -from sys import platform -from typing import List, Optional, TypedDict -from constants import DATA + # *==============Imported libraries import discord -import eyed3 -import numpy as np -import openai -import pdf2image -import PyPDF2 -import requests -import spotipy -from discord.ext import commands, tasks -from spotipy.oauth2 import SpotifyClientCredentials - -from communication_subroutine import ( - IN_COMM_Q, - OUT_COMM_Q, - QueryControl, - comm_subroutine, -) -from ai_functions import num_tokens_from_string -# *=========================== Classes definition - -# The above code is defining a TypedDict called `Music_Config` in Python. This TypedDict has three -# keys: "ctx" which is an optional string, "queue" which is a list of strings, and "requester" which -# is a list of strings. This TypedDict is used to define the structure of a dictionary that should -# have these specific keys and value types. -Music_Config = TypedDict( - "Music_Config", - { - "ctx": Optional[str], - "queue": List[str], - "requester": List[str], - }, -) - - -class MusicFileList(object): - """ - The `MusicFileList` class manages a list of music files, with the ability to refresh the list from a - file service or local directory and update the list with new items. - """ - - def __init__(self, uplogger) -> None: - self.music_file_list = [] - self.file_service_active = False - self.logger = uplogger - self.logger.info("Created Playlist organizer class") - - def refresh_file_list(self): - """ - The `refresh_file_list` function attempts to connect to a file service to retrieve a list of music - files, and if unsuccessful, it populates the list by scanning a local directory for .mp3 files. - """ - try: - self.logger.info("Attempt to connect to file service") - response = requests.get(f"{FILE_SERVICE_ADDRESS}{GET_MP3}", timeout=360) - 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"): - temp_music_file = mp3_item.as_posix() - if platform == "win32": - temp_music_file = temp_music_file.replace("/", "\\") - self.music_file_list.append(temp_music_file) - self.file_service_active = False - self.logger.error(e.strerror) - self.logger.error("Service Unavailable") - - def get_file_list(self): - """ - The `get_file_list` function returns the list of music files stored in the object. - :return: The `music_file_list` attribute is being returned. - """ - return self.music_file_list - - def update_file_list(self, item): - """ - The `update_file_list` function appends an item to a music file list and sends a POST request with - the item data to a file service address. - - :param item: The `item` parameter in the `update_file_list` method is the file that you want to add - to the `music_file_list` attribute of the class instance. It is then sent as a JSON payload in a - POST request to a file service address along with the endpoint `SEND_MP3` - """ - self.music_file_list.append(item) - post_data = {"item": str(item)} - requests.post(f"{FILE_SERVICE_ADDRESS}{SEND_MP3}", json=post_data, timeout=360) - - -# *=========================================== Predefines -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 = "" -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" - -GET_MP3 = "/mp3" -SEND_MP3 = "/update_mp3" -GET_PLAYLIST = "/get_music" -ADD_TO_PRIO_PLAYLIST = "/add_to_priority" -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" -TIME_BETWEEN_CALLS = 100000 -LAST_SPONTANEOUS_CALL = datetime.now() -HOST_ADDRESS = "192.168.1.191" -PORT_ADDRESS = 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" - - 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/RetroPie/mp3/" - SETTINGS_FILE = "/home/pi/Conjurer/settings.json" - NETRC_FILE = "/home/pi/.netrc" - LOGSTORE = "/home/pi/RetroPie/logs/" - ACCIDENT_LOG = "/home/pi/Conjurer/accident_log.json" - ENCODING = "utf-8" - GRAPHICS_PATH = "/home/pi/RetroPie/Conjurer_graphics/" - DIR_PATH_SADOX = "/home/pi/RetroPie/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 = "\\" - - -# *=========================================== Keys -netrc_mod = netrc.netrc(NETRC_FILE) -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], - ) -) - -REMOTE_HOST_NAME = "openai" -authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) -openai.api_key = authTokens[2] -openAIClient = openai.AsyncOpenAI(api_key=openai.api_key) - +from discord.ext import commands +from communication_subroutine import comm_subroutine +from constants import ENCODING, LOGFILE, TOKEN # *=========================================== Initializations intents = discord.Intents.default() @@ -242,49 +40,7 @@ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(messag handler.setFormatter(formatter) logger.addHandler(handler) random.seed() -logger.debug(platform) -logger.info("Playlist generation") - -# *=========================================== Load Data files - -music_file_list = MusicFileList(logger) -music_file_list.refresh_file_list() -logger.info("Playlist generation done") - -logger.info("Loading GPT settings") -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) -logger.info("Done") - -logger.info("Loading memory file") -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) -logger.info("Done") - -logger.info("Loading music memory 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) -logger.info("Done") - -logger.info("Loading settings file") - -logger.info("Loading reactions") -word_reactions = DATA["word_reactions"] -cyclic_words = DATA["cyclic_words"] -for key in word_reactions: - word_reactions[key][2] = datetime.now() -logger.info("Done") - - -# *=========================================== Create Client -logger.info("Creating discord bot") client = commands.Bot(intents=intents, command_prefix="$") -logger.info("Done") -logger.info("Creating flask app") -logger.info("Done") # *=========================================== Define Events @@ -292,1877 +48,27 @@ logger.info("Done") async def on_ready(): """Metoda wywoływana przy połączeniu do serwera.""" logger.debug("%s has connected to Discord!", client.user) - #TODO: load vs reload - #try: - await client.load_extension("voice_recognition_commands") + # TODO: load vs reload + await client.load_extension("administration_commands") + + await client.load_extension("librarian_commands") await client.load_extension("music_commands") - await client.load_extension("other_commands") + await client.load_extension("radio_commands") + + await client.load_extension("ai_commands") + + await client.load_extension("other_commands") + await client.load_extension("voice_recognition_commands") + + logger.info(client.cogs) await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) await client.tree.sync() for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) - check_self.start() - check_data_q.start() -@client.event -async def on_message(message): - """ - Handle incoming messages in a Discord server, perform various - checks and actions based on the content and context of the message, and respond accordingly. - - :param message: The message object that is received when a user sends a message in a Discord server - or DM. The code is checking various conditions and performing actions based on the content of the - message and the context in which it was sent. It also includes TODOs for future improvements - :return: The function `on_message` is being returned. - """ - vykidailo = False - channel = None - if message.author == client.user: - return - if isinstance(message.author, discord.Member): - for role in message.author.roles: - if role.name == "Vykidailo": - vykidailo = True - - if ("Conjurer Śpij Słodko Aniołku" in message.content) and vykidailo: - sys.exit() - # kanal bez bota - if message.channel.id == 1095985579147141202: - return - # wentylacja - if message.channel.id == 1083804024173764739: - return - # legendy - if message.channel.id == 1084448332841230388: - return - # interrogation booth - if message.channel.id == 1111625221171052595: - return - if isinstance(message.channel, discord.DMChannel): - if message.author.id == 346956223645614080: - await message.reply("Witam witam") - return - else: - channel = client.get_channel(1064888712565100614) - await channel.send("Słyszałem ja żem że: " + message.content) - return - channel = message.channel - await client.process_commands(message) - - message.content = message.content.lower() - - tdelta = datetime.now() - MASTER_TIMEOUT - tdelta = tdelta.total_seconds() - if "opowiedz o fabryczce" in message.content: - await message.reply(DATA["fabryczka"]) - if "opowiedz mi o fabryczce" in message.content: - await message.reply(DATA["fabryczka"]) - - if tdelta > INITIAL_TIME_WAIT: - for word in word_reactions: - if re.search(r"\b" + word + r"\b", message.content): - tdelta = datetime.now() - word_reactions[word][2] - tdelta = tdelta.total_seconds() - security_clearance = word_reactions[word][4] - reaction = word_reactions[word][3] - if tdelta > word_reactions[word][1]: - # TODO: to zrobic reactiony - logger.info("Ping z procedury reakcji") - if reaction: - emoji = client.get_emoji(word_reactions[word][0]) - await message.add_reaction(emoji) - elif security_clearance and vykidailo: - await message.reply(word_reactions[word][0]) - elif not security_clearance: - await message.reply(word_reactions[word][0]) - word_reactions[word][2] = datetime.now() - - # TODO: drobne literówki, mentiony, spacja przed dwukropkiem. napraw. - kondziu_mentioned = False - for mention in message.mentions: - if mention == client.user: - kondziu_mentioned = True - - if kondziu_mentioned or "conjurer:" in message.content: - async with channel.typing(): - logger.debug("Procedura chatu") - - message.content = message.content.replace("conjurer: ", "") - if message.author.nick: - username = message.author.nick - else: - username = message.author.name - vykidailo = False - bartender = False - if kondziu_mentioned: - prompt = message.clean_content - else: - prompt = message.content - for role in message.author.roles: - if role.name == "Vykidailo": - vykidailo = True - if role.name == "Bartender": - bartender = True - global MESSAGE_TABLE # pylint: disable=global-statement - - result, MESSAGE_TABLE = await handle_response( - prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" - ) - - if len(result) < 1500: - await message.reply(result) - else: - while len(result) > 1500: - await message.reply(result[:1500]) - result = result[1500:] - if "imaginuje sobie:" in message.content: - async with channel.typing(): - logger.info("Poczatek procedury obrazkowej") - message.content = message.content.replace("imaginuje sobie: ", "") - logger.debug("Wywolanie obrazka: %s", message.content) - try: - response = await openAIClient.images.generate( - model="dall-e-3", - prompt=message.content, - size="1024x1024", - quality="standard", - n=1, - ) - except openai.APITimeoutError as e: - # Handle timeout error, e.g. retry or log - await message.reply( - f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" - ) - except openai.APIConnectionError as e: - await message.reply( - f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" - ) - except openai.BadRequestError as e: - # Handle invalid request error, e.g. validate parameters or log - if message.author.nick: - username = message.author.nick - else: - username = message.author.name - resp, _ = await handle_response( - f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie '{message.content}'", - True, - True, - MESSAGE_TABLE, - username, - "RANDOM", - ) - await message.reply( - f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" - ) - except openai.AuthenticationError as e: - # Handle authentication error, e.g. check credentials or log - await message.reply( - f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" - ) - except openai.PermissionDeniedError as e: - # Handle permission error, e.g. check scope or log - await message.reply( - ( - f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" - ) - ) - except openai.RateLimitError as e: - await message.reply( - f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" - ) - except openai.APIError as e: - # Handle API error, e.g. retry or log - await message.reply( - f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" - ) - if response: - logger.info(response) - image_url = response.data[0].url - image_desc = response.data[0].revised_prompt - logger.debug("Wynikowy obrazek pod url: %s", image_url) - response = requests.get(image_url, timeout=360) - - temp_file_name = message.content + ".png" - temp_file_name = GRAPHICS_PATH + message.content + ".png" - - with open(temp_file_name, "wb") as dalle_file: - dalle_file.write(response.content) - logger.info("Koniec procedury obrazkowej.") - fnord = discord.File( - temp_file_name, spoiler=False, description=message.content - ) - await message.reply(f"{image_desc}", file=fnord) - - -# *=========================================== Define Functions - - -async def connect(ctx, arg=None): - """ - Connect the bot to a voice channel if it is not already connected. - - :param ctx: ctx is short for context and refers to the context in which the command was invoked. It - contains information about the message, the channel, the server, and the user who invoked the - command - :param arg: The `arg` parameter is an optional argument that can be passed to the `connect` - function. It is not used in the code snippet provided, but it could potentially be used to specify a - specific voice channel to connect to - :return: If the voice client is already connected, the function will return without doing anything. - If the function successfully connects to the voice channel, it will return a voice channel - connection object. If it is not possible to connect to the voice channel, the function will log an - error message and return nothing. - """ - if ctx and arg: - logger.info("Ctx and arg defined for connect") - - if client.voice_clients: - logger.info("Already connected with other client") - else: - vc = None - if ctx.author.voice.channel: - vc = await ctx.author.voice.channel.connect() - else: - voice_channel = client.get_channel(1060349757349974066) - vc = await voice_channel.connect() - if not vc: - logger.error("Not possible to connect to voice") - return - logger.info("Connected to voice") - - -async def disconnect(ctx): - """ - Asynchronous Python function that disconnects the voice client if it is connected and - logs a message if the context is defined. - - :param ctx: ctx is short for context and refers to the context in which a command is being executed. - It contains information about the message, the user who sent the message, the channel the message - was sent in, and more. In this case, the `disconnect` function is likely being called as a command - in - """ - if ctx: - logger.info("Ctx defined for disconnect") - if client.voice_clients: - voice_client = client.voice_clients[0] - if voice_client.is_connected(): - await voice_client.disconnect() - - -async def play(ctx, zamawial=None, arg=None): - """ - Play a music file, retrieve metadata about the file, and send a - message to a Discord channel with information about the song being played. - - :param ctx: The "ctx" parameter is a context object that contains information about the current - Discord command invocation, such as the message, the channel, and the user who invoked the command. - It is passed to the function automatically by the Discord.py library - :param zamawial: The parameter `zamawial` is a variable that stores the user who requested the song - to be played. It is an optional parameter and can be None if no user requested the song - :param arg: The `arg` parameter is an optional argument that can be passed to the `play` function. - It is not used in the code provided, so its purpose is unclear without additional context - """ - # TODO: Uzupełnianie metadanych. Jeśli ich nie ma sprawdzamy nazwę (w sposób inteligentny - czyli jeśli składa się z samych cyfr i/lub słowa track to sprawdzić album w sensie folder) - # TODO: Jeśli w metadanych jest tylko "audiotrack" i "album" też po - # folderze. Jeśli są niekompletne uzupełnić dalej. - logger.info("Play procedure") - if arg: - logger.info("Arg defined for play") - - file_to_play = None - voice_client = client.voice_clients[0] - if not client.voice_clients: - voice_client = client.voice_clients[0] - await connect(ctx) - if MUZYKA["queue"]: - file_to_play = MUZYKA["queue"].pop() - zamawial = MUZYKA["requester"].pop() - logger.info("Muzyka z listy zamówień") - else: - index = random.randint(0, len(music_file_list.get_file_list()) - 1) - file_to_play = music_file_list.get_file_list()[index] - logger.info("Muzyka losowo") - - logger.info(file_to_play) - metadata = eyed3.load(file_to_play) - query = None - temp = file_to_play.split(SEPARATOR_FILE_PATH)[-1] - kawalek = f"Zagram teraz kawalek {temp} z prywatnej kolekcji Hammera." - if metadata: - if metadata.tag: - if metadata.tag.title: - query = f"Opowiedz mi o kawałku {metadata.tag.title}" - kawalek = f"Zagram teraz kawalek {metadata.tag.title}" - if metadata.tag.artist: - query += f" wykonawcy {metadata.tag.artist}" - kawalek += f" wykonawcy {metadata.tag.artist}" - if metadata.tag.album: - query += f" z albumu {metadata.tag.album}" - kawalek += f" z albumu {metadata.tag.album}" - kawalek += " z prywatnej kolekcji Hammera." - if zamawial: - kawalek += f"Na specjalne życzenie <@{zamawial.id}>." - await ctx.send(kawalek) - - voice_client.play( - discord.PCMVolumeTransformer( - discord.FFmpegPCMAudio(source=file_to_play), volume=0.3 - ) - ) # NIE DOTYKAC POKRĘTŁA GŁOŚNOŚCI!!!! - if query: - logger.debug("Zapytanie do openai") - logger.debug(query) - vykidailo = False - bartender = False - for role in ctx.message.author.roles: - if role.name == "Vykidailo": - vykidailo = True - if role.name == "Bartender": - bartender = True - if ctx.message.author.nick: - username = ctx.message.author.nick - else: - username = ctx.message.author.name - global MESSAGE_TABLE # pylint: disable=global-statement - result, MESSAGE_TABLE = await handle_response( - query, vykidailo, bartender, message_table_muzyka, username, "MUSIC" - ) - logger.debug("Obecna historia czatu: %s", message_table_muzyka) - if len(result) < 1500: - await ctx.send(result) - else: - while len(result) > 1500: - await ctx.send(result[:1500]) - result = result[1500:] - - -async def archive_channel(channel_no): - """ - The function `archive_channel` retrieves messages from a specified channel, processes them, and - saves the data in a JSON file with a timestamp in the filename. - - :param channel_no: The `archive_channel` function you provided seems to be incomplete. It looks like - you are trying to archive messages from a Discord channel into a JSON file - """ - channel = client.get_channel(channel_no) - messages = [message async for message in channel.history(limit=None)] - path_newfile = f"{LOGSTORE}channeldump_{channel_no}_{datetime.now()}.json" - new_data = [] - for message in messages: - new_data.append(message) - with open(path_newfile, "x", encoding=ENCODING) as new_file: - new_file.seek(0) - json.dump(new_data, new_file, indent=4) - - -@tasks.loop(seconds=1) -async def check_music(): - """ - This Python function continuously checks for music playback in a voice client and plays music if - conditions are met. - """ - if client.voice_clients: - voice_client = client.voice_clients[0] - if ( - voice_client - and voice_client.is_connected() - and not voice_client.is_playing() - and not voice_client.is_paused() - ): - await play(MUZYKA["ctx"], MUZYKA["requester"]) - await asyncio.sleep(2) - - -@tasks.loop(seconds=3) -async def check_data_q(): - """ - This function checks the data queue for any new entries and processes them accordingly. - """ - try: - fresh_data = IN_COMM_Q.get(block=False) - entries = [] - if fresh_data.stop: - searcher = fresh_data.author - query = fresh_data.content - l_p = 1 - for doi in fresh_data.entries: - logger.info(doi) - desc = fresh_data.entries[doi] - title = desc["Title"][0] - entries.append( - f"{l_p}. {title} pod linkiem https://www.sci-hub.se/{doi} i jest to {desc['type']}\n" - ) - l_p += 1 - message = "*Z podłogi wysuwa się winda na książki*" - if fresh_data.ctx is not None: - ctx = fresh_data.ctx - message += f" Dzielny poszukiwaczu @{searcher} w odpowiedzi na twoje zapytanie {query} nr {fresh_data.uuid}" - else: - ctx = client.get_channel(1062047571557744721) - message += f" Mam wynik dla zapytania {fresh_data.uuid} ale że Hammer coś grzebał nie pamiętam kto to chciał i co chciał" - - message += "nasza biblioteka służy Ci odpowiedzią. Przy dźwięku fanfar winda się otwiera a w środku " - if len(entries) < 1: - message += "niestety nie ma nic" - await ctx.send(message) - elif len(entries) >= 1 and len(entries) < 5: - message += " znajduje się coś:\n" - for item in entries: - message += item - await ctx.send(message) - else: - message += " znajduje się cholernie dużo:\n" - for item in entries: - message += item - if len(message) > 1500: - await ctx.send(message) - message = "" - - # Kept for sentimental reasons - # await ctx.send(f"O. A tak będzie wyglądało coś ciekawego w przyszłości: {data}") - except Empty: - pass - - -@tasks.loop(seconds=120) -async def check_self(): - """ - The function `check_data` periodically checks for conditions to send messages and manage log files - in a Discord channel. - """ - # logger.info("Heartbeat of cleanup proc") - channel = client.get_channel(1062047367337095268) - messages = [message async for message in channel.history(limit=1)] - for mess in messages: - channel = mess.channel - if os.path.getsize(LOGFILE) > 60000000: - await channel.send( - "*Conjurer porządkuje bar, wypala szklanki do czysta miotaczem płomieni ze swojej zbroi i ogólnie wygląda na zajętego....*" - ) - async with channel.typing(): - shutil.copyfile(LOGFILE, f"{LOGSTORE}discord{datetime.now()}") - logger.info("Log rollover") - handler.doRollover() - await channel.send( - "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* ...sać powieści o naszym wspaniałym barze SZEFIE!" - ) - - if os.path.getsize(MEMORY_FIVE_MUZYKA) > 3000000: - await channel.send( - "*Conjurer porządkuje bar, wypala szklanki do czysta miotaczem płomieni ze swojej zbroi i ogólnie wygląda na zajętego....*" - ) - async with channel.typing(): - path_newfile = f"{LOGSTORE}pamiec_muzyki{datetime.now()}.json" - logger.info(path_newfile) - with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: - with open(path_newfile, "x", encoding=ENCODING) as new_file: - # First we load existing data into a dict. - file_data = json.load(file_music_memory) - new_data = [] - new_data.append(file_data[0]) - new_data.extend(file_data[:-20]) - file_music_memory.truncate(0) - file_music_memory.seek(0) - # convert back to json. - new_file.seek(0) - json.dump(new_data, file_music_memory, indent=4) - json.dump(file_data, new_file, indent=4) - await channel.send( - "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* ...sać powieści o naszym wspaniałym barze SZEFIE!" - ) - if os.path.getsize(MEMORY_FIVE_SIARA) > 3000000: - path_newfile = f"{LOGSTORE}pamiec_rozmow{datetime.now()}.json" - logger.info(path_newfile) - await channel.send( - "*Conjurer porządkuje bar, ścina lekkim laserem pulsacyjnym powierzchnie baru o grubości kilku mikronów i ogólnie wygląda na zajętego....*" - ) - with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file: - with open(path_newfile, "x", encoding=ENCODING) as new_file: - # First we load existing data into a dict. - file_data = json.load(file) - new_data = [] - new_data.append(file_data[0]) - new_data.extend(file_data[-20]) - file.truncate(0) - file.seek(0) - new_file.seek(0) - # convert back to json. - json.dump(new_data, file, indent=4) - json.dump(file_data, new_file, indent=4) - await channel.send( - "*Przeciąga się za barem* No dobra - porobione to można dalej pi... *Zauważa spojrzenie Hammera* eprzyć o głupotach z klientami... Szefie... *Bierze 'ukradkowy' łyk z piersiówki*" - ) - global LAST_SPONTANEOUS_CALL - global TIME_BETWEEN_CALLS - tdelta = datetime.now() - LAST_SPONTANEOUS_CALL - tdelta = tdelta.total_seconds() - if tdelta > TIME_BETWEEN_CALLS: - logger.info("Spontaneous call") - TIME_BETWEEN_CALLS = random.randint( - 10200, 272800 - ) # temp random, set after each call - logger.debug(TIME_BETWEEN_CALLS) - LAST_SPONTANEOUS_CALL = datetime.now() - async with channel.typing(): - message = await get_random_cyclic_message() - logger.info("Odpowiedz") - logger.info(message) - await channel.send(message) - - -async def get_random_cyclic_message(): - """ - The function `get_random_cyclic_message` returns a random cyclic message from a list of cyclic - words. - :return: a random cyclic message from the list `cyclic_words`. - """ - channel_id = 1062047367337095268 - channel = client.get_channel(channel_id) - ai_check = random.randint(0, 10) - logger.info("Losowa wypowiedź") - if ai_check < 2: - logger.info("Predefiniowana") - messnum = random.randint(0, len(cyclic_words)) - logger.debug(messnum) - logger.debug(len(cyclic_words)) - mess_key = list(cyclic_words.keys())[messnum] - return cyclic_words[mess_key][0] - ai_check2 = random.randint(0, 10) - global MESSAGE_TABLE - if ai_check2 < 6: - logger.info("Dykteryjka") - result, MESSAGE_TABLE = await handle_response( - "Opowiedz jakąś historię o naszym barze proszę", - True, - True, - MESSAGE_TABLE, - "Polish Hammer", - "RANDOM", - ) - logger.info(result) - else: - logger.info("Wtracenie w dyskusje") - messages = [message async for message in channel.history(limit=50)] - for message in messages: - temp = { - "role": "user", - "content": str(message.author) + ":" + str(message.content), - } - MESSAGE_TABLE.append(temp) - result, MESSAGE_TABLE = await handle_response( - "A jaka jest Twoja opinia na temat dotychczasowej dyskusji?", - True, - True, - MESSAGE_TABLE, - "Polish Hammer", - "RANDOM", - ) - logger.info(result) - return result - - -async def handle_response( - prompt, vykidailo, bartender, history, username, request_type -): - """ - Handle responses by appending them to a history, use OpenAI to - generate a response, and then append the generated response to the history. - - :param prompt: The prompt for the OpenAI chatbot to generate a response to - :param vykidailo: It is a boolean variable that indicates whether the user invoking the function is - an administrator or not - :param bartender: The bartender parameter is a boolean value indicating whether the user making the - request is a bartender or not - :param history: A list containing the conversation history between the user and the assistant - :param username: The username of the user who initiated the conversation - :param music: The "music" parameter is a boolean value that indicates whether the conversation is - related to music or not. If it is True, the conversation history will be stored in a different file - and the response will be generated using a different model - :return: The function `handle_response` returns a tuple containing the `result` and `MESSAGE_TABLE`. - """ - # TODO: Wykrywać "Pracuję nad odpowiedzią i tego typu rzeczy" - logger.info("Wywolanie procedury openai z promptem: %s", prompt) - temp = {"role": "user", "content": username + ":" + prompt} - if vykidailo or bartender: - logger.info("Administrator coś chciał") - history.append(temp) - if request_type == "MUSIC": - with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: - # First we load existing data into a dict. - file_data = json.load(file_music_memory) - # Join new_data with file_data inside emp_details - file_data.append(temp) - file_music_memory.seek(0) - # convert back to json. - json.dump(file_data, file_music_memory, indent=4) - elif request_type == "RANDOM": - with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: - # First we load existing data into a dict. - file_data = json.load(file_memory) - # Join new_data with file_data inside emp_details - file_data.append(temp) - file_memory.seek(0) - # convert back to json. - json.dump(file_data, file_memory, indent=4) - else: - with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: - # First we load existing data into a dict. - file_data = json.load(file_memory) - # Join new_data with file_data inside emp_details - file_data.append(temp) - file_memory.seek(0) - # convert back to json. - json.dump(file_data, file_memory, indent=4) - history = [] - history.append(GPT_SETTINGS[0]) - chat_gpt_config_request_size = num_tokens_from_string(GPT_SETTINGS[0], "gpt-4") - - for slowo, reakcja in word_reactions.items(): - if not reakcja[3]: - content = ( - "Kiedy słyszysz " - + slowo - + " to reagujesz lub dzieje się to " - + reakcja[0] - ) - temp = {"role": "system", "content": content} - chat_gpt_config_request_size += num_tokens_from_string(temp, "gpt-4") - history.append(temp) - - final_prompt = username + ":" + prompt - logger.debug( - "Rozmiar zapytania przed dodaniem historii %s", chat_gpt_config_request_size - ) - if request_type == "MUSIC": - algorithm = "gpt-4o" - table = message_table_muzyka - token_amount = 10700 - elif request_type == "RANDOM": - algorithm = "gpt-4o" - table = MESSAGE_TABLE - token_amount = 10700 - else: - table = MESSAGE_TABLE - algorithm = "gpt-4o" - token_amount = 10700 - - prompt_gpt_request_size = num_tokens_from_string( - {"role": "user", "content": final_prompt}, "gpt-4" - ) - temptable = [] - for i in reversed(table): - temp_token = num_tokens_from_string(i, "gpt-4") - logger.debug( - "Rozmiar zapytania %s prompt %s temp %s", - chat_gpt_config_request_size, - prompt_gpt_request_size, - temp_token, - ) - if ( - chat_gpt_config_request_size - < token_amount + prompt_gpt_request_size + temp_token - ): - temptable.insert(1, i) - chat_gpt_config_request_size += temp_token - history.extend(temptable) - temp = {"role": "user", "content": final_prompt} - history.append(temp) - logger.info("Rozmiar zapytania po wyslaniu %s", chat_gpt_config_request_size) - try: - response = await openAIClient.chat.completions.create( - model=algorithm, messages=history - ) - except openai.APITimeoutError as e: - # Handle timeout error, e.g. retry or log - result = f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" - except openai.APIConnectionError as e: - result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" - except openai.BadRequestError as e: - # Handle invalid request error, e.g. validate parameters or log - resp, _ = await handle_response( - f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'", - True, - True, - MESSAGE_TABLE, - username, - "RANDOM", - ) - result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" - except openai.APIResponseValidationError as e: - # Handle invalid request error, e.g. validate parameters or log - resp, _ = await handle_response( - f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie 'prompt'", - True, - True, - MESSAGE_TABLE, - username, - "RANDOM", - ) - result = f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" - except openai.AuthenticationError as e: - # Handle authentication error, e.g. check credentials or log - result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" - except openai.PermissionDeniedError as e: - # Handle permission error, e.g. check scope or log - result = f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" - except openai.RateLimitError as e: - result = f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" - except openai.UnprocessableEntityError as e: - result = f"*Kondziu patrzy na terminal. Potem na to co każesz mu wysłać....* Ja wiem że jesteśmy w barze BDSM - ale nie da się włożyć TEGO w TO. *Za jego plecami na terminalu pojawia się:* {e}" - except openai.APIError as e: - # Handle API error, e.g. retry or log - result = f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" - - logger.info("Historia wysłana:") - logger.info(history) - await asyncio.sleep(15) - result = "" - logger.debug("Odpowiedzi") - logger.info(response) - logger.debug(response.choices) - for choice in response.choices: - result += choice.message.content - logger.info("Sformatowane odpowiedzi") - logger.info(result) - temp = {"role": "assistant", "content": result} - history.append(temp) - if request_type == "MUSIC": - with open(MEMORY_FIVE_MUZYKA, "r+", encoding=ENCODING) as file_music_memory: - # First we load existing data into a dict. - file_data = json.load(file_music_memory) - # Join new_data with file_data inside emp_details - file_data.append(temp) - file_music_memory.seek(0) - # convert back to json. - json.dump(file_data, file_music_memory, indent=4) - elif request_type == "RANDOM": - with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: - # First we load existing data into a dict. - file_data = json.load(file_memory) - # Join new_data with file_data inside emp_details - file_data.append(temp) - file_memory.seek(0) - # convert back to json. - json.dump(file_data, file_memory, indent=4) - else: - with open(MEMORY_FIVE_SIARA, "r+", encoding=ENCODING) as file_memory: - # First we load existing data into a dict. - file_data = json.load(file_memory) - # Join new_data with file_data inside emp_details - file_data.append(temp) - file_memory.seek(0) - # convert back to json. - json.dump(file_data, file_memory, indent=4) - return result, MESSAGE_TABLE - - -# define an asynchronous generator -async def async_iterator_generator(range_of_iterable): - """ - Generate asynchronouse iterator. - This is an incomplete function definition for an asynchronous iterator generator that takes a range - of iterable as input. - - :param range_of_iterable: The parameter `range_of_iterable` is likely a range or iterable object - that the async iterator generator will iterate over asynchronously. It could be a list, tuple, set, - or any other iterable object. The generator will yield each item in the iterable object - asynchronously, allowing other code to run in between - """ - # normal loop - for i in range_of_iterable: - # pylint: disable=pointless-string-statement - # yield the result - yield i - """ - Alternatywna wersja przerobienia fora na asynchroniczny. - # traverse the iterable of awaitables - for item in coros: - # await and get the result from the awaitable - result = await item - # report the results - print(result) - """ - - -async def get_stats(ctx, history_limit): - """ - The `get_stats` function retrieves the message history of a specific channel and counts the - frequency of each word in the messages. - - :param ctx: The `ctx` parameter is an object that represents the context of the command being - executed. It contains information such as the message, the author, the server, and other relevant - details - :param history_limit: The `history_limit` parameter is the maximum number of messages to retrieve - from the channel history. It determines how far back in time the statistics will be calculated - :return: The function `get_stats` returns a dictionary `stats` that contains the frequency count of - words found in the messages from the specified channel. - """ - channel_id = 1062047367337095268 - async with ctx.typing(): - stats = {} - channel = client.get_channel(channel_id) - messages = [message async for message in channel.history(limit=history_limit)] - # traverse the iterable of awaitables - for message in messages: - # await and get the result from the awaitable - result = message.content - words = result.split() - for word in words: - if word in stats: - stats[word] += 1 - else: - stats[word] = 1 - for name, how_many in stats.items(): - yield name, how_many - - -async def wyszukaj(ctx, how_many=0, slowa_kluczowe=None): - """ - Take in a context object and an optional integer parameter "how_many" and perform search - operation on music library. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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 how_many: how_many is a parameter that specifies the number of search results to be returned. - It is an optional parameter with a default value of 0, which means that if no value is provided for - how_many, the function will return all search results, defaults to 0 (optional) - """ - # i sieć neuronową z możliwością wyrażenia opinii o dopasowaniu. - if slowa_kluczowe: - word_list = slowa_kluczowe - else: - word_list = ctx.message.content.split() - logger.info("Wyszukuje") - jrequest = { - "lista_slow": word_list, - "dlugosc_plejlisty": how_many, - "UUID": str(uuid.uuid4()), - } - coroutine = asyncio.to_thread( - requests.post, - f"{FILE_SERVICE_ADDRESS}{GET_PLAYLIST}", - json=jrequest, - timeout=360, - ) - - return_data = await coroutine - if not return_data.status_code == 200: - await ctx.send("Wołaj szefa - coś się wyjebało") - return - logger.info(return_data.json()["data"]) - return return_data.json()["data"] - - -async def remove_characters(string, character): - """ - The `remove_characters` function removes all occurrences of a specified character from a given - string. - - :param string: The string parameter is the input string from which characters will be removed - :param character: The character parameter is the character that you want to remove from the string - :return: a new string where all occurrences of the specified character have been removed. - """ - return string.replace(character, "") - - -async def max_weight(lista): - """ - Take a list of weights and return the maximum weight. - - :param lista: It seems like the parameter `lista` is a list of items, possibly representing weights. - The function name `max_weight` suggests that the function is intended to find the maximum weight - from the list. However, without more context or information about the problem, it's difficult to say - for sure what the - """ - maximum_weight = 0 - for iterator in lista: - if iterator[0] > maximum_weight: - maximum_weight = iterator[0] - return maximum_weight - - -# *=========================================== Define Commands - - -@client.hybrid_command( - name="skip_track", - description="Przeskocz kawałek w radiu", - guild=discord.Object(id=664789470779932693), -) -async def skip_track(ctx): - async with ctx.typing(): - allowed = False - for role in ctx.author.roles: - if role.name in ("Thane", "Jarl"): - allowed = True - if not allowed: - await ctx.send("Łapy precz od radia") - else: - coroutine = asyncio.to_thread( - requests.get, - f"{RADIO_HARBOR_ADDRESS}{SKIP_TRACK}", - timeout=360, - ) - result = await coroutine - logger.info("Done %s", result) - await ctx.send("Zrobione szefie!") - - -@client.hybrid_command( - name="zrestartuj_radio", - description="Komenda ktora uruchamia radio ponownie jakby się zawiesiło", - guild=discord.Object(id=664789470779932693), -) -async def zrestartuj_radio(ctx): - async with ctx.typing(): - allowed = False - for role in ctx.author.roles: - if role.name in ("Thane", "Jarl"): - allowed = True - if not allowed: - await ctx.send("Łapy precz od radia") - else: - retcode = subprocess.run( - "/home/pi/Conjurer/restart_radio.sh", - shell=False, - check=False, - capture_output=True, - ) - logger.info("Wynik: %s", retcode) - - - - - - -@client.hybrid_command( - name="update_banlist", - description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", - guild=discord.Object(id=664789470779932693), -) -async def update_banlist(ctx): - """ - Update a banlist in a Discord guild from preprovided list in channel. - - :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It - contains information about the context in which the command was invoked, such as the message, the - channel, the server, and the user who invoked the command. This information can be used to perform - various actions, such - """ - async with ctx.channel.typing(): - allowed = False - for role in ctx.author.roles: - if role.name == "Jarl": - allowed = True - - if ctx.channel.id != 1102190666827702342: - # trunk-ignore(codespell/misspelled) - await ctx.send("Nie wydaje mnie sie") - elif not allowed: - await ctx.send("Idź bo Cię zdziele") - else: - tobancunter = 0 - async with ctx.typing(): - # 1. get list of users posted on channel for banning - channel = ctx.channel - messages = [message async for message in channel.history(limit=None)] - ids_to_ban = [] - for message in messages: - lines = message.content.split("\n") - for line in lines: - match = re.search( - "\\d{18}", - line, - # trunk-ignore(codespell/misspelled) - ) # poprawilem backslash na podwojny bo linter sie czepial - if match: - ids_to_ban.append(match.group()) - tobancunter += 1 - # 2. get list of already banned users for all servers Conjurer - # has rights to - for guild in client.guilds: - bancunter = 0 - # trunk-ignore(codespell/misspelled) - logger.info("Serwer: %s", guild) - current_banlist = [] - permission = True - try: - async for entry in guild.bans(limit=None): - current_banlist.append(entry.user.id) - bancunter += 1 - except BaseException: # pylint: disable=broad-exception-caught - permission = False - if permission: - # 3. compare lists ban only users on list 2 and not on - # list 1 - cunter_counter = 0 - bad_id_cunter_counter = 0 - ban_list = np.setdiff1d(ids_to_ban, current_banlist) - for ident in ban_list: - try: - tmp_user = await client.fetch_user(int(ident)) - await guild.ban( - tmp_user, - reason="Automatyczna lista banów", - delete_message_seconds=0, - ) - cunter_counter += 1 - except discord.NotFound: - logger.info( - "Znaleziono uzyszkodnika ktory juz nie ma konta" - ) - bad_id_cunter_counter += 1 - # 4. return message success/failure - how many banned - await ctx.send( - "Obecnie na serwerze {guild} jest {bancunter} zbanowanych użytkowników. Znaleziono na tym kanale {tobancunter} id użytkowników do zbanowania. Zbanowano {cunter_counter} użytkowników. Na liście jest {bad_id_cunter_counter} użytkowników którzy skasowali już konto" - ) - else: - await ctx.send( - f"Nie mam na serwerze {guild} uprawnień do banowania. :()" - ) - await ctx.send("Zrobione tak czy inaczej") - - -@client.hybrid_command( - nsfw=True, - name="get_image_sadox", - description="Wyświetla losową stronę z losowego komiksu FanSadox. Bardzo NSFW.", - guild=discord.Object(id=664789470779932693), -) -async def get_image_sadox(ctx): - """ - Take in a context parameter and retrieve an image related from fansadox collection. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. In this case, it is likely being used - to determine - """ - logger.info("Get sadox") - channel = ctx.message.channel - async with channel.typing(): - # select random file - res = [] - # Iterate directory - for path in os.listdir(DIR_PATH_SADOX): - # check if current path is a file - if os.path.isfile(os.path.join(DIR_PATH_SADOX, path)): - res.append(path) - filename = res[random.randrange(0, len(res) - 1)] - # select random page - file = open(DIR_PATH_SADOX + filename, "rb") - readpdf = PyPDF2.PdfReader(file) - totalpages = len(readpdf.pages) - page = random.randrange(1, totalpages) - # convert page to image - image = pdf2image.convert_from_path( - DIR_PATH_SADOX + filename, first_page=page, last_page=page - ) - byte_io_stream = io.BytesIO() - image[0].save(byte_io_stream, "JPEG") - byte_io_stream.seek(0) - byte_io_stream.name = "image.jpg" - file = discord.File(byte_io_stream) - await ctx.send(file=file) - logger.info("Get sadox completed") - - -@client.hybrid_command( - name="graj_muzyko", - description="Włącza muzykę na kanale #nocna-zmiana.", - guild=discord.Object(id=664789470779932693), -) -async def graj_muzyko(ctx): - """ - Async function named "graj_muzyko" starts music playback on channel "Nocna Zmiana". - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - - logger.info("Press play on tape") - async with ctx.typing(): - global MUZYKA # pylint: disable=global-variable-not-assigned - MUZYKA["ctx"] = ctx - await connect(ctx=ctx) - await play(ctx=ctx) - logger.info("Press play on tape completed") - check_music.start() - - -@client.hybrid_command( - name="radio_hardkor", - description="Włącza radio Conjurer na kanale #nocna-zmiana.", - guild=discord.Object(id=664789470779932693), -) -async def radio_hardkor(ctx): - """ - Plays the radio hardkor stream in the voice channel. - - Args: - ctx: The context object representing the invocation context. - - Returns: - None - """ - logger.info("Press play on radio hardkor") - async with ctx.typing(): - global MUZYKA # pylint: disable=global-variable-not-assigned - MUZYKA["ctx"] = ctx - if not client.voice_clients: - await connect(ctx=ctx) - voice_client = client.voice_clients[0] - voice_client.play( - discord.PCMVolumeTransformer( - discord.FFmpegPCMAudio("http://95.175.16.246:666/mp3-stream"), - volume=0.3, - ) - ) - logger.info("Press play on radio completed") - else: - logger.error("Already playing") - await asyncio.sleep(12) - check_music.start() - - -@client.hybrid_command( - name="cisza", - description="Wyłącza muzykę i czyści plejliste.", - guild=discord.Object(id=664789470779932693), -) -async def cisza(ctx): - """ - Stop playback of the music and clear the queue. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - logger.info("Stop") - global MUZYKA # pylint: disable=global-variable-not-assigned - MUZYKA["ctx"] = None - voice_client = client.voice_clients[0] - check_music.stop() - if voice_client.is_connected(): - await disconnect(ctx=ctx) - while MUZYKA["queue"]: - MUZYKA["queue"].pop() - MUZYKA["requester"].pop() - logger.info("Stop completed") - - -@client.hybrid_command( - name="dalej", - description="Przerzuca na następny kawałek", - guild=discord.Object(id=664789470779932693), -) -async def dalej(ctx): - """ - Play next track in queue. - - :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It - represents the context in which a command is being executed, including information such as the - message, the channel, the server, and the user who invoked the command. The context object is used - to access and manipulate this - """ - if ctx: - logger.info("Ctx defined for dalej") - logger.info("Next") - voice_client = client.voice_clients[0] - if voice_client.is_connected(): - voice_client.stop() - logger.info("Next completed") - - -@client.hybrid_command( - name="daj_mi_chwile", - description="Pauzuje", - guild=discord.Object(id=664789470779932693), -) -async def daj_mi_chwile(ctx): - """ - Pause music playback. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in most - Discord.py commands and is - """ - if ctx: - logger.info("Ctx defined for pause") - - logger.info("Pause") - voice_client = client.voice_clients[0] - if voice_client.is_connected(): - voice_client.pause() - logger.info("Pause completed") - - -@client.hybrid_command( - name="graj_dalej", - description="Odpauzowuje", - guild=discord.Object(id=664789470779932693), -) -async def graj_dalej(ctx): - """ - Unpause music and continue playback. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - if ctx: - logger.info("Ctx defined for graj_dalej") - logger.info("Unpause") - voice_client = client.voice_clients[0] - if voice_client.is_paused(): - voice_client.resume() - logger.info("Unpause completed") - - -@client.hybrid_command( - name="zagraj_mi_kawalek", - description="Wyszukuje w bibliotece muzycznej Hammera kawałek który ma zostać zagrany", - guild=discord.Object(id=664789470779932693), -) -async def zagraj_mi_kawalek(ctx): - """ - Play a song or music piece in response to a command - triggered by a user in a Discord chat context. - - :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It - contains information about the context in which the command was invoked, such as the message, the - channel, the server, and the user who invoked the command. This information can be used to perform - various actions, such - """ - async with ctx.typing(): - search_time_glob = datetime.now() - logger.info("Zaczynam szukać timestamp %s", datetime.now()) - file = await wyszukaj(ctx=ctx) - logger.info( - "Koniec szukania(timestamp %s zajęło %s", - datetime.now(), - datetime.now() - search_time_glob, - ) - - if file: - for plik in file: - global MUZYKA # pylint: disable=global-variable-not-assigned - MUZYKA["queue"].insert(0, plik[1]) - MUZYKA["requester"].insert(0, ctx.author) - metadata = eyed3.load(plik[1]) - reply = "Dodałem do playlisty {plik[1]} z prywatnej kolekcji Hammera." - if metadata.tag.title: - reply = f"Dodałem do playlisty kawalek {metadata.tag.title}" - if metadata.tag.artist: - reply += f" wykonawcy {metadata.tag.artist}" - if metadata.tag.album: - reply += f" z albumu {metadata.tag.album}" - reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę." - await ctx.send(reply) - else: - await ctx.send( - "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" - ) - - -@client.hybrid_command( - name="dodaj_do_ulubionych", - description="Dodaje do listy ulubionych w radiu", - guild=discord.Object(id=664789470779932693), -) -async def dodaj_do_ulubionych(ctx): - """ - Generate a playlist in queue. First word in this command shall be int defining length of the playlist. - Rest of the line are search terms. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - async with ctx.typing(): - logger.info("Zaczynam szukać timestamp %s", datetime.now()) - - dlugosc_playlisty = ctx.message.content.split()[1] - word_list = ctx.message.content.split() - jrequest = { - "lista_slow": word_list, - "dlugosc_plejlisty": dlugosc_playlisty, - "UUID": str(uuid.uuid4()), - } - coroutine = asyncio.to_thread( - requests.post, - f"{FILE_SERVICE_ADDRESS}{ADD_TO_PRIO_PLAYLIST}", - json=jrequest, - timeout=360, - ) - result = await coroutine - logger.info("Done %s", result) - await ctx.send("Zrobione szefie!") - - -@client.hybrid_command( - name="ja_chciol", - description="Dodaje do listy ulubionych w radiu", - guild=discord.Object(id=664789470779932693), -) -async def request_radio(ctx): - """ - Generate a playlist in queue. First word in this command shall be int defining length of the playlist. - Rest of the line are search terms. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - async with ctx.typing(): - logger.info("Zaczynam szukać timestamp %s", datetime.now()) - - word_list = ctx.message.content.split() - jrequest = { - "lista_slow": word_list, - "UUID": str(uuid.uuid4()), - } - coroutine = asyncio.to_thread( - requests.post, - f"{FILE_SERVICE_ADDRESS}{REQUEST_MUSIC}", - json=jrequest, - timeout=360, - ) - result = await coroutine - logger.info("Done %s", result) - await ctx.send("Zrobione szefie!") - - -@client.hybrid_command( - name="stworz_audycje", - description="Dodaje do listy ulubionych w radiu", - guild=discord.Object(id=664789470779932693), -) -async def stworz_audycje(ctx): - """ - Generate a playlist in queue. First word in this command shall be int defining length of the playlist. - Rest of the line are search terms. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - async with ctx.typing(): - logger.info("Zaczynam szukać timestamp %s", datetime.now()) - - dlugosc_playlisty = ctx.message.content.split()[1] - word_list = ctx.message.content.split() - jrequest = { - "lista_slow": word_list, - "dlugosc_plejlisty": dlugosc_playlisty, - "UUID": str(uuid.uuid4()), - } - coroutine = asyncio.to_thread( - requests.post, - f"{FILE_SERVICE_ADDRESS}{CREATE_PRIO_PLAYLIST}", - json=jrequest, - timeout=360, - ) - result = await coroutine - logger.info("Done %s", result) - await ctx.send("Zrobione szefie!") - - -@client.hybrid_command( - name="wyczysc_ulubione", - description="Czysci liste ulubionych w radiu", - guild=discord.Object(id=664789470779932693), -) -async def wyczysc_ulubione(ctx): - """ - Generate a playlist in queue. First word in this command shall be int defining length of the playlist. - Rest of the line are search terms. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - async with ctx.typing(): - coroutine = asyncio.to_thread( - requests.get, - f"{FILE_SERVICE_ADDRESS}{CLEAR_PRIO}", - timeout=360, - ) - result = await coroutine - logger.info("Done %s", result) - await ctx.send("Zrobione szefie!") - - -@client.hybrid_command( - name="zrob_mi_plejliste", - description="Generuje playliste - param1 - dlugosc, potem slowa wyszukiwania", - guild=discord.Object(id=664789470779932693), -) -async def zrob_mi_plejliste(ctx): - """ - Generate a playlist in queue. First word in this command shall be int defining length of the playlist. - Rest of the line are search terms. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - reply = "Wygenerowana plejlista:\n" - async with ctx.typing(): - logger.info("Zaczynam szukać timestamp %s", datetime.now()) - search_time_glob = datetime.now() - - dlugosc_playlisty = ctx.message.content.split()[1] - file = await wyszukaj(ctx=ctx, how_many=dlugosc_playlisty) - logger.info( - "Koniec szukania(timestamp %s zajęło %s", - datetime.now(), - datetime.now() - search_time_glob, - ) - index = 1 - if file: - for plik in file: - if plik[1]: - logger.info(plik) - global MUZYKA # pylint: disable=global-variable-not-assigned - MUZYKA["queue"].insert(0, plik[1]) - MUZYKA["requester"].insert(0, ctx.author) - metadata = eyed3.load(plik[1]) - if metadata and metadata.tag: - if metadata.tag.title: - reply += f"{index} {metadata.tag.title}" - else: - reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera." - - if metadata.tag.artist: - reply += f" wykonawcy {metadata.tag.artist}" - if metadata.tag.album: - reply += f" z albumu {metadata.tag.album}" - if metadata.tag.title: - reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę.\n" - else: - reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera.\n" - - index += 1 - if len(reply) > 1800: - await ctx.send(reply) - reply = "" - else: - await ctx.send( - "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" - ) - await ctx.send(reply) - logger.info("Plejlista zrobiona") - - - - - -@client.hybrid_command( - name="chata_hammera", - description="Czas od ostatniego incydentu w AbsinthHammerTimeSpaceContinuum", -) -async def chata_hammera(ctx): - """ - Send measured time from last incident in Hammer Fortress to the chat. - - :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It - contains information about the context in which the command was invoked, such as the message, the - channel, the server, and the user who invoked the command. This information can be used to perform - various actions, such - """ - with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file: - # First we load existing data into a dict. - file_data = json.load(new_file) - opis = file_data[-1][0] - czas = datetime.strptime(file_data[-1][1], "%Y-%m-%d %H:%M:%S.%f") - await ctx.send( - f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Od ostatniego incydentu w chacie Hammera minęło {datetime.now() - czas}. Incydent to: {opis}" - ) - - -@client.hybrid_command( - name="historia_incydentow_u_hammera", - description="Wyswietla liste incydentów które miały miejsce w AbsinthHammerTimeSpaceContinuum.", -) -async def historia_incydentow_u_hammera(ctx): - """ - Send a list of incidents in Hammer Fortress to the chat. - - :param ctx: ctx stands for "context" and is a parameter commonly used in Discord.py commands. It - contains information about the context in which the command was invoked, such as the message, the - channel, the server, and the user who invoked the command. This information can be used to perform - various actions, such - """ - with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file_accidents: - # First we load existing data into a dict. - file_data = json.load(new_file_accidents) - return_data = "Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Historia zarejestrownych incydentów u Hammera:\n" - index = 1 - for iter_data in file_data: - opis = iter_data[0] - czas = datetime.strptime(iter_data[1], "%Y-%m-%d %H:%M:%S.%f") - add_data = f"{index}. Opis: {opis} Data: {czas}" - return_data = return_data + add_data + "\n" - index += 1 - await ctx.send(return_data) - - -@client.hybrid_command( - name="reset_the_clock", - description="Resetowanie zegara - dostępne wyłącznie dla Hammera", - guild=discord.Object(id=664789470779932693), -) -async def reset_the_clock(ctx): - """ - Reset the clock on "accidents" in Hammer Fortress adding a new one. - - :param ctx: ctx stands for "context" and is a parameter commonly used 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. This parameter is required in all - Discord.py commands - """ - hammer = False - for role in ctx.message.author.roles: - if role.name == "Bartender": - hammer = True - if hammer: - with open(ACCIDENT_LOG, "r+", encoding=ENCODING) as new_file_accidents: - # First we load existing data into a dict. - file_data = json.load(new_file_accidents) - czas = datetime.strptime(file_data[-1][1], "%Y-%m-%d %H:%M:%S.%f") - opis = ctx.message.content[16:] - accident = accident = [opis, f"{datetime.now()}"] - file_data.append(accident) - new_file_accidents.seek(0) - # convert back to json. - json.dump(file_data, new_file_accidents, indent=4) - await ctx.send( - f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n RESET THE CLOCK. Od ostatniego incydentu w chacie Hammera minęło {czas}. No ale teraz się odpierdoliło: {opis}" - ) - else: - await ctx.send( - f"Komenda mierząca incydenty w Eldritch AbsinthHammerTimeSpaceContinuum.\n Gdzie z łapami do zegara? {client.get_user(346956223645614080).mention} coś albo się komuś stało albo zaraz się stanie jak będzie zegar tykał...." - ) - - -@client.hybrid_command( - name="zagraj_muzyke_mego_ludu", - description="Gra muzyke wyszukana na bazie tematow konwersacji na kanale NZ", - guild=discord.Object(id=664789470779932693), -) -async def zagraj_muzyke_mego_ludu(ctx): - """ - Play completeley random playlist based on messaging history. - - :param ctx: ctx stands for "context" and is typically used in Discord.py commands to represent the - context in which the command was invoked. This includes information such as the message, the - channel, the author, and other relevant details. In this case, it is being passed as a parameter to - the `get_image - """ - stats = {} - if ctx: - logger.info("Zagraj muzyke mego ludu: wywolane") - stat_iter = get_stats(ctx, MUZYKA_MOJEGO_LUDU_HISTORIA) - async for name, how_many in stat_iter: - if len(name) > 3: - stats[name] = how_many - - sorted_stats = sorted(stats.items(), key=lambda x: x[1]) - logger.info("Zagraj muzyke mego ludu:zebrano statystyki") - key_words = [] - for stat in sorted_stats: - key_words.append(stat[0]) - logger.info(sorted_stats[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE]) - logger.info(key_words[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE]) - logger.info(sorted_stats[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) - logger.info(key_words[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) - - final_key_words = key_words[:MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE] - final_key_words.extend(key_words[-MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE:]) - reply = "Wygenerowana plejlista:\n" - async with ctx.typing(): - logger.info( - "Zagraj muzyke mego ludu:Zaczynam szukać timestamp %s", datetime.now() - ) - search_time_glob = datetime.now() - file = await wyszukaj( - ctx=ctx, - how_many=MUZYKA_MOJEGO_LUDU_PLAJLISTA, - slowa_kluczowe=final_key_words, - ) - logger.info("Zagraj muzyke mego ludu:wygenerowano plejliste") - logger.info(file) - logger.info( - "Zagraj muzyke mego ludu: Koniec szukania(timestamp %s zajęło %s", - datetime.now(), - datetime.now() - search_time_glob, - ) - index = 1 - if file: - for plik in file: - global MUZYKA # pylint: disable=global-variable-not-assigned - MUZYKA["queue"].insert(0, plik[1]) - MUZYKA["requester"].insert(0, ctx.author) - metadata = eyed3.load(plik[1]) - if metadata and metadata.tag: - if metadata.tag.title: - reply += f"{index} {metadata.tag.title}" - else: - reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera." - - if metadata.tag.artist: - reply += f" wykonawcy {metadata.tag.artist}" - if metadata.tag.album: - reply += f" z albumu {metadata.tag.album}" - if metadata.tag.title: - reply += " z prywatnej kolekcji Hammera i na Twoją specjalną rpośbę.\n" - else: - reply += f"{index}. {plik[1]} z prywatnej kolekcji Hammera.\n" - - index += 1 - if len(reply) > 1800: - await ctx.send(reply) - reply = "" - else: - await ctx.send( - "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" - ) - await ctx.send(reply) - logger.info("Plejlista zrobiona") - - -@client.hybrid_command( - name="parametry_muzyki_mego_ludu", - description="Konfiguruje komende zagraj muzyke mojego ludu", - guild=discord.Object(id=664789470779932693), -) -async def parametry_muzyki_mego_ludu( - ctx, ile_historii=1500, ile_slow_kluczowych=15, jak_dluga_plejlista=30 -): - """ - The function `parametry_muzyki_mego_ludu` sets global variables for the number of history entries, - number of keywords, and length of playlist for a music application. - - :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating - Discord bots. It represents the context of the command being executed, including information about - the message, the server, and the user who invoked the command - :param ile_historii: The parameter "ile_historii" represents the number of history items for the - music playlist, defaults to 1500 (optional) - :param ile_slow_kluczowych: The parameter "ile_slow_kluczowych" represents the number of keywords or - key phrases related to music that you want to include in your analysis or search, defaults to 30 - (optional) - :param jak_dluga_plejlista: The parameter "jak_dluga_plejlista" determines the length of the - playlist. It specifies how many songs should be included in the playlist, defaults to 30 (optional) - """ - - if ctx: - async with ctx.typing(): - # if ':' in ile_historii: - # _, _, ile_historii = ile_historii.partition(':') - # if ':' in ile_slow_kluczowych: - # _, _, ile_slow_kluczowych = ile_slow_kluczowych.partition(':') - # if ':' in jak_dluga_plejlista: - # _, _, jak_dluga_plejlista = jak_dluga_plejlista.partition(':') - try: - global MUZYKA_MOJEGO_LUDU_HISTORIA # pylint: disable=global-statement - global MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE # pylint: disable=global-statement - global MUZYKA_MOJEGO_LUDU_PLAJLISTA # pylint: disable=global-statement - logger.info( - "Długość historii wyszukiwania: %s\n Ilość słów kluczowych: %s\n Długość plejlisty do wygenerowanie: %s", - MUZYKA_MOJEGO_LUDU_HISTORIA, - MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE, - MUZYKA_MOJEGO_LUDU_PLAJLISTA, - ) - ctx.send( - f"Dotychczasowe wartośći:\n Długość historii wyszukiwania: {MUZYKA_MOJEGO_LUDU_HISTORIA}\n Ilość słów kluczowych: {MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE}\n Długość plejlisty do wygenerowanie:{MUZYKA_MOJEGO_LUDU_PLAJLISTA}" - ) - MUZYKA_MOJEGO_LUDU_HISTORIA = ile_historii - MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE = ile_slow_kluczowych - MUZYKA_MOJEGO_LUDU_PLAJLISTA = jak_dluga_plejlista - logger.info( - "Długość historii wyszukiwania: %s\n Ilość słów kluczowych: %s\n Długość plejlisty do wygenerowanie: %s", - MUZYKA_MOJEGO_LUDU_HISTORIA, - MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE, - MUZYKA_MOJEGO_LUDU_PLAJLISTA, - ) - ctx.send( - f"Zmienione wartośći:\n Długość historii wyszukiwania: {MUZYKA_MOJEGO_LUDU_HISTORIA}\n Ilość słów kluczowych: {MUZYKA_MOJEGO_LUDU_SLOWA_KLUCZOWE}\n Długość plejlisty do wygenerowanie:{MUZYKA_MOJEGO_LUDU_PLAJLISTA}" - ) - except discord.ext.commands.errors.BadArgument as exce: - ctx.send("Spierdalaj") - logger.info(exce) - - -@client.hybrid_command( - name="wyszukaj_linki_do_dokumentow", - description="Szuka linkow doi w bazie crossref i podaje linki do scihuba", - guild=discord.Object(id=664789470779932693), -) -async def wyszukaj_linki_do_dokumentow(ctx): - """ - The function `wyszukaj_linki_do_dokumentow` searches for links to documents in a crossref database and provides links to scihub. - - :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating Discord bots. - It represents the context of the command being executed, including information about the message, the server, - and the user who invoked the command. - """ - query = ctx.message.content - query_uuid = uuid.uuid4() - # TODO: TESTING ONLY!! - # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') - ctx.message.content = ctx.message.content.replace( - "$wyszukaj_linki_do_dokumentow", "" - ) - - json_query = { - "UUID": str(query_uuid), - "query": str(query), - "page": 1, - "deep_search": False, - } - coroutine = asyncio.to_thread( - requests.post, - f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", - json=json_query, - timeout=360, - ) - await ctx.send( - "*Conjurer powoli notuje podane przez Ciebie. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" - + " wrzuca twój liścik do środka* To teraz trza poczekać kilka godzin. Biblioteka to 3/4 stacji." - + " A przecież pół stacji to browar, motelik dla zabaw cielesnych i ten Bar. Więc 12/8 stacji teraz będzie ciężko pracować" - ) - query_response = await coroutine - if not query_response.status_code == 200: - await ctx.send( - "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." - + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" - ) - return - - query, query_uuid, queue_size = ( - query_response.json()["data"][0], - query_response.json()["data"][1], - query_response.json()["data"][2], - ) - if ctx.message.author.nick: - username = ctx.message.author.nick - else: - username = ctx.message.author.name - query_object = QueryControl(username, query_uuid, query, ctx) - OUT_COMM_Q.put(query_object) - await ctx.send( - f"No dobra poszło. Zapamiętaj proszę {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce." - + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni." - ) - - -@client.hybrid_command( - name="glebokie_gardlo", - description="Przygotowuje drinka o nazwie głębokie gardło", - guild=discord.Object(id=664789470779932693), -) -async def wyszukaj_linki_do_dokumentow_deep(ctx): - """ - The function `wyszukaj_linki_do_dokumentow_deep` performs a deep search for links to documents in - a crossref database and provides links to scihub. - - :param ctx: The `ctx` parameter is typically used in Discord.py, a Python library for creating Discord bots. - It represents the context of the command being executed, including information about the message, the server, - and the user who invoked the command. - """ - # TODO: Implement deep search logic here - query = ctx.message.content.replace("$glebokie_gardlo", "") - allowed = False - for role in ctx.message.author.roles: - if role.name == "Bartender": - allowed = True - if role.name == "Scribe": - allowed = True - if role.name == "Thane": - allowed = True - if not allowed: - if ctx.message.author.nick: - username = ctx.message.author.nick - else: - username = ctx.message.author.name - vykidailo = False - bartender = False - prompt = "Przygotuj mi drinka o nazwie Głębokie Gardło, inspirowanego tym sławnym filmem oraz skandalem Watergate" - for role in ctx.message.author.roles: - if role.name == "Vykidailo": - vykidailo = True - if role.name == "Bartender": - bartender = True - global MESSAGE_TABLE # pylint: disable=global-statement - - result, MESSAGE_TABLE = await handle_response( - prompt, vykidailo, bartender, MESSAGE_TABLE, username, "CONVERSATION" - ) - if len(result) < 1500: - await ctx.send(result) - else: - while len(result) > 1500: - await ctx.send(result[:1500]) - result = result[1500:] - return - - query_uuid = uuid.uuid4() - # TODO: TESTING ONLY!! - # query_uuid = uuid.UUID('{12345678-1234-5678-1234-567812345678}') - json_query = { - "UUID": str(query_uuid), - "query": str(query), - "page": 1, - "deep_search": True, - } - coroutine = asyncio.to_thread( - requests.post, - f"{LIBRARIAN_SERVICE_ADDRESS}{SEND_QUERY}", - json=json_query, - timeout=360, - ) - await ctx.send( - "*Conjurer mruga okiem i zamiast drinka wyjmuje dysk usb z terminala. Podchodzi do sprytnie ukrytej przy barze rury od poczty pneumatycznej i" - + " wrzuca ten dysk do środk* To teraz trza poczekać kilka godzin. TO będzie głębokie wyszukanie" - + " Rozgląda się dookoła i stawia przed tobą mętnego drinka o zapachu mięty, gwiezdnego pyłu i oleju silnikowego" - ) - query_response = await coroutine - if not query_response.status_code == 200: - await ctx.send( - "*Z rury wydobywa się dym. Conjurer łapie pierwszą ciecz pod ręką i pryska na rurę. Następuje drobna eksplozja i wszystko zaczyna się palić." - + " Conjurer jest skonfundowany..* Wołaj szefa - mam nieodparte wrażenie że się coś wyjebało" - ) - return - - query, query_uuid, queue_size = ( - query_response.json()["data"][0], - query_response.json()["data"][1], - query_response.json()["data"][2], - ) - if ctx.message.author.nick: - username = ctx.message.author.nick - else: - username = ctx.message.author.name - query_object = QueryControl(username, query_uuid, query, ctx) - OUT_COMM_Q.put(query_object) - await ctx.send( - f"Wypij wypi {query_uuid} - to identyfikator twojego zapytania. Jesteś {queue_size} w kolejce niestety kolejka obowiazuje zawsze." - + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzin - ale mogą być też dni. Głębokie zabawy trwają dłużej. *Znowu mruga*" - ) - +# TODO: ADMINISTRATION # *================================== Run diff --git a/voice_recognition_commands.py b/voice_recognition_commands.py index 3626779..67b9be5 100644 --- a/voice_recognition_commands.py +++ b/voice_recognition_commands.py @@ -1,11 +1,12 @@ -import logging -import wave import asyncio +import logging import netrc import time -import discord +import wave + import assemblyai as aai -from discord.ext import commands, voice_recv, tasks +import discord +from discord.ext import commands, tasks, voice_recv from discord.opus import Decoder as OpusDecoder # Replace with your API key @@ -19,37 +20,45 @@ CHANNELS = OpusDecoder.CHANNELS SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS SAMPLING_RATE = OpusDecoder.SAMPLING_RATE -#rotate file after there is 0.5s between last received pcm for user. -#delete messsages after user disconnect +# rotate file after there is 0.5s between last received pcm for user. +# delete messsages after user disconnect + +LOCATION = "/home/pi/Conjurer/transcripts/" -logger = logging.getLogger("discord") -location = "/home/pi/Conjurer/transcripts/" class CommunicationObject: def __init__(self, msg_type, user, data): + self.logger = logging.getLogger("discord") self.type = msg_type self.user = user self.data = data + def __repr__(self): return f"{self.type} : {self.user} : {self.data}" + class WaveWriter: - def __init__(self, user_id, queue): + def __init__(self, user_id, queue, location): self.queue = queue self.user = user_id self.username = str(user_id.id) - #here we can add hashing function to make transcription files not possible to be connected with discord id + self.location = location + # here we can add hashing function to make transcription files not possible to be connected with discord id self.present_file_id = 0 self.file_name_past = None - self.file_name_present = location + self.username + "_" + str(self.present_file_id) + ".mp3" - self.transcript_file_present : wave.Wave_write = wave.open( + self.file_name_present = ( + self.location + self.username + "_" + str(self.present_file_id) + ".mp3" + ) + self.transcript_file_present: wave.Wave_write = wave.open( self.file_name_present, "wb" ) self.transcript_file_present.setnchannels(CHANNELS) self.transcript_file_present.setsampwidth(SAMPLE_WIDTH) self.transcript_file_present.setframerate(SAMPLING_RATE) - self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" - self.transcript_file_future : wave.Wave_write = wave.open( + self.file_name_future = ( + self.location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" + ) + self.transcript_file_future: wave.Wave_write = wave.open( self.file_name_future, "wb" ) self.transcript_file_future.setnchannels(CHANNELS) @@ -66,14 +75,18 @@ class WaveWriter: self.present_file_id += 1 self.file_name_present = self.file_name_future self.transcript_file_present = self.transcript_file_future - self.file_name_future = location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" - self.transcript_file_future : wave.Wave_write = wave.open( + self.file_name_future = ( + self.location + self.username + "_" + str(self.present_file_id + 1) + ".mp3" + ) + self.transcript_file_future: wave.Wave_write = wave.open( self.file_name_future, "wb" ) self.transcript_file_future.setnchannels(CHANNELS) self.transcript_file_future.setsampwidth(SAMPLE_WIDTH) self.transcript_file_future.setframerate(SAMPLING_RATE) - operation = CommunicationObject(msg_type="send_file",user=self.user,data=[self.file_name_past]) + operation = CommunicationObject( + msg_type="send_file", user=self.user, data=[self.file_name_past] + ) await self.queue.put(operation) @@ -81,7 +94,7 @@ class WaveWriter: self.transcript_file_present.writeframes(pcmdata) def cleanup(self): - logger.info("Cleanup for user %s", self.username) + self.logger.info("Cleanup for user %s", self.username) self.transcript_file_present.close() self.transcript_file_future.close() @@ -90,17 +103,26 @@ class SRBuffer(voice_recv.AudioSink): """Endpoint AudioSink that generates a wav file. Best used in conjunction with a silence generating sink. (TBD) """ + # on member join dodajemytypa do listy # on member disconnect - dropujemy go - def __init__(self, queue): + def __init__(self, queue, location): super().__init__() self.queue = queue self.wavewriter = {} + self.logger = logging.getLogger("discord") + self.location = location def on_user_connect(self, username): - self.wavewriter[str(username.id)] = [WaveWriter(username, self.queue), time.time_ns(), time.time_ns(), False, False] + self.wavewriter[str(username.id)] = [ + WaveWriter(username, self.queue, self.location), + time.time_ns(), + time.time_ns(), + False, + False, + ] - def on_user_disconnect(self,username): + def on_user_disconnect(self, username): self.wavewriter[str(username.id)].cleanup() self.wavewriter.pop(str(username.id)) @@ -108,21 +130,23 @@ class SRBuffer(voice_recv.AudioSink): return False def write(self, user, data) -> None: - #logger.info("DAta write") + # logger.info("DAta write") if user: self.wavewriter[str(user.id)][0].writeframes(data.pcm) - self.wavewriter[str(user.id)][1] = time.time_ns() #time from last write - self.wavewriter[str(user.id)][3] = True #data written in last iter - self.wavewriter[str(user.id)][4] = True #data in buff + self.wavewriter[str(user.id)][1] = time.time_ns() # time from last write + self.wavewriter[str(user.id)][3] = True # data written in last iter + self.wavewriter[str(user.id)][4] = True # data in buff time.sleep(0.001) def cleanup(self) -> None: try: - logger.info("Cleanup for SRBuffer") + self.logger.info("Cleanup for SRBuffer") for item in self.wavewriter.values(): item[0].cleanup() except Exception: - logger.warning("WaveSink got error closing file on cleanup", exc_info=True) + self.logger.warning( + "WaveSink got error closing file on cleanup", exc_info=True + ) class Transcriber(commands.Cog): @@ -130,41 +154,53 @@ class Transcriber(commands.Cog): self.bot = bot self.threads = [] self.comm_queue = asyncio.Queue() - self.wsink = SRBuffer(self.comm_queue) + self.wsink = SRBuffer(self.comm_queue, LOCATION) self.worker = None self.mc = None self.vc = None + self.logger = logging.getLogger("discord") @commands.hybrid_command(name="transcribe") async def test(self, ctx): if self.vc: - vc = self.vc #to juz powinien byc voice channel z funkcja conenct + vc = self.vc # to juz powinien byc voice channel z funkcja conenct else: vc = None self.mc = ctx.message.channel if self.bot.voice_clients: - if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient): - logger.debug("Already transcribing") + if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient): + self.logger.debug("Already transcribing") else: - logger.debug("Already connected with other client") + self.logger.debug("Already connected with other client") else: - logger.debug("Connected") + self.logger.debug("Connected") vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) - logger.info(self.bot.voice_clients) + self.logger.info(self.bot.voice_clients) self.check_data.start() self.worker = asyncio.create_task(self.transcribe_output_queue()) vc.listen(self.wsink) - @tasks.loop(seconds=0.5) async def check_data(self): for item in self.wsink.wavewriter.values(): if not item[3]: timediff_rotation = time.time_ns() - item[2] - timediff_write = time.time_ns() -item[1] - if timediff_rotation > 13000149433 and timediff_write > 500014943 and item[4]: - logger.info("File rotation time since last write %s %s", timediff_write, timediff_write/1e9) - logger.info("File rotation time since last rotation %s %s",timediff_rotation, timediff_rotation/1e9) + timediff_write = time.time_ns() - item[1] + if ( + timediff_rotation > 13000149433 + and timediff_write > 500014943 + and item[4] + ): + self.logger.info( + "File rotation time since last write %s %s", + timediff_write, + timediff_write / 1e9, + ) + self.logger.info( + "File rotation time since last rotation %s %s", + timediff_rotation, + timediff_rotation / 1e9, + ) item[2] = time.time_ns() item[4] = False await item[0].rotate() @@ -173,67 +209,71 @@ class Transcriber(commands.Cog): @commands.Cog.listener() async def on_voice_state_update(self, user, before, after): if user == self.bot.user: - logger.debug("Ignoring self") + self.logger.debug("Ignoring self") return if before.channel is None: - logger.info("User %s connected to channel %s", user, after.channel.name) + self.logger.info( + "User %s connected to channel %s", user, after.channel.name + ) self.wsink.on_user_connect(user) - elif after.channel is None: - logger.info("User %s disconnected from channel %s", user, before.channel.name) - operation = CommunicationObject(msg_type="user_cleanup", user=user, data=None) + elif after.channel is None: + self.logger.info( + "User %s disconnected from channel %s", user, before.channel.name + ) + operation = CommunicationObject( + msg_type="user_cleanup", user=user, data=None + ) await self.comm_queue.put(operation) else: - logger.debug("User VC status changed %s", user.id) - logger.debug("Before %s", before) - logger.debug("After %s", after) - + self.logger.debug("User VC status changed %s", user.id) + self.logger.debug("Before %s", before) + self.logger.debug("After %s", after) @commands.command(name="stop_transcribe") async def stop(self, ctx): self.check_data.stop() - stop_token = CommunicationObject("STOP",None,None) + stop_token = CommunicationObject("STOP", None, None) await self.comm_queue.put(stop_token) await ctx.voice_client.disconnect() async def transcribe_output_queue(self): - logger.info("Transcript uploader start") + self.logger.info("Transcript uploader start") config = aai.TranscriptionConfig(language_code="pl") transcriber = aai.Transcriber() - logger.debug("Transcriber queue id: %s",id(self.comm_queue)) + self.logger.debug("Transcriber queue id: %s", id(self.comm_queue)) while True: - logger.debug("waiting for tasks") + self.logger.debug("waiting for tasks") item = await self.comm_queue.get() - logger.debug("Got %s", item) + self.logger.debug("Got %s", item) if "STOP" in item.type: - logger.debug("Queue ended") + self.logger.debug("Queue ended") break elif "send_file" in item.type: - logger.debug("Sending file for transcription") + self.logger.debug("Sending file for transcription") transcript = None for iter in item.data: try: coro = asyncio.to_thread( - transcriber.transcribe, - iter, - config=config + transcriber.transcribe, iter, config=config ) transcript = await coro except Exception as e: - logger.warn("Exceptiom occured %s", e) + self.logger.warn("Exceptiom occured %s", e) await self.mc.send(f"{item.user} : {transcript.text}") if transcript.error: - logger.error(transcript.error) + self.logger.error(transcript.error) raise AssertionError elif "user_cleanup" in item.type: - logger.info("User %s disconnected - cleanup action") + self.logger.info("User %s disconnected - cleanup action") else: - logger.warn("Something went wrong with object %s", item) - logger.info("Transcript uploader stoped") + self.logger.warn("Something went wrong with object %s", item) + self.logger.info("Transcript uploader stoped") + async def setup(bot): logger = logging.getLogger("discord") await bot.add_cog(Transcriber(bot)) - logger.info("Loading voice transcribed module done") + logger.info("Loading voice transcriber module done") From 972387f6607e7b60e0c05be9430d593250df4d9e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 15:09:46 +0100 Subject: [PATCH 169/283] Deploy script refactor --- deploy.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index 3793d1e..2eb3401 100755 --- a/deploy.sh +++ b/deploy.sh @@ -4,12 +4,16 @@ git pull cp ./deploy.sh /home/pi/ cd /home/pi || exit cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done + + cp ./conjurer/settings.json ./Conjurer/ cp ./conjurer/system_gpt_settings.json ./Conjurer/ -cp ./conjurer/communication_subroutine.py ./Conjurer/ -cp ./conjurer/voice_recognition_commands.py ./Conjurer/ + + +cp ./conjurer/administration_commands.py ./Conjurer/ cp ./conjurer/ai_commands.py ./Conjurer/ cp ./conjurer/ai_functions.py ./Conjurer/ +cp ./conjurer/communication_subroutine.py ./Conjurer/ cp ./conjurer/constants.py ./Conjurer/ cp ./conjurer/librarian_commands.py ./Conjurer/ cp ./conjurer/music_commands.py ./Conjurer/ @@ -17,6 +21,7 @@ cp ./conjurer/music_functions.py ./Conjurer/ cp ./conjurer/other_commands.py ./Conjurer/ cp ./conjurer/other_functions.py ./Conjurer/ cp ./conjurer/radio_commands.py ./Conjurer/ +cp ./conjurer/voice_recognition_commands.py ./Conjurer/ cp ./conjurer/thin_client.py ./Conjurer/ sudo systemctl restart conjurer.service From f0d0ebb859398f83ea6984a7726a36bd3312b55a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 15:16:39 +0100 Subject: [PATCH 170/283] fx --- administration_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administration_commands.py b/administration_commands.py index 3e2e42c..c480e37 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -11,7 +11,7 @@ import numpy as np from discord.ext import commands, tasks from ai_functions import get_random_cyclic_message -from constants import ENCODING, LOGFILE, LOGSTORE, MEMORY_FIVE_MUZYKA, MEMORY_FIVE_SIARA +from constants import ENCODING, LOGFILE, LOGSTORE, MEMORY_FIVE_MUZYKA, MEMORY_FIVE_SIARA, LAST_SPONTANEOUS_CALL, TIME_BETWEEN_CALLS class AdministrationModule(commands.Cog): From d56b0b7f1d39602f78d1b52921ecb01cdd067ec3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 15:17:31 +0100 Subject: [PATCH 171/283] fx2 --- deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index 2eb3401..75b6639 100755 --- a/deploy.sh +++ b/deploy.sh @@ -3,7 +3,7 @@ cd /home/pi/conjurer || exit git pull cp ./deploy.sh /home/pi/ cd /home/pi || exit -cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done +#cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done cp ./conjurer/settings.json ./Conjurer/ @@ -22,6 +22,6 @@ cp ./conjurer/other_commands.py ./Conjurer/ cp ./conjurer/other_functions.py ./Conjurer/ cp ./conjurer/radio_commands.py ./Conjurer/ cp ./conjurer/voice_recognition_commands.py ./Conjurer/ -cp ./conjurer/thin_client.py ./Conjurer/ +cp ./conjurer/thin_client.py ./Conjurer/bot.py sudo systemctl restart conjurer.service From 6aec5d05d26529fa27f5b1dca05e2c4f0e01ac32 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 15:38:59 +0100 Subject: [PATCH 172/283] bgfx --- librarian_commands.py | 2 +- other_commands.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/librarian_commands.py b/librarian_commands.py index 1210331..e3c67e8 100644 --- a/librarian_commands.py +++ b/librarian_commands.py @@ -187,7 +187,7 @@ class DataModule(commands.Cog): description="Przygotowuje drinka o nazwie głębokie gardło", guild=discord.Object(id=664789470779932693), ) - async def wyszukaj_linki_do_dokumentow_deep(ctx): + async def wyszukaj_linki_do_dokumentow_deep(self, ctx): """ The function `wyszukaj_linki_do_dokumentow_deep` performs a deep search for links to documents in a crossref database and provides links to scihub. diff --git a/other_commands.py b/other_commands.py index 3dbab50..22e4691 100644 --- a/other_commands.py +++ b/other_commands.py @@ -20,7 +20,7 @@ class OtherModule(commands.Cog): @commands.hybrid_command( name="przytul", description="Przytul kogoś - daj mention po komendzie :)" ) - async def przytul(ctx, arg: Optional[discord.Member] = None): + async def przytul(self, ctx, arg: Optional[discord.Member] = None): """ Generate a text about hugging mentioned user. @@ -105,7 +105,7 @@ class OtherModule(commands.Cog): name="chata_hammera", description="Czas od ostatniego incydentu w AbsinthHammerTimeSpaceContinuum", ) - async def chata_hammera(ctx): + async def chata_hammera(self, ctx): """ Send measured time from last incident in Hammer Fortress to the chat. @@ -127,7 +127,7 @@ class OtherModule(commands.Cog): name="historia_incydentow_u_hammera", description="Wyswietla liste incydentów które miały miejsce w AbsinthHammerTimeSpaceContinuum.", ) - async def historia_incydentow_u_hammera(ctx): + async def historia_incydentow_u_hammera(self, ctx): """ Send a list of incidents in Hammer Fortress to the chat. From baeb3de67e5a429fd3944d32210fef98579ccac4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 16:10:26 +0100 Subject: [PATCH 173/283] fxbilocate --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index b68aeeb..be196f1 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -73,7 +73,7 @@ class Events(commands.Cog): await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel - await self.bot.process_commands(message) + #await self.bot.process_commands(message) #uncomment to bilocate message.content = message.content.lower() From ab199fc285c8e41a77e549d74ab8cc14c15312c7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 16:23:27 +0100 Subject: [PATCH 174/283] attachment test --- music_commands.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/music_commands.py b/music_commands.py index 4a1b7f1..ca1c9f9 100644 --- a/music_commands.py +++ b/music_commands.py @@ -597,7 +597,15 @@ class MusicModule(commands.Cog): self.logger.error("Not possible to connect to voice") return self.logger.info("Connected to voice") - + @commands.hybrid_command( + name="batch_downloade", + description="Zaciąga obiekty z pliku", + guild=discord.Object(id=664789470779932693), + ) + async def batch_download(self,ctx, attachment): + self.ctx.reply(attachment.content_type) + contents = await attachment.read() + self.logger.info(contents) async def disconnect(self, ctx): """ Asynchronous Python function that disconnects the voice client if it is connected and From d53feec56399f741538f4ff31dd90e310d1017a3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 16:26:55 +0100 Subject: [PATCH 175/283] fx --- music_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_commands.py b/music_commands.py index ca1c9f9..7bdebe1 100644 --- a/music_commands.py +++ b/music_commands.py @@ -598,7 +598,7 @@ class MusicModule(commands.Cog): return self.logger.info("Connected to voice") @commands.hybrid_command( - name="batch_downloade", + name="batch_download", description="Zaciąga obiekty z pliku", guild=discord.Object(id=664789470779932693), ) From 73f184b263dfab5f85e4ab754917bde3f3b10641 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 17:00:29 +0100 Subject: [PATCH 176/283] Batch fix --- music_commands.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/music_commands.py b/music_commands.py index 7bdebe1..7083e4d 100644 --- a/music_commands.py +++ b/music_commands.py @@ -597,15 +597,17 @@ class MusicModule(commands.Cog): self.logger.error("Not possible to connect to voice") return self.logger.info("Connected to voice") + @commands.hybrid_command( name="batch_download", description="Zaciąga obiekty z pliku", guild=discord.Object(id=664789470779932693), ) - async def batch_download(self,ctx, attachment): - self.ctx.reply(attachment.content_type) - contents = await attachment.read() - self.logger.info(contents) + async def batch_download(self, ctx): + self.logger.info(ctx.message.attachments[0].content_type) + file_bytes = ctx.message.attachments[0].read() + self.logger.info(file_bytes.decode(encoding="utf-8")) + async def disconnect(self, ctx): """ Asynchronous Python function that disconnects the voice client if it is connected and From 63366f7ea1a7600f827f1a05b77b371b08d846d6 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 18:25:12 +0100 Subject: [PATCH 177/283] work ongoing --- conjurer_musician/radio_conjurer.liq | 5 ++--- music_commands.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index 143f3b8..198e94f 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -67,7 +67,7 @@ s = add([s, b]) a = interactive.float("main_volume", min=0., max=20., 0.4) s = compress.multiband.interactive(bands=7, s) -mic_gain = interactive.float("mic_volume", min=0., max=20., 6.) +mic_gain = interactive.float("mic_volume", min=0., max=120., 6.) # Apply audio processing effects tmic = buffer(input.pulseaudio()) # Microphone mic = amplify(mic_gain, tmic) @@ -75,12 +75,11 @@ mic = gate(threshold=-80., range=-120., mic) mic = compress(threshold=0., ratio=2.,mic) mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) -s = add([s, mic]) # Apply audio processing effects s = nrj(normalize(s)) s = amplify(a, s) # Skip blank sections in the stream -s = blank.skip(max_blank=10., s) +s = blank.skip(max_blank=10., s)192 #Manual audition override source_control = interactive.bool("Source control", true) diff --git a/music_commands.py b/music_commands.py index 7083e4d..a53591f 100644 --- a/music_commands.py +++ b/music_commands.py @@ -604,9 +604,19 @@ class MusicModule(commands.Cog): guild=discord.Object(id=664789470779932693), ) async def batch_download(self, ctx): - self.logger.info(ctx.message.attachments[0].content_type) - file_bytes = ctx.message.attachments[0].read() - self.logger.info(file_bytes.decode(encoding="utf-8")) + content_type = ctx.message.attachments[0].contluzent_type + check = re.search ("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) + if check: + file_bytes = await ctx.message.attachments[0].read() + text = file_bytes.decode(encoding=check.group(1)) + self.logger.info(text) + lines = text.split("\n") + for item in lines: + self.logger.info(item) + + else: + ctx.reply("Spierdalaj") + async def disconnect(self, ctx): """ From 489dd9feb65ad8b21d3f98605a78b46075ac1d1c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 18:31:45 +0100 Subject: [PATCH 178/283] cat got more sleep --- music_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_commands.py b/music_commands.py index a53591f..c66ed7e 100644 --- a/music_commands.py +++ b/music_commands.py @@ -604,7 +604,7 @@ class MusicModule(commands.Cog): guild=discord.Object(id=664789470779932693), ) async def batch_download(self, ctx): - content_type = ctx.message.attachments[0].contluzent_type + content_type = ctx.message.attachments[0].content_type check = re.search ("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) if check: file_bytes = await ctx.message.attachments[0].read() From c225a3e2526b536d1b0f15bbc54ce054047e506e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 19:08:59 +0100 Subject: [PATCH 179/283] Batch download --- music_commands.py | 13 ++++++++++--- music_functions.py | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/music_commands.py b/music_commands.py index c66ed7e..ecbf242 100644 --- a/music_commands.py +++ b/music_commands.py @@ -611,9 +611,16 @@ class MusicModule(commands.Cog): text = file_bytes.decode(encoding=check.group(1)) self.logger.info(text) lines = text.split("\n") - for item in lines: - self.logger.info(item) - + for link in lines: + pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" + if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): + self.logger.info("%s to link do jutuba", link) + elif re.match(pat, link) and re.match(".*spotify.*", link): + self.logger.info("%s to link do spotify", link) + elif re.match(pat, link): + self.logger.info("%s to link do czegos", link) + else: + ctx.reply("Spierdalaj") else: ctx.reply("Spierdalaj") diff --git a/music_functions.py b/music_functions.py index bb005c5..78372f4 100644 --- a/music_functions.py +++ b/music_functions.py @@ -150,9 +150,10 @@ async def get_file(ctx, source, link): elif source == "Youtube": link_ok = False pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" - if re.match(pat, link) and re.match(".*youtube.*", link): + if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): link_ok = True + if link_ok: dir_path = "" file_list = [] From 3d02d06862855a70f3ad3863a0244423e3874288 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 19:48:47 +0100 Subject: [PATCH 180/283] FX --- ai_commands.py | 11 ++++++----- conjurer_musician/radio_conjurer.liq | 13 ++++++++++--- music_commands.py | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index be196f1..5e4b2ba 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -16,6 +16,7 @@ from constants import ( INITIAL_TIME_WAIT, MASTER_TIMEOUT, OPENAICLIENT, + WORD_REACTIONS ) @@ -85,13 +86,13 @@ class Events(commands.Cog): await message.reply(DATA["fabryczka"]) if tdelta > INITIAL_TIME_WAIT: - for word in self.word_reactions: + for word in WORD_REACTIONS: if re.search(r"\b" + word + r"\b", message.content): - tdelta = datetime.now() - self.word_reactions[word][2] + tdelta = datetime.now() - WORD_REACTIONS[word][2] tdelta = tdelta.total_seconds() - security_clearance = self.word_reactions[word][4] - reaction = self.word_reactions[word][3] - if tdelta > self.word_reactions[word][1]: + security_clearance = WORD_REACTIONS[word][4] + reaction = WORD_REACTIONS[word][3] + if tdelta > WORD_REACTIONS[word][1]: # TODO: to zrobic reactiony self.logger.info("Ping z procedury reakcji") if reaction: diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index 198e94f..998401b 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -56,7 +56,7 @@ s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", trac # Set up an interactive harbor for controlling the stream interactive.harbor(port = 9999) - +harbor.input(buffer=30.0) # Set up interactive controls for bass boost f = interactive.float("f", description="Frequency", min=0., max=1000., unit="Hz", 200.) g = interactive.float("g", description="Gain", min=0., max=20., unit="dB", 8.) @@ -82,8 +82,15 @@ s = amplify(a, s) s = blank.skip(max_blank=10., s)192 #Manual audition override -source_control = interactive.bool("Source control", true) -s = switch(track_sensitive=false, [(source_control,mic),({true},s)]) +live_enabled = interactive.bool("Going Live!", true) +s = switch(track_sensitive=false, + [({!live_enabled}, live), + ({true}, radio)]) + + +def fading_transition(a,b) + sequence([fade.out(a.source),fade.in(b.source)]) +end # Configure logging settings log_to_stdout = true diff --git a/music_commands.py b/music_commands.py index ecbf242..1aa37dc 100644 --- a/music_commands.py +++ b/music_commands.py @@ -612,6 +612,7 @@ class MusicModule(commands.Cog): self.logger.info(text) lines = text.split("\n") for link in lines: + self.logger.info(link) pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): self.logger.info("%s to link do jutuba", link) From bda7c6cc2ea06b56ab8e29ea83e770b791fb5145 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 19:53:22 +0100 Subject: [PATCH 181/283] fx --- music_commands.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/music_commands.py b/music_commands.py index 1aa37dc..8195cc6 100644 --- a/music_commands.py +++ b/music_commands.py @@ -614,6 +614,7 @@ class MusicModule(commands.Cog): for link in lines: self.logger.info(link) pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" + self.logger.info("Verification") if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): self.logger.info("%s to link do jutuba", link) elif re.match(pat, link) and re.match(".*spotify.*", link): @@ -621,9 +622,11 @@ class MusicModule(commands.Cog): elif re.match(pat, link): self.logger.info("%s to link do czegos", link) else: - ctx.reply("Spierdalaj") + self.logger.info("Spierdalaj") + ctx.message.reply("Spierdalaj") else: - ctx.reply("Spierdalaj") + self.logger.info("Spierdalaj") + ctx.message.reply("Spierdalaj") async def disconnect(self, ctx): From 061bab8e6ddb7903f96aa6cf7153e06f81f52474 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 20:10:44 +0100 Subject: [PATCH 182/283] Batch hopefully --- music_commands.py | 100 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/music_commands.py b/music_commands.py index 8195cc6..3126a54 100644 --- a/music_commands.py +++ b/music_commands.py @@ -606,27 +606,85 @@ class MusicModule(commands.Cog): async def batch_download(self, ctx): content_type = ctx.message.attachments[0].content_type check = re.search ("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) - if check: - file_bytes = await ctx.message.attachments[0].read() - text = file_bytes.decode(encoding=check.group(1)) - self.logger.info(text) - lines = text.split("\n") - for link in lines: - self.logger.info(link) - pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" - self.logger.info("Verification") - if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): - self.logger.info("%s to link do jutuba", link) - elif re.match(pat, link) and re.match(".*spotify.*", link): - self.logger.info("%s to link do spotify", link) - elif re.match(pat, link): - self.logger.info("%s to link do czegos", link) - else: - self.logger.info("Spierdalaj") - ctx.message.reply("Spierdalaj") - else: - self.logger.info("Spierdalaj") - ctx.message.reply("Spierdalaj") + await ctx.reply("Kurwa. Aleś mi roboty narobił... No nic. Ku radości. Skal!") + with ctx.typing(): + if check: + file_bytes = await ctx.message.attachments[0].read() + text = file_bytes.decode(encoding=check.group(1)) + self.logger.info(text) + lines = text.split("\n") + for link in lines: + link = link.strip() + self.logger.info(link) + pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" + self.logger.info("Verification") + if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): + self.logger.info("%s to link do jutuba", link) + dir_path, files = await music_functions.get_file( + ctx, "Youtube", link + ) + if files: + for file_iter in files: + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert(0, file_iter) + music_functions.MUSIC_FILE_LIST.update_file_list(file_iter) + MUZYKA["requester"].insert(0, ctx.author) + await ctx.send(f"Dodałem do listy {file_iter}") + self.logger.info("Jutub udany") + + elif re.match(pat, link) and re.match(".*spotify.*", link): + self.logger.info("%s to link do spotify", link) + dir_path, files = await music_functions.get_file( + ctx, "Spotify", link + ) + if platform == "win32": + separator = "\\" + else: + separator = "/" + for file in files: + file_path = ( + dir_path + + separator + + file["artist"] + + " - " + + file["name"] + + ".mp3" + ) + global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert( + 0, + dir_path + + separator + + file["artist"] + + " - " + + file["name"] + + ".mp3", + ) + MUZYKA["requester"].insert(0, ctx.author) + music_functions.MUSIC_FILE_LIST.update_file_list(file_path) + await ctx.send( + f"Dodałem do listy {dir_path}{file['artist']} - + {file['name']}.mp3" + ) + self.logger.info("Spotifaj udany") + + + elif re.match(pat, link): + self.logger.info("%s to link do czegos", link) + sciezka,files = await music_functions.get_file( + ctx, "Pornol", link + ) + if files: + self.logger.info("Pornol udany") + await ctx.send( + f"Jest w tajnym archiwum pod adresem{sciezka})" + ) + + else: + self.logger.info("Spierdalaj") + await ctx.message.reply("Spierdalaj") + else: + self.logger.info("Spierdalaj") + await ctx.message.reply("Spierdalaj") async def disconnect(self, ctx): From f0bb8f7ac2d00a908c4757a1f6c30255bf4a1f1f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 20:13:06 +0100 Subject: [PATCH 183/283] glbfx --- music_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/music_commands.py b/music_commands.py index 3126a54..e628725 100644 --- a/music_commands.py +++ b/music_commands.py @@ -609,6 +609,7 @@ class MusicModule(commands.Cog): await ctx.reply("Kurwa. Aleś mi roboty narobił... No nic. Ku radości. Skal!") with ctx.typing(): if check: + global MUZYKA # pylint: disable=global-variable-not-assigned file_bytes = await ctx.message.attachments[0].read() text = file_bytes.decode(encoding=check.group(1)) self.logger.info(text) From bbc91fbd7356e8d532de38a0b14de183a62ad03c Mon Sep 17 00:00:00 2001 From: Michal T <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 20:11:55 +0000 Subject: [PATCH 184/283] last fix --- music_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music_commands.py b/music_commands.py index e628725..e2d0d96 100644 --- a/music_commands.py +++ b/music_commands.py @@ -607,7 +607,7 @@ class MusicModule(commands.Cog): content_type = ctx.message.attachments[0].content_type check = re.search ("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) await ctx.reply("Kurwa. Aleś mi roboty narobił... No nic. Ku radości. Skal!") - with ctx.typing(): + async with ctx.typing(): if check: global MUZYKA # pylint: disable=global-variable-not-assigned file_bytes = await ctx.message.attachments[0].read() @@ -651,7 +651,7 @@ class MusicModule(commands.Cog): + file["name"] + ".mp3" ) - global MUZYKA # pylint: disable=global-variable-not-assigned + MUZYKA["queue"].insert( 0, dir_path From dbe33286dacb866915f161c1bc947c73334359df Mon Sep 17 00:00:00 2001 From: migatu <mtuszowski@gmail.com> Date: Sun, 10 Nov 2024 23:01:00 +0000 Subject: [PATCH 185/283] Final fixes in radio --- radio_conjurer.liq | 178 +++++++++++++++++++++++++++++++++++++++++++++ test_mic2.liq | 4 + test_mic3.liq | 4 + 3 files changed, 186 insertions(+) create mode 100644 radio_conjurer.liq create mode 100644 test_mic2.liq create mode 100644 test_mic3.liq diff --git a/radio_conjurer.liq b/radio_conjurer.liq new file mode 100644 index 0000000..a66ae62 --- /dev/null +++ b/radio_conjurer.liq @@ -0,0 +1,178 @@ +# FILEPATH: /home/mtuszowski/conjurer/conjurer_musician/radio_conjurer.liq + +# This script sets up a Liquidsoap radio stream with various features and configurations. + +# Load icecast credentials from a JSON file +let json.parse credentials = file.contents("/home/pi/Conjurer/icecast_credentials.json") + +# Enable replaygain metadata processing +enable_replaygain_metadata() + +# Set up a playlog for tracking played tracks + +l = playlog(duration = 72000.0, persistency="/home/pi/Conjurer/persistence.log") + +# Function to check if a track can be played based on its metadata +def check(r) + m = request.metadata(r) + if l.last(m) < 36000. then + log.info("Rejecting #{m['filename']} (played #{l.last(m)}s ago).") + false + else + l.add(m) + true + end +end + +# Define playlists to be used in the stream +s1 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/all_playlist.playlist")) +s2 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/priority_queue.playlist")) +s3 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/hit.playlist")) + +# Create a request queue for user-generated requests +requests_queue = request.queue() + +# Function to process the request queue and add new requests +def queue_processing() + text=file.lines("/home/pi/Conjurer/request.playlist") + if text != [] then + list.iter(fun(item) -> requests_queue.push.uri(item), text) + file.remove("/home/pi/Conjurer/request.playlist") + f = file.open("/home/pi/Conjurer/request.playlist", create=true) + f.close() + end +end + +# Randomly select a playlist with weights +s4 = random(id="randomizer", weights=[2, 3, 5], [s1, s2, s3]) + +# Load jingles playlist +jingles = (playlist(reload_mode="watch", "/home/pi/Conjurer/jingles.playlist")) + +# Create the main stream with random playlist and jingles +s = rotate(id="randomizer", weights=[10, 1], [s4, jingles]) + +s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", track_sensitive=false, [requests_queue, s])) + +# Set up an interactive harbor for controlling the stream +interactive.harbor(port = 9999) +#harbor.input(buffer=30.0) +# Set up interactive controls for bass boost +f = interactive.float("f", description="Frequency", min=0., max=1000., unit="Hz", 200.) +g = interactive.float("g", description="Gain", min=0., max=20., unit="dB", 8.) +b = bass_boost(frequency=f, gain=g, s) +s = add([s, b]) + +# Set up interactive control for main volume +a = interactive.float("main_volume", min=0., max=20., 0.4) +s = compress.multiband.interactive(bands=7, s) + +mic_gain = interactive.float("mic_volume", min=0., max=120., 6.) + +# Apply audio processing effects +tmic = buffer(input.pulseaudio()) # Microphone +mic = amplify(mic_gain, tmic) +mic = gate(threshold=-80., range=-120., mic) +mic = compress(threshold=0., ratio=2.,mic) +mic = nrj(normalize(mic)) + +# Apply audio processing effects +s = nrj(normalize(s)) +s = amplify(a, s) +# Skip blank sections in the stream +s = blank.skip(max_blank=10., s) + +#Manual audition override +live_enabled = interactive.bool("Going Live!", true) +s = switch(track_sensitive=false, + [(live_enabled, mic), + ({true}, s)]) + + +def fading_transition(a,b) + sequence([fade.out(a.source),fade.in(b.source)]) +end + +# Configure logging settings +log_to_stdout = true +log_to_file = true +logpath = "/home/pi/Conjurer/radio_log.log" +loglevel = 3 +set("log.stdout", log_to_stdout) +set("log.level", loglevel) + +# Enable logging to file +set("log.file", log_to_file) +set("log.file.path", logpath) + +# Set up emergency fallback track +emergency = single("/home/pi/RetroPie/mp3/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3") +radio = fallback(id="switcher2", track_sensitive=false, [s, emergency]) + +# Set up an interactive control for skipping tracks +p = interactive.bool("Skip track", false) +def http_skip(~protocol, ~data, ~headers, uri)= + radio.skip() + http.response(code=200,data="Skipped") +end +harbor.http.register(port=54321,method="GET","/skip", http_skip) + + +# Function to process the request queue and skip the current track if requested +def check_skip() + if p() then + log.info("Skipping current track.") + radio.skip() + p.set(false) + end +end + +# Run the queue processing function every 60 seconds +thread.run(every=60., queue_processing) + + +# Run the check_skip function every 5 seconds +thread.run(every=15., check_skip) + + +# Enable persistent script parameters +interactive.persistent("script.params") + +# Configure output formats and destinations + + +output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio) +output.pulseaudio(radio) +#output.file.hls("/tmp/hls", [("mp3-low", %mp3(bitrate=96)), ("mp3-hi", %mp3(bitrate=160))], radio) +# Uncomment the following lines to enable additional output formats +# output.icecast(%opus, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="opus-stream", radio) +# output.icecast(%ogg, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="ogg-stream", radio) +# output.icecast(%aac, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="aac-stream", radio) +# output.icecast(%flac, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="flac-stream", radio) +# output.icecast(%vorbis, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="vorbis-stream", radio) +# output.icecast(%speex, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="speex-stream", radio) +# output.icecast(%wav, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="wav-stream", radio) +# output.icecast(%pcm, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="pcm-stream", radio) +# output.icecast(%raw, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="raw-stream", radio) +# output.icecast(%s16l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s16l-stream", radio) +# output.icecast(%s16b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s16b-stream", radio) +# output.icecast(%s24l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s24l-stream", radio) +# output.icecast(%s24b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s24b-stream", radio) +# output.icecast(%s32l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s32l-stream", radio) +# output.icecast(%s32b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s32b-stream", radio) +# output.icecast(%s64l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s64l-stream", radio) +# output.icecast(%s64b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s64b-stream", radio) +# output.icecast(%s128l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s128l-stream", radio) +# output.icecast(%s128b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s128b-stream", radio) +# output.icecast(%s256l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s256l-stream", radio) +# output.icecast(%s256b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s256b-stream", radio) +# output.icecast(%s512l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s512l-stream", radio) +# output.icecast(%s512b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s512b-stream", radio) +# output.icecast(%s1024l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s1024l-stream", radio) +# output.icecast(%s1024b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s1024b-stream", radio) +# output.icecast(%s2048l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s2048l-stream", radio) +# output.icecast(%s2048b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s2048b-stream", radio) +# output.icecast(%s4096l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s4096l-stream", radio) +# output.icecast(%s4096b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s4096b-stream", radio) +# output.icecast(%s8192l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s8192l-stream", radio) +# output.icecast(%s8192b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s8192b-stream", radio) diff --git a/test_mic2.liq b/test_mic2.liq new file mode 100644 index 0000000..b97de2a --- /dev/null +++ b/test_mic2.liq @@ -0,0 +1,4 @@ +let json.parse credentials = file.contents("/home/pi/Conjurer/icecast_credentials.json") + +output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream",input.alsa(bufferize=false)) + diff --git a/test_mic3.liq b/test_mic3.liq new file mode 100644 index 0000000..38d20b4 --- /dev/null +++ b/test_mic3.liq @@ -0,0 +1,4 @@ +let json.parse credentials = file.contents("/home/pi/Conjurer/icecast_credentials.json") + +output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream",input.pulseaudio()) + From 94132a896216f31e47bbd79d6f5abc207c635d78 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 11:49:36 +0100 Subject: [PATCH 186/283] Fixes --- conjurer_musician/conjurer_musician.py | 94 +++++++++++++++----------- music_commands.py | 2 + radio_conjurer.liq | 2 + 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index e89c7da..1e46469 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -7,8 +7,8 @@ API endpoints. import json import logging import os +import random import re -import requests import threading import time from datetime import datetime @@ -16,7 +16,16 @@ from logging import handlers from pathlib import Path from platform import uname from sys import platform -from flask import Flask, jsonify, redirect, request, send_from_directory, render_template + +import requests +from flask import ( + Flask, + jsonify, + redirect, + render_template, + request, + send_from_directory, +) from waitress import serve MAIN_BOT_ADDRESS = "http://192.168.1.191:5000" @@ -31,8 +40,8 @@ if platform in ("linux", "linux2"): NETRC_FILE = "/home/mtuszowski/.netrc" LOGSTORE = "/home/mtuszowski/conjurer/logs/" ENCODING = "utf-8" - RADIOLOG_PATH = '/home/pi/Conjurer/radio_log.log' - PERSISTENCE_PATH = '/home/pi/Conjurer/persistence.log' + RADIOLOG_PATH = "/home/pi/Conjurer/radio_log.log" + PERSISTENCE_PATH = "/home/pi/Conjurer/persistence.log" else: LOGFILE = "/home/pi/Conjurer/discord_mus_service.log" @@ -41,13 +50,14 @@ if platform in ("linux", "linux2"): ENCODING = "utf-8" MUSIC_FOLDER = "/home/pi/RetroPie/mp3/" PRIORITY_FOLDER = "/home/pi/RetroPie/mp3/Magiczne i chuj/" - RADIOLOG_PATH = '/home/pi/Conjurer/radio_log.log' - PERSISTENCE_PATH = '/home/pi/Conjurer/persistence.log' - + RADIOLOG_PATH = "/home/pi/Conjurer/radio_log.log" + PERSISTENCE_PATH = "/home/pi/Conjurer/persistence.log" +random.seed() music_file_list = [] priority_list = [] + def rescan(): """ The `rescan` function logs a message, scans for mp3 files in a specified folder, @@ -69,8 +79,8 @@ def rescan(): priority_list.append(temp_music_file) with open( - "/home/pi/Conjurer/all_playlist.playlist", "w", encoding="utf-8" - ) as w_file: + "/home/pi/Conjurer/all_playlist.playlist", "w", encoding="utf-8" + ) as w_file: try: for item in music_file_list: w_file.write(item) @@ -100,11 +110,11 @@ def thread_rescan(): def scan_tracks(): - #Set the filename and open the file + # Set the filename and open the file logger = logging.getLogger("conjurer_musician") - file = open(RADIOLOG_PATH,'r') - #Find the size of the file and move to the end + file = open(RADIOLOG_PATH, "r") + # Find the size of the file and move to the end st_results = os.stat(RADIOLOG_PATH) st_size = st_results[6] file.seek(st_size) @@ -121,14 +131,13 @@ def scan_tracks(): st_results1 = os.stat(PERSISTENCE_PATH) st_size1 = st_results1[6] time.sleep(0.1) - file1 = open(PERSISTENCE_PATH, 'r') + file1 = open(PERSISTENCE_PATH, "r") lines = file1.readlines() result = ["next", lines[2]] file1.close() returned = requests.post( - f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", - json=result, - timeout=360) + f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", json=result, timeout=360 + ) logger.info("SENT") logger.info(returned.status_code) logger.info("SEND CONFIRMED") @@ -139,33 +148,32 @@ def scan_tracks(): time.sleep(1) file.seek(where) else: - if re.match(".*Prepared.*",line): + if re.match(".*Prepared.*", line): result = None if re.match(".*jingles.*", line): logger.info("jingles") - logger.info(line) # already has newline + logger.info(line) # already has newline result = ["jingles", line] elif re.match(".*priority.*", line): logger.info("priority") - logger.info(line) # already has newline + logger.info(line) # already has newline result = ["priority", line] elif re.match(".*hit.*", line): logger.info("hit") - logger.info(line) # already has newline + logger.info(line) # already has newline result = ["hit", line] elif re.match(".*all_playlist.*", line): logger.info("all") - logger.info(line) # already has newline + logger.info(line) # already has newline result = ["all", line] elif re.match(".*request.*", line): logger.info("requests") - logger.info(line) # already has newline + logger.info(line) # already has newline result = ["requests", line] if result: returned = requests.post( - f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", - json=result, - timeout=360) + f"{MAIN_BOT_ADDRESS}{MUSIC_TRACKER}", json=result, timeout=360 + ) logger.info("SENT") logger.info(returned.status_code) logger.info("SEND CONFIRMED") @@ -330,9 +338,10 @@ def stream_music(): :return: A JSON response containing a key "music_file_list" with the value of the variable `music_file_list`. """ - #return send_from_directory("/tmp/hls", "stream.m3u8") + # return send_from_directory("/tmp/hls", "stream.m3u8") return render_template("/home/pi/Conjurer/stream.html") + @app.route("/<string:file_name>") def stream(file_name): """ @@ -348,6 +357,7 @@ def stream(file_name): video_dir = "/tmp/hls" return send_from_directory(video_dir, file_name) + @app.route("/stream_mp3", methods=["GET"]) def stream_music_mp3(): """ @@ -367,8 +377,10 @@ def clear_pr_pls(): :return: A JSON response indicating the success of the operation. """ app.logger.info("CLEARING PLAYLIST") - with open('/home/pi/Conjurer/priority_queue.playlist', 'w', encoding='utf-8') as cleared_pl: - cleared_pl.write('') + with open( + "/home/pi/Conjurer/priority_queue.playlist", "w", encoding="utf-8" + ) as cleared_pl: + cleared_pl.write("") return_data = jsonify(isError=False, message="Success", statusCode=200, data=[]) return return_data, 200 @@ -438,21 +450,20 @@ def add_request(): app.logger.info(record) app.logger.info(record["lista_slow"]) app.logger.info(record["UUID"]) - return_data = wyszukaj( - record["lista_slow"], 0, app.logger, False - ) + return_data = wyszukaj(record["lista_slow"], 0, app.logger, False) - with open( - "/home/pi/Conjurer/request.playlist", "a", encoding="utf-8" - ) as s_file: + with open("/home/pi/Conjurer/request.playlist", "a", encoding="utf-8") as s_file: for item in return_data: s_file.write(item[1] + "\n") return_data = ( - jsonify(isError=False, message="Success", statusCode=200, data={"status": "OK"}), + jsonify( + isError=False, message="Success", statusCode=200, data={"status": "OK"} + ), 200, ) return return_data + @app.route("/create_priority_playlist", methods=["POST"]) def create_priority_playlist(): """ @@ -474,13 +485,14 @@ def create_priority_playlist(): return_data = wyszukaj( record["lista_slow"], record["dlugosc_plejlisty"], app.logger, False ) - with open( - "/home/pi/Conjurer/request.playlist", "a", encoding="utf-8" - ) as s_file: + return_data = random.shuffle(return_data) + with open("/home/pi/Conjurer/request.playlist", "a", encoding="utf-8") as s_file: for item in return_data: s_file.write(item[1] + "\n") return_data = ( - jsonify(isError=False, message="Success", statusCode=200, data={"status": "OK"}), + jsonify( + isError=False, message="Success", statusCode=200, data={"status": "OK"} + ), 200, ) return return_data @@ -513,7 +525,9 @@ def add_to_priority(): for item in return_data: s_file.write(item[1] + "\n") return_data = ( - jsonify(isError=False, message="Success", statusCode=200, data={"status": "OK"}), + jsonify( + isError=False, message="Success", statusCode=200, data={"status": "OK"} + ), 200, ) return return_data diff --git a/music_commands.py b/music_commands.py index e2d0d96..9f94eb3 100644 --- a/music_commands.py +++ b/music_commands.py @@ -682,8 +682,10 @@ class MusicModule(commands.Cog): else: self.logger.info("Spierdalaj") + self.logger.info(link) await ctx.message.reply("Spierdalaj") else: + self.logger.info(content_type) self.logger.info("Spierdalaj") await ctx.message.reply("Spierdalaj") diff --git a/radio_conjurer.liq b/radio_conjurer.liq index a66ae62..01126bb 100644 --- a/radio_conjurer.liq +++ b/radio_conjurer.liq @@ -75,6 +75,8 @@ mic = amplify(mic_gain, tmic) mic = gate(threshold=-80., range=-120., mic) mic = compress(threshold=0., ratio=2.,mic) mic = nrj(normalize(mic)) +mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) +s = add([s, mic]) # Apply audio processing effects s = nrj(normalize(s)) From de3bae7a7f276093b10dcee8c657749becaa653e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 14:26:14 +0100 Subject: [PATCH 187/283] Happy Fucking Indepence Day Poland!!! --- playlista11.11.playlist | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 playlista11.11.playlist diff --git a/playlista11.11.playlist b/playlista11.11.playlist new file mode 100644 index 0000000..3aad580 --- /dev/null +++ b/playlista11.11.playlist @@ -0,0 +1,51 @@ +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - World War Tour 2010.m4a +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/08-Schmaletz-REWOLUCYJNA_NSZ(1).mp3 +/home/pi/RetroPie/mp3/Youtube/ADU feat MATEJKO - Nie strzelam do zdrajców oczami [PGL6Q9G4_JM].mp3 +/home/pi/RetroPie/mp3/Youtube/ADU - Lecimy ze Smoleńska z powrotem [f3CVfG4pV8M].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/Horytnica Czas Patriotów/Horytnica - Promo track CD 2011 Nie musimy umiera.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/01. Prolog.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/02. Honor Legionisty.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/03. Kraj Zdradzony.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/04. Mały Powstaniec.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/05. Lisowczycy.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/06. Śląski Rycerz.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/08. Sierp I Młot.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/09. Świty Zmartwychwstania.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/10. Katyńskie Łzy.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/11. Kochana Ma Polska.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/12. Słowiańska Armia Pracy.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/13. Pamięć I Duma.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/14. Epilog.mp3 +/home/pi/RetroPie/mp3/2022 Nov-Dec/Anahata - Winged Hussars - Cover.m4a +/home/pi/RetroPie/mp3/Youtube/ADU - Nie chcę w lewo, nie chcę w prawo [J3Ok-KcbQ80].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/Hallman/Hallman - 7 Bram/Niezłomność/Twardzi jak Stal/03-Forteca-1942.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Forteca - Bagnet na broń.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/02-2 Forteca.mp3 +/home/pi/RetroPie/mp3/Muzyka/2013/SABATON-Metalus Hammerus Rex (djdariush)/40-1.mp3 +/home/pi/RetroPie/mp3/Muzyka/2014/Sabaton - Heroes (Deluxe Earbook Edition) 2014/CD1/04. Inmate 4859_[plixid.com].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - 1942.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Apel poległych.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - BEZIMIEŃCOM.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - DZIŚ IDE WALCZYĆ MAMO.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Elegia o Chłopcu polskim.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - GNIEW.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - KATYŃ.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Forteca - Miałeś tylko karabin.mp3 +/home/pi/RetroPie/mp3/Muzyka/2010/Sabaton -2010- Coat Of Arms/03- Uprising.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - ORZEŁ BIAŁY.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - SŁOŃCE WRZEŚNIA.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Żołnierz Polski.mp3 +/home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - Mighty Polish Tank.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj. Celownik & Hallmann - O jau mano mielas [zapiska.pl].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Duma o Zakrzewskim [zapiska.pl].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Na lipe slowianska [zapiska.pl].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Piesn Legijonu Litewskiego [zapiska.pl].mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/08-Schmaletz-REWOLUCYJNA_NSZ(1).mp3 +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - Live, at Woodstock Festival.m4a +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Uprising - Live, at Woodstock Festival.m4a +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Inmate 4859.m4a +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Winged Hussars.m4a +/home/pi/RetroPie/mp3/Youtube/ADU Ada Karczmarczyk - Hej husarzu! ⧸ NIE_PODLE_głości dzień [SReTynvd6ek].mp3 +/home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - A Very Polish Christmas.mp3 From 28c15654050ca1554a47029b33bfe62816eab7f7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 14:39:15 +0100 Subject: [PATCH 188/283] Fade in/out for live --- radio_conjurer.liq | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/radio_conjurer.liq b/radio_conjurer.liq index 01126bb..e07eb91 100644 --- a/radio_conjurer.liq +++ b/radio_conjurer.liq @@ -86,14 +86,12 @@ s = blank.skip(max_blank=10., s) #Manual audition override live_enabled = interactive.bool("Going Live!", true) -s = switch(track_sensitive=false, + + +s = crossfade(fade_out=10., fade_in=5., duration=15., switch(track_sensitive=false, [(live_enabled, mic), ({true}, s)]) - - -def fading_transition(a,b) - sequence([fade.out(a.source),fade.in(b.source)]) -end +) # Configure logging settings log_to_stdout = true From a830dfb18c09c901be1569c48e0aa1978562c53b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 15:21:25 +0100 Subject: [PATCH 189/283] pls 11.11 --- patriotyczna_krotka.playlist | 14 ++++++++++++++ playlista11.11.playlist | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 patriotyczna_krotka.playlist diff --git a/patriotyczna_krotka.playlist b/patriotyczna_krotka.playlist new file mode 100644 index 0000000..ae3cf30 --- /dev/null +++ b/patriotyczna_krotka.playlist @@ -0,0 +1,14 @@ + +/home/pi/RetroPie/mp3/Muzyka/2011/Hallman/Hallman - 7 Bram/Niezłomność/Twardzi jak Stal/03-Forteca-1942.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/09. Świty Zmartwychwstania.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/Horytnica Czas Patriotów/Horytnica - Promo track CD 2011 Nie musimy umiera.mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/08-Schmaletz-REWOLUCYJNA_NSZ(1).mp3 +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - Live, at Woodstock Festival.m4a +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Uprising - Live, at Woodstock Festival.m4a +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Inmate 4859.m4a +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Winged Hussars.m4a +/home/pi/RetroPie/mp3/Youtube/ADU Ada Karczmarczyk - Hej husarzu! ⧸ NIE_PODLE_głości dzień [SReTynvd6ek].mp3 +/home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - A Very Polish Christmas.mp3 +/home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - Mighty Polish Tank.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 + diff --git a/playlista11.11.playlist b/playlista11.11.playlist index 3aad580..0d2ed0b 100644 --- a/playlista11.11.playlist +++ b/playlista11.11.playlist @@ -38,7 +38,6 @@ /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Żołnierz Polski.mp3 /home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - Mighty Polish Tank.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj. Celownik & Hallmann - O jau mano mielas [zapiska.pl].mp3 -/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Duma o Zakrzewskim [zapiska.pl].mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Na lipe slowianska [zapiska.pl].mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Piesn Legijonu Litewskiego [zapiska.pl].mp3 From 93330638be907809a0a837f6dbcf8673e8e46142 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 15:38:32 +0100 Subject: [PATCH 190/283] mood fixes --- patriotyczna_krotka.playlist | 5 +++++ playlista11.11.playlist | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/patriotyczna_krotka.playlist b/patriotyczna_krotka.playlist index ae3cf30..df7b187 100644 --- a/patriotyczna_krotka.playlist +++ b/patriotyczna_krotka.playlist @@ -3,6 +3,7 @@ /home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/09. Świty Zmartwychwstania.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/Horytnica Czas Patriotów/Horytnica - Promo track CD 2011 Nie musimy umiera.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/08-Schmaletz-REWOLUCYJNA_NSZ(1).mp3 +/home/pi/RetroPie/mp3/2022 Nov-Dec/Anahata - Winged Hussars - Cover.m4a /home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - Live, at Woodstock Festival.m4a /home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Uprising - Live, at Woodstock Festival.m4a /home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Inmate 4859.m4a @@ -11,4 +12,8 @@ /home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - A Very Polish Christmas.mp3 /home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - Mighty Polish Tank.mp3 /home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 +/home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - World War Tour 2010.m4a +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/01. Prolog.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/02. Honor Legionisty.mp3 +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 diff --git a/playlista11.11.playlist b/playlista11.11.playlist index 0d2ed0b..8a179d0 100644 --- a/playlista11.11.playlist +++ b/playlista11.11.playlist @@ -28,14 +28,10 @@ /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Apel poległych.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - BEZIMIEŃCOM.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - DZIŚ IDE WALCZYĆ MAMO.mp3 -/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Elegia o Chłopcu polskim.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - GNIEW.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - KATYŃ.mp3 -/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Forteca - Miałeś tylko karabin.mp3 /home/pi/RetroPie/mp3/Muzyka/2010/Sabaton -2010- Coat Of Arms/03- Uprising.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - ORZEŁ BIAŁY.mp3 -/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - SŁOŃCE WRZEŚNIA.mp3 -/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/FORTECA - Żołnierz Polski.mp3 /home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - Mighty Polish Tank.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj. Celownik & Hallmann - O jau mano mielas [zapiska.pl].mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/Dj.Celownik & Hallmann - Duma o Zakrzewskim [zapiska.pl].mp3 From 1625149c64588d54c0ec50a7bef13736bbde6ebd Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 15:53:26 +0100 Subject: [PATCH 191/283] =?UTF-8?q?Fajne=20laski=20na=20prawicy=20s=C4=85?= =?UTF-8?q?=20:D=20:P?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patriotyczna_krotka.playlist | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/patriotyczna_krotka.playlist b/patriotyczna_krotka.playlist index df7b187..8917121 100644 --- a/patriotyczna_krotka.playlist +++ b/patriotyczna_krotka.playlist @@ -3,6 +3,8 @@ /home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/09. Świty Zmartwychwstania.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/Horytnica Czas Patriotów/Horytnica - Promo track CD 2011 Nie musimy umiera.mp3 /home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/08-Schmaletz-REWOLUCYJNA_NSZ(1).mp3 +/home/pi/RetroPie/mp3/Muzyka/2011/muza patriotyczna/schmaletz_-_nie_ma_fajnych_lasek_na_prawicy.mp3 +/home/pi/RetroPie/mp3/Youtube/ADU - Lecimy ze Smoleńska z powrotem [f3CVfG4pV8M].mp3 /home/pi/RetroPie/mp3/2022 Nov-Dec/Anahata - Winged Hussars - Cover.m4a /home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - Live, at Woodstock Festival.m4a /home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - Uprising - Live, at Woodstock Festival.m4a @@ -11,9 +13,8 @@ /home/pi/RetroPie/mp3/Youtube/ADU Ada Karczmarczyk - Hej husarzu! ⧸ NIE_PODLE_głości dzień [SReTynvd6ek].mp3 /home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - A Very Polish Christmas.mp3 /home/pi/RetroPie/mp3/2023 Aug-Oct/Sabadu - Mighty Polish Tank.mp3 -/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 +/home/pi/RetroPie/mp3/Youtube/ADU - Nie chcę w lewo, nie chcę w prawo [J3Ok-KcbQ80].mp3 /home/pi/RetroPie/mp3/2022 Nov-Dec/Sabaton - 40#1 - World War Tour 2010.m4a /home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/01. Prolog.mp3 /home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/02. Honor Legionisty.mp3 -/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 - +/home/pi/RetroPie/mp3/Muzyka/2012/Głos Patriotów (2011) (Wirrrus)/07. Już Nie Musimy Umierać.mp3 \ No newline at end of file From 443d06280b6a9ddb0854148a873894c3eb14c635 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 17:45:10 +0100 Subject: [PATCH 192/283] Duplication error --- conjurer_musician/radio_conjurer.liq | 19 +-- radio_conjurer.liq | 178 --------------------------- 2 files changed, 11 insertions(+), 186 deletions(-) delete mode 100644 radio_conjurer.liq diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index 998401b..e07eb91 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -56,7 +56,7 @@ s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", trac # Set up an interactive harbor for controlling the stream interactive.harbor(port = 9999) -harbor.input(buffer=30.0) +#harbor.input(buffer=30.0) # Set up interactive controls for bass boost f = interactive.float("f", description="Frequency", min=0., max=1000., unit="Hz", 200.) g = interactive.float("g", description="Gain", min=0., max=20., unit="dB", 8.) @@ -68,29 +68,30 @@ a = interactive.float("main_volume", min=0., max=20., 0.4) s = compress.multiband.interactive(bands=7, s) mic_gain = interactive.float("mic_volume", min=0., max=120., 6.) + # Apply audio processing effects tmic = buffer(input.pulseaudio()) # Microphone mic = amplify(mic_gain, tmic) mic = gate(threshold=-80., range=-120., mic) mic = compress(threshold=0., ratio=2.,mic) +mic = nrj(normalize(mic)) mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) +s = add([s, mic]) # Apply audio processing effects s = nrj(normalize(s)) s = amplify(a, s) # Skip blank sections in the stream -s = blank.skip(max_blank=10., s)192 +s = blank.skip(max_blank=10., s) #Manual audition override live_enabled = interactive.bool("Going Live!", true) -s = switch(track_sensitive=false, - [({!live_enabled}, live), - ({true}, radio)]) -def fading_transition(a,b) - sequence([fade.out(a.source),fade.in(b.source)]) -end +s = crossfade(fade_out=10., fade_in=5., duration=15., switch(track_sensitive=false, + [(live_enabled, mic), + ({true}, s)]) +) # Configure logging settings log_to_stdout = true @@ -107,6 +108,7 @@ set("log.file.path", logpath) # Set up emergency fallback track emergency = single("/home/pi/RetroPie/mp3/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3") radio = fallback(id="switcher2", track_sensitive=false, [s, emergency]) + # Set up an interactive control for skipping tracks p = interactive.bool("Skip track", false) def http_skip(~protocol, ~data, ~headers, uri)= @@ -138,6 +140,7 @@ interactive.persistent("script.params") # Configure output formats and destinations + output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio) output.pulseaudio(radio) #output.file.hls("/tmp/hls", [("mp3-low", %mp3(bitrate=96)), ("mp3-hi", %mp3(bitrate=160))], radio) diff --git a/radio_conjurer.liq b/radio_conjurer.liq deleted file mode 100644 index e07eb91..0000000 --- a/radio_conjurer.liq +++ /dev/null @@ -1,178 +0,0 @@ -# FILEPATH: /home/mtuszowski/conjurer/conjurer_musician/radio_conjurer.liq - -# This script sets up a Liquidsoap radio stream with various features and configurations. - -# Load icecast credentials from a JSON file -let json.parse credentials = file.contents("/home/pi/Conjurer/icecast_credentials.json") - -# Enable replaygain metadata processing -enable_replaygain_metadata() - -# Set up a playlog for tracking played tracks - -l = playlog(duration = 72000.0, persistency="/home/pi/Conjurer/persistence.log") - -# Function to check if a track can be played based on its metadata -def check(r) - m = request.metadata(r) - if l.last(m) < 36000. then - log.info("Rejecting #{m['filename']} (played #{l.last(m)}s ago).") - false - else - l.add(m) - true - end -end - -# Define playlists to be used in the stream -s1 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/all_playlist.playlist")) -s2 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/priority_queue.playlist")) -s3 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/hit.playlist")) - -# Create a request queue for user-generated requests -requests_queue = request.queue() - -# Function to process the request queue and add new requests -def queue_processing() - text=file.lines("/home/pi/Conjurer/request.playlist") - if text != [] then - list.iter(fun(item) -> requests_queue.push.uri(item), text) - file.remove("/home/pi/Conjurer/request.playlist") - f = file.open("/home/pi/Conjurer/request.playlist", create=true) - f.close() - end -end - -# Randomly select a playlist with weights -s4 = random(id="randomizer", weights=[2, 3, 5], [s1, s2, s3]) - -# Load jingles playlist -jingles = (playlist(reload_mode="watch", "/home/pi/Conjurer/jingles.playlist")) - -# Create the main stream with random playlist and jingles -s = rotate(id="randomizer", weights=[10, 1], [s4, jingles]) - -s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", track_sensitive=false, [requests_queue, s])) - -# Set up an interactive harbor for controlling the stream -interactive.harbor(port = 9999) -#harbor.input(buffer=30.0) -# Set up interactive controls for bass boost -f = interactive.float("f", description="Frequency", min=0., max=1000., unit="Hz", 200.) -g = interactive.float("g", description="Gain", min=0., max=20., unit="dB", 8.) -b = bass_boost(frequency=f, gain=g, s) -s = add([s, b]) - -# Set up interactive control for main volume -a = interactive.float("main_volume", min=0., max=20., 0.4) -s = compress.multiband.interactive(bands=7, s) - -mic_gain = interactive.float("mic_volume", min=0., max=120., 6.) - -# Apply audio processing effects -tmic = buffer(input.pulseaudio()) # Microphone -mic = amplify(mic_gain, tmic) -mic = gate(threshold=-80., range=-120., mic) -mic = compress(threshold=0., ratio=2.,mic) -mic = nrj(normalize(mic)) -mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) -s = add([s, mic]) - -# Apply audio processing effects -s = nrj(normalize(s)) -s = amplify(a, s) -# Skip blank sections in the stream -s = blank.skip(max_blank=10., s) - -#Manual audition override -live_enabled = interactive.bool("Going Live!", true) - - -s = crossfade(fade_out=10., fade_in=5., duration=15., switch(track_sensitive=false, - [(live_enabled, mic), - ({true}, s)]) -) - -# Configure logging settings -log_to_stdout = true -log_to_file = true -logpath = "/home/pi/Conjurer/radio_log.log" -loglevel = 3 -set("log.stdout", log_to_stdout) -set("log.level", loglevel) - -# Enable logging to file -set("log.file", log_to_file) -set("log.file.path", logpath) - -# Set up emergency fallback track -emergency = single("/home/pi/RetroPie/mp3/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3") -radio = fallback(id="switcher2", track_sensitive=false, [s, emergency]) - -# Set up an interactive control for skipping tracks -p = interactive.bool("Skip track", false) -def http_skip(~protocol, ~data, ~headers, uri)= - radio.skip() - http.response(code=200,data="Skipped") -end -harbor.http.register(port=54321,method="GET","/skip", http_skip) - - -# Function to process the request queue and skip the current track if requested -def check_skip() - if p() then - log.info("Skipping current track.") - radio.skip() - p.set(false) - end -end - -# Run the queue processing function every 60 seconds -thread.run(every=60., queue_processing) - - -# Run the check_skip function every 5 seconds -thread.run(every=15., check_skip) - - -# Enable persistent script parameters -interactive.persistent("script.params") - -# Configure output formats and destinations - - -output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio) -output.pulseaudio(radio) -#output.file.hls("/tmp/hls", [("mp3-low", %mp3(bitrate=96)), ("mp3-hi", %mp3(bitrate=160))], radio) -# Uncomment the following lines to enable additional output formats -# output.icecast(%opus, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="opus-stream", radio) -# output.icecast(%ogg, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="ogg-stream", radio) -# output.icecast(%aac, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="aac-stream", radio) -# output.icecast(%flac, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="flac-stream", radio) -# output.icecast(%vorbis, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="vorbis-stream", radio) -# output.icecast(%speex, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="speex-stream", radio) -# output.icecast(%wav, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="wav-stream", radio) -# output.icecast(%pcm, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="pcm-stream", radio) -# output.icecast(%raw, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="raw-stream", radio) -# output.icecast(%s16l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s16l-stream", radio) -# output.icecast(%s16b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s16b-stream", radio) -# output.icecast(%s24l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s24l-stream", radio) -# output.icecast(%s24b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s24b-stream", radio) -# output.icecast(%s32l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s32l-stream", radio) -# output.icecast(%s32b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s32b-stream", radio) -# output.icecast(%s64l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s64l-stream", radio) -# output.icecast(%s64b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s64b-stream", radio) -# output.icecast(%s128l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s128l-stream", radio) -# output.icecast(%s128b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s128b-stream", radio) -# output.icecast(%s256l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s256l-stream", radio) -# output.icecast(%s256b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s256b-stream", radio) -# output.icecast(%s512l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s512l-stream", radio) -# output.icecast(%s512b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s512b-stream", radio) -# output.icecast(%s1024l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s1024l-stream", radio) -# output.icecast(%s1024b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s1024b-stream", radio) -# output.icecast(%s2048l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s2048l-stream", radio) -# output.icecast(%s2048b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s2048b-stream", radio) -# output.icecast(%s4096l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s4096l-stream", radio) -# output.icecast(%s4096b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s4096b-stream", radio) -# output.icecast(%s8192l, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s8192l-stream", radio) -# output.icecast(%s8192b, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="s8192b-stream", radio) From 24247aa7602979922e85eefe259117680d8056b6 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 20:27:11 +0100 Subject: [PATCH 193/283] Fix silence --- conjurer_musician/radio_conjurer.liq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index e07eb91..bbe3f3b 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -75,7 +75,7 @@ mic = amplify(mic_gain, tmic) mic = gate(threshold=-80., range=-120., mic) mic = compress(threshold=0., ratio=2.,mic) mic = nrj(normalize(mic)) -mic = blank.strip(max_blank=10., min_noise=.1, threshold=-20., mic) +mic = blank.strip(max_blank=15., min_noise=.1, threshold=-30., mic) s = add([s, mic]) # Apply audio processing effects From d3bd79b5050b41901f8ee8caf43b32086718b501 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 20:32:08 +0100 Subject: [PATCH 194/283] silence fx 2 --- conjurer_musician/radio_conjurer.liq | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index bbe3f3b..591e91e 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -88,10 +88,10 @@ s = blank.skip(max_blank=10., s) live_enabled = interactive.bool("Going Live!", true) -s = crossfade(fade_out=10., fade_in=5., duration=15., switch(track_sensitive=false, +switch(track_sensitive=false, [(live_enabled, mic), ({true}, s)]) -) + # Configure logging settings log_to_stdout = true From bf808eb6341ebfe6d29c1c8e631872171d4e720c Mon Sep 17 00:00:00 2001 From: migatu <mtuszowski@gmail.com> Date: Mon, 11 Nov 2024 20:12:47 +0000 Subject: [PATCH 195/283] Fixed live --- conjurer_musician/radio_conjurer.liq | 7 +++---- test_mic3.liq | 9 ++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index 591e91e..c26f5e1 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -76,7 +76,7 @@ mic = gate(threshold=-80., range=-120., mic) mic = compress(threshold=0., ratio=2.,mic) mic = nrj(normalize(mic)) mic = blank.strip(max_blank=15., min_noise=.1, threshold=-30., mic) -s = add([s, mic]) +mic = fallback(id="switcher2", track_sensitive=false, [mic, blank()]) # Apply audio processing effects s = nrj(normalize(s)) @@ -87,12 +87,11 @@ s = blank.skip(max_blank=10., s) #Manual audition override live_enabled = interactive.bool("Going Live!", true) - -switch(track_sensitive=false, +s = add([mic,s]) +s=switch(track_sensitive=false, [(live_enabled, mic), ({true}, s)]) - # Configure logging settings log_to_stdout = true log_to_file = true diff --git a/test_mic3.liq b/test_mic3.liq index 38d20b4..215af26 100644 --- a/test_mic3.liq +++ b/test_mic3.liq @@ -1,4 +1,11 @@ let json.parse credentials = file.contents("/home/pi/Conjurer/icecast_credentials.json") -output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream",input.pulseaudio()) +mic = buffer(input.pulseaudio()) +emergency = blank() +mic = blank.strip(max_blank=10., min_noise=0.2, threshold=-30., mic) + + +radio = fallback(id="switcher2", track_sensitive=false, [mic, emergency]) + +output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream",radio) From 86f512cb341aaaeaaac42403a31da9db2d86eafc Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:26:10 +0100 Subject: [PATCH 196/283] first test of secret channel --- ai_commands.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 5e4b2ba..51fa4bc 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -46,6 +46,7 @@ class Events(commands.Cog): channel = None if message.author == self.bot.user: return + if isinstance(message.author, discord.Member): for role in message.author.roles: if role.name == "Vykidailo": @@ -67,8 +68,17 @@ class Events(commands.Cog): return if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: - await message.reply("Witam witam") + #hammer pisze dma + member = self.bot.get_user(703985955312238664) + channel = await member.create_dm() + await channel.send(message.content) + await message.reply(f"Poszlo do Saint {message.content}") return + elif message.author.id == 703985955312238664: + #saint pisze DMA + await message.reply("Hejka Saint!") + return + else: channel = self.bot.get_channel(1064888712565100614) await channel.send("Słyszałem ja żem że: " + message.content) From 3ccc89f572d391ad03317c80d9de778e85ca0762 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:31:41 +0100 Subject: [PATCH 197/283] tst --- ai_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_commands.py b/ai_commands.py index 51fa4bc..a070f9c 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -70,6 +70,7 @@ class Events(commands.Cog): if message.author.id == 346956223645614080: #hammer pisze dma member = self.bot.get_user(703985955312238664) + print(member) channel = await member.create_dm() await channel.send(message.content) await message.reply(f"Poszlo do Saint {message.content}") From 61acd3f3f31950525c4b9917e02f52bc39683c25 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:33:16 +0100 Subject: [PATCH 198/283] txt --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index a070f9c..24ae994 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -69,7 +69,7 @@ class Events(commands.Cog): if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: #hammer pisze dma - member = self.bot.get_user(703985955312238664) + member = await self.bot.get_user(703985955312238664) print(member) channel = await member.create_dm() await channel.send(message.content) From 0674e1a1a85e46b98dfe02af5eade4994e298f91 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:37:06 +0100 Subject: [PATCH 199/283] tst --- ai_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 24ae994..771b4d8 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -69,8 +69,9 @@ class Events(commands.Cog): if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: #hammer pisze dma + self.logger.info (self.bot) member = await self.bot.get_user(703985955312238664) - print(member) + self.logger.info(member) channel = await member.create_dm() await channel.send(message.content) await message.reply(f"Poszlo do Saint {message.content}") From 6b5d31223179ccf2902efb67d359e4e5f8663de7 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:42:01 +0100 Subject: [PATCH 200/283] Test --- ai_commands.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 771b4d8..211bf77 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -70,11 +70,15 @@ class Events(commands.Cog): if message.author.id == 346956223645614080: #hammer pisze dma self.logger.info (self.bot) - member = await self.bot.get_user(703985955312238664) - self.logger.info(member) - channel = await member.create_dm() - await channel.send(message.content) - await message.reply(f"Poszlo do Saint {message.content}") + user = await self.bot.fetch_user(703985955312238664) + if user: + self.logger.info(user) + member = self.bot.get_user(703985955312238664) + if member: + self.logger.info(member) + #channel = await member.create_dm() + #await channel.send(message.content) + #await message.reply(f"Poszlo do Saint {message.content}") return elif message.author.id == 703985955312238664: #saint pisze DMA From 0ad06cc69c86ee8b0280fe3c8e071d56128c5e08 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:43:36 +0100 Subject: [PATCH 201/283] FX --- ai_commands.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 211bf77..c5f1a28 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -73,12 +73,9 @@ class Events(commands.Cog): user = await self.bot.fetch_user(703985955312238664) if user: self.logger.info(user) - member = self.bot.get_user(703985955312238664) - if member: - self.logger.info(member) - #channel = await member.create_dm() - #await channel.send(message.content) - #await message.reply(f"Poszlo do Saint {message.content}") + channel = await user.create_dm() + await channel.send(message.content) + await message.reply(f"Poszlo do Saint {message.content}") return elif message.author.id == 703985955312238664: #saint pisze DMA From 1f0e03590d21f6c570ca2b8a73595f8bf1037aea Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Tue, 12 Nov 2024 22:48:21 +0100 Subject: [PATCH 202/283] FAAFO --- ai_commands.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index c5f1a28..90c5185 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -69,19 +69,16 @@ class Events(commands.Cog): if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: #hammer pisze dma - self.logger.info (self.bot) user = await self.bot.fetch_user(703985955312238664) - if user: - self.logger.info(user) channel = await user.create_dm() await channel.send(message.content) await message.reply(f"Poszlo do Saint {message.content}") return elif message.author.id == 703985955312238664: #saint pisze DMA + self.logger.info(message.content) await message.reply("Hejka Saint!") return - else: channel = self.bot.get_channel(1064888712565100614) await channel.send("Słyszałem ja żem że: " + message.content) From b3e5a1aaf30fa2e26fbd389fdb2f8cbe0efc3d9f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 13 Nov 2024 21:57:14 +0100 Subject: [PATCH 203/283] Start --- ai_commands.py | 3 ++- ai_functions.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 90c5185..dadb40c 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -9,7 +9,7 @@ import openai import requests from discord.ext import commands -from ai_functions import handle_response +from ai_functions import handle_response, start_assistants from constants import ( DATA, GRAPHICS_PATH, @@ -30,6 +30,7 @@ class Events(commands.Cog): print("Ready!") print("Logged in as ---->", self.bot.user) print("ID:", self.bot.user.id) + await start_assistants(self.bot) @commands.Cog.listener() async def on_message(self, message): diff --git a/ai_functions.py b/ai_functions.py index 0e738d7..86222d3 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -17,7 +17,7 @@ from constants import ( OPENAICLIENT, WORD_REACTIONS, ) - +ASSISTANTS = {} def num_tokens_from_string(message, model): """ @@ -292,3 +292,34 @@ async def get_random_cyclic_message(client): ) logger.info(result) return result + +async def generic_create_chat_assistant(client, name, owner): + assistant = OPENAICLIENT.beta.assistasnts.create( + name = name, + instruction = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", + model = "gpt-4o", + tools= [{"type":"file_search"}] + ) + thread = OPENAICLIENT.beta.threads.create() + + return assistant, thread +async def hammer_assistant_create(client): + #this will be personalized but for now I will use it as a templae for hedgehod and saint assistants + id = 346956223645614080 + name = "Conjurer" + owner = "Polish Hammer" + assistant, thread = await generic_create_chat_assistant(client, name, owner) + ASSISTANTS[name] = (owner, assistant, id, thread) + + +async def saint_assistant_chat(client): + pass + +async def saint_assistant_bul(client): + pass +async def start_assistants(client): + print("Preparring assistants") + print("Hammer") + await hammer_assistant_create(client) + print("Saint") + print("Hedgehog") From 651d7962543c2a6ae40bc2b2016cd79003ce1051 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 13 Nov 2024 22:10:43 +0100 Subject: [PATCH 204/283] Logfix --- ai_commands.py | 13 ++++++++----- ai_functions.py | 6 ------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index dadb40c..d4e7ee4 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -9,7 +9,7 @@ import openai import requests from discord.ext import commands -from ai_functions import handle_response, start_assistants +from ai_functions import handle_response, hammer_assistant_create from constants import ( DATA, GRAPHICS_PATH, @@ -27,10 +27,13 @@ class Events(commands.Cog): @commands.Cog.listener() async def on_ready(self): - print("Ready!") - print("Logged in as ---->", self.bot.user) - print("ID:", self.bot.user.id) - await start_assistants(self.bot) + self.logger.info("Ready!") + self.logger.info("Logged in as ---->", self.bot.user) + self.logger.info("ID:", self.bot.user.id) + self.logger.info("Starting personal assistants") + await hammer_assistant_create(self.bot) + self.logger.info("Started personal assistants") + @commands.Cog.listener() async def on_message(self, message): diff --git a/ai_functions.py b/ai_functions.py index 86222d3..d3b67c4 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -317,9 +317,3 @@ async def saint_assistant_chat(client): async def saint_assistant_bul(client): pass -async def start_assistants(client): - print("Preparring assistants") - print("Hammer") - await hammer_assistant_create(client) - print("Saint") - print("Hedgehog") From e05f58624a4c7dd1a2df1233dfcf7e5d98bb76bc Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 13 Nov 2024 22:48:40 +0100 Subject: [PATCH 205/283] fx --- ai_commands.py | 6 +----- thin_client.py | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index d4e7ee4..086e4a6 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -25,11 +25,7 @@ class Events(commands.Cog): self.bot = bot self.logger = logging.getLogger("discord") - @commands.Cog.listener() - async def on_ready(self): - self.logger.info("Ready!") - self.logger.info("Logged in as ---->", self.bot.user) - self.logger.info("ID:", self.bot.user.id) + async def cog_load(self): self.logger.info("Starting personal assistants") await hammer_assistant_create(self.bot) self.logger.info("Started personal assistants") diff --git a/thin_client.py b/thin_client.py index aed2484..541eed3 100644 --- a/thin_client.py +++ b/thin_client.py @@ -67,6 +67,8 @@ async def on_ready(): for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) + logger.info("Logged in as ---->", client.user) + logger.info("ID:", client.user.id) # TODO: ADMINISTRATION From 6d3ce5d7145241e2cff7474dc52ccc54fbb72f7d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 13 Nov 2024 22:51:27 +0100 Subject: [PATCH 206/283] bgfx --- ai_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index d3b67c4..4a44c5a 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -294,7 +294,7 @@ async def get_random_cyclic_message(client): return result async def generic_create_chat_assistant(client, name, owner): - assistant = OPENAICLIENT.beta.assistasnts.create( + assistant = OPENAICLIENT.beta.assistants.create( name = name, instruction = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", model = "gpt-4o", From 64669887a359386e23d657ef3c3a00ef703aaf54 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 13 Nov 2024 22:53:19 +0100 Subject: [PATCH 207/283] fx --- ai_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index 4a44c5a..8e13959 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -296,7 +296,7 @@ async def get_random_cyclic_message(client): async def generic_create_chat_assistant(client, name, owner): assistant = OPENAICLIENT.beta.assistants.create( name = name, - instruction = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", + instructions = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", model = "gpt-4o", tools= [{"type":"file_search"}] ) From f1129f18cb82a26c2fc55878f14bb92570c36cd1 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 00:04:59 +0100 Subject: [PATCH 208/283] Rich presence --- thin_client.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/thin_client.py b/thin_client.py index 541eed3..aa7997a 100644 --- a/thin_client.py +++ b/thin_client.py @@ -62,7 +62,24 @@ async def on_ready(): logger.info(client.cogs) - await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) + radio_is_alive = True + if radio_is_alive: + radio_hardkor = discord.Activity( + name = "Radio Hammer/Radio Conjurer/Wolne Bałuity Kurwa", + url = "http://95.175.16.246:666/mp3-stream", + type = discord.ActivityType.streaming, + platform = "Liquidsoap+RaspberyPi+RolandSeratoDJ202", + state = "Where the f*** is the DJ booth?", + details= "This Is Radio Hardkor based in Wolne Księstwo Bałuty, the best pirate radiostation on both sides of Łódka river", + buttons= [{"label":"WOLNE BAŁUTY KURWAAA!!!", "url":"http://95.175.16.246:666/mp3-stream"}] + #assets= {"large_image": "", "large_text":"Fuckewry", "small_image":"", "small_text":"Hi!"} + ) + await client.change_presence(activity=radio_hardkor) + + else: + + await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) + await client.tree.sync() for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) From 1b26fdd841d4e457cdaa748b65d27dceb159594c Mon Sep 17 00:00:00 2001 From: migatu <mtuszowski@gmail.com> Date: Wed, 13 Nov 2024 23:40:45 +0100 Subject: [PATCH 209/283] Assets --- fuckery.jpg | Bin 0 -> 31490 bytes willowisp.png | Bin 0 -> 39844 bytes wod_beacon.jpg | Bin 0 -> 22684 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 fuckery.jpg create mode 100644 willowisp.png create mode 100644 wod_beacon.jpg diff --git a/fuckery.jpg b/fuckery.jpg new file mode 100644 index 0000000000000000000000000000000000000000..01b6c7c7b662c990eb7470244f774be9b89465d7 GIT binary patch literal 31490 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 literal 0 HcmV?d00001 diff --git a/willowisp.png b/willowisp.png new file mode 100644 index 0000000000000000000000000000000000000000..75ed5e34e7546a6b13bb5566939ecce7778b0226 GIT binary patch literal 39844 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 bo!$N)e+Q9?Xg87V00000NkvXXu0mjfj*Oxv literal 0 HcmV?d00001 diff --git a/wod_beacon.jpg b/wod_beacon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dcf3ef887b81fc3e53d358ab86a8d9820baa6c17 GIT binary patch literal 22684 zcmb4pbyQqU^XA|#2`+;K*WfU?B)DsE2oT&YxI=<F1b26LcZb2<-Q8L8zTda|JG*~v zPn~n`>Aut5x4Q0APgVEZ{M$MJT~bs+6aWDM0T6%x0p6AXA^><;SU6Z1csMvX1O#|Q zWK0xfBqU_Kj~Hl}Kzw2%AU+`>2{|Jb$tQX;LPBaz8hR#XHa0e5Di9wR3oj!p8_VBL zAP^7`kdcsaQBZJMNC`<<{=dsx2jBxdgcQUz6vQV0<Oc|-4-juX073u&0vZYe@IML! zBrF^hGz|QEs}?!{0tyNe8WIKu5e^v+4h8@LfP8QJ080w@ky!{s0sfP|Z4@R8tD+AU z2dABbV`gPlH5nVbuzlwwHV(PScVGYLn5?cTTqTw4nvHV`E^))y?w&8oss@|yz2m=Y z>7Vj{28a0DJM4QK<%f3-LcSNM_v^p&gMj=1^^ufWNC6sy<rAi`zOB#OA^`cl?Y;g3 zKmd@P6ZJ~>;_S!=b{Bhbc2^FO*iY_jXjD~PM4^uUlScNcKx)#-wFD?@%iAC-m~z59 z<D}9IqlfW%MIN(nHHHm^oLmn0QPa5GtV_-L>)HLzU13w;sChFfppXoB`NY%XHRs$* zMEBa<MpK|Bf0nSYO7?zFeL;M)av>vZ_J1DvTTN3SBhsd8{4?V5^gjEaf3hPKI1eA6 zeRu<CuVMaEpO?vw`rwiJ&ma;1*3Tr0?l)3I^H!w#a1Ev5)ahDy{*OU;;$}0!IK`>8 zbBJ^f;HjX*kx;ywmUzp4Vs@jk$1>etZ-BD1VBWuPuRyLSLK?0Lim>V}!zE?c#bQ+d z&qxXX&PKw)Goa-tDR{tQH<ml9#37wJvf9`fw}bzy_LsASMbP`Kcs3)j{>~PQZAOb` zx`}(pYEDcZ|Hr`mfUhxV!C59m*uS8jK7VkPJpG?n>K}LRHO$V9Z)-~mM%Bg7`yo7L z;4`ub7_X*K_gaP0^qST1dQ9*dNb(=1yDWbGXPW=KeAKan9Vnuol~BWW7xg0@x$OU? zm1(;FM|Fj$@e_|h>VvEnv7f%JlSr{e!-{sfBB$lgv*bpeB`o=Ew!bgkf9B6My-YU! zDBG#v(#XAPsz;B8KC;I%Yh%*E034A263sA7bLsW3?%aO-<Y`^Q3g8QMi00tf`ZI=t zcSZYmX)QQ1dyEsG6@L?YrJV&=wzt66y#Y#U-T*g@Z-6M(8HcF}wdqN@;VmWUd&3<N z;V3zw8w=*N`MV8ps{uUhFTMfk#@;XD5;R4JtoB3C|1NvXzl(u5i1B|Jixis|s`cV- zQc>2#Av2K8m-9JG5gg502O;<2hq`X8+P}v5*EVR?O1SJjC$6V!ahw$r4-J{Cw=WY2 zeNrzWe^WaRxu=nPioF}f;-05i$yqSic%0~_)ti5YKFlCqSHDlR!B?D=^Uu2b)`e6A zeMZgY9-I+7{z<|@?)cc40A}Xd@t(UpL3%B<X`hn(dv(hAm`g-2;duYJ0OsbJuRM=F zJL=rAcRSP^=q`77@L_MW3f)D6*(c>%L^IlsCqH0S*FvwQ-Z(wjx_r8E4XeW71_J7u zM63c=scUu3QcpMKs(I7*fb}Z%LK7u4!GIW(o4y_QXgdw##iQ1qYSy`z)c&eEqSx{# zT*lF3JSUU!HQmN%>$-NfboDh%`sH70-8{o5)~?(y*K6$*#wW$Z;l#e?^;?}!yR=)A zb7y%k4|D>S-=1xrF|^x>(AJnmKAU|GYGEwggu!xD<}}$4pf3%rJ;wu8v8H7@pT0)S zJ>r=dWHgHCUjFV;Z0=;POKlkEY5gvK2Sayl{VLq{sz!Xw@CFb^S{WM6E2UhpDael2 zn9@3QK_XnAjfNnrv#qL|H55JN566;UBBp3o;K*t$9oA1u?lue>PUPCUF5(y84pi%A z1rk;^R36)On)`IZxV7Qnh!Ff9WPmQmHjigd5t<G&fyg>g@D|9S-BRp_`IbcBSHEKV zWjN;WQ=KM}mtT@9PMgB+EF{Okq4=$l+UJD;x9G)3B*(JoVA^1(emSI1=3m0**)t&K z=F60$q(=AWc`??_@_%M{g=P}wyhJQr7bU3eH;?^lstP>A^y;c<Li{D+U=J}D8;L3^ zDuj0*@Y6>0#j#b_F=F`W&@979iWogf_yW&9cfiUO8TOcCp2}5JvGS32PHl+yn-zne zv(237b3};dxq6(1OYOHNDF0gg+it!oq-Evrv<XU0R7wdcfwc?cOvgLgpM_jGRqZt$ zQBXaDfBt~FSloC6K+Z~FshkPUEHs2jWsllZ#^-$_vlRbT13^k~xmPFWIiy?2g`%Bb zv(k0$2+a-|jwAgYGdH)x5pS1E@kGt?qLtF2+Rtu*2D<0T0E75mxZ1H+V5o*N!w>Is z-8976`AWo8byHIW`}vdqOw#SJl0)zb7P8HBlFeL)j+ThVru_(ig%+Pz9@S)1!s%C; z!MJft9txH^M#0tZPJMFvk7}7eF+^z^RIC$PmBG?pi`brdFv{k*2Gc}Q!qHm%Da$4Y z%iuPR+3G~kAj(nVoCXxPY3zL~PRaWdCh3X$O=i3w417p|f>vTR=}aX^)`+!?(Y2$n zNi*`YM5p#r+G0cfJV24fO%2bkQXc<u-vt$VW3+Ib#{7Z6Xe@&cq}IN(EV`UVHDy%^ zVDE`x+((=Sw!ZGr)Qf1jMOC{Pnxg4C?OwPtisp6|RWbK$%mnK51p^?OjnFClG8Q*j z3$A2Z!)ndtN2V|Atnk7Y(b$H1zUtAY!$<LnZeYLMGJXeT63s@`{BmtTK=$UHyGms@ zac6w$^Y7q6=A_?Nr-uRA>#3X7+G+>8TDXgnxLeU~A%z3hWy^uJjW-cG_w$7~**2e! z3OYeO^2l$1lpDuG6m|Wn$zF=nJs9-FC|pme^za4~=oCZi6Xo)37ub_R_<aNah?}%B zutYww04&R^8rl(U?8X{$8KGc&)(;hyz!mBBL0ZBwXVCpPon}-_s9{W<n2O`Y)w#jv z9h=vxibwuFyzbvx%n8eswjFN(3ctt5ufPt1-1sNc29486txvvnc$DGaT5y=dY{>ma zM4B8HTp^TFwA!>8S_I$mlKqBlVo(veBv^QM@Q;BNA?z6LK<5?hmVyp?@hgwQ3*8&Q zq(qA0+ZzC-;5F_IP@^CvcR94^sui_0G;qmd&xLlRxfaN;%J^%&_DXFjt4Ud<*Q{Qe z6=^$kkUYEEkA+WZSNu+2s+f-;g(k(sQGji0x6W0X=?MXq-0fWD0{R13sUMmiIhZn# zxpaTl#~ZTnCz*TWs+C6nX@KKbDY8<7EQ=`|_=nIxDC0th_4;+gJf&v5Z-By@nK|*5 z?b=nE9u*X89xZUZICyfDuV2^qh8W&qR5W3N?fOPhgnY&hze|4*z*oA1M`G>GlKC2S z%`})-GP9H2A70Y|413V&62GsYp@KS%o6{i_q3}?SLkr9kCvylQXKv`WKOWMo`oa@7 zt8IEIRl9K4WS{Hfjr7yrufL?TVY~C@Zk+R$7DXp5%GumsWwuyaCA!KtD|Jc+N|u1k zuMer^!YANsaj0|*5wXqbRu59Z7l#|<?5y0Khp$o#qrS%*rB(|(L|eo#yiJe1TKmXY z9OfgArxqoQzCYbDhNGf^BSQUJ7CAcM3O?CoQ<e!@Ql1~GpOo&;+?&*Qr%LPQrl)?^ zD)Q|jAb~>Md5wW$B~G*|QMR8yn29z%_Fea}kGqvA)s~tJ+OqHfP<PxMT|~r%V8n8u z@Z`UJvRo|FYFURI|FCuK9a;98cJ0=(Y?58rs{OsTah*#^3@wa=NS3#Q?WwBX$}^n3 zQi)$mH9I&<|4P3wk2%OIZH~|#E4!z7e4xSJy&$99LPaLO$2wS^XgLxU9s~zn!b?(K z)@ai<u40>}en?1R9l{!tvin<yAljJ+z~76)`^TYPb)k%%<0ycq@Q5ZwonGz`{GxXQ z*Fd{k@5(!;#Sj~pt=Q42fJA}J=6`v`MrfS8*vKClI|;rZ0^v>o6)c0*BDo_U@E((J zvajb>OS3Pjz4RC~#ILZZ;H_rZ@nhR6D<;0Rs3zqL+RNXvHOGaGb>pt_7&@vFXPb`t z-g`Y1yaDv(xcrK1USi~yOXmnmd9iAhSvm0Sq|!EbxljVdp-*C}d|WwR&ejBIBW^M0 z2M=(N<FLc{pc!`KCFhvKt|%H1sS19gDde-Acx>h?3GNrHJLLLC`d-40!X+1S5t}u$ zJ4MGRX<yICg<z1>ss%TGd)~PxfT&Ldx=|bFFkEeOvqVqyf0v3A-y^c^4<fz``G{y9 zqyd@z3#+!~d^tOl&9+Kd7a=A&F)BP8h}rkhp)F*Y6>jc0M38d&C+Z$0|NKED9BGh| z2la`eT=WgFGyDeFp7aFA`ScUyIu-|g9JVe>iZ`Xr3LQim1`bJ(LvjKk6<tMi_m7kr zo+l5T@K?1ixnn-yxELpM-EJK#-#B7@EvqV9vx(>`&WW3pUfzb|9Jm8^>U2)tD9xE9 zqi9QqM`yV?@>aF>Q;a!SlBc`SjV^!5GIGYZa=JvLQ0a=sfxY294kez=R;;hFhW+eX zw1zTPRmLL&-N8>}(jt*#x__$WJ>h1!*M=G1JULq!d+OM(R3+hkV^Em0V;)gEYD>1R za@ohh!5(U2uEB&43qUMhPnFFq%$cTqJ_`9&HloY0H$YCL2WobhxA?$u^|Cr_3%+v= zQCUm`&x*`64r~1dgGKl0>@PNL4`no?4y@fm7Ush+%A=nh{4HE?nIpLkBUx_?_j35! znAlz>!yr9fOjFi5?WIJ#`8Uyeis1reez!1!y9n)`V=*F1Bp6>nU+>kb9(lOFxTC1P z0npKuC4@nq%L263Gu+%)S*Ny>E1SQnY-IS9@t8;86xuerEqqIo!gRGW?q^&JXV^JO zf0X!ig_WX4Qz2K!M744dc5F5Bn<w{Lefq|p8@M83Vrfv6(ZFJiS2zrah+04O$SqRo zTgk>}4RUU^`w<^3Zpv`p-W^?KqdoF^Vjwm~M#kfvKYsnw=GVPzog4;%y*y`vz|oHM zPx|?OMEae?UHkmce8VlqdjgM_@-JdVj2dL&EZU!EIAKkPlB^`1`bX|lcsY&<z=^zE zX-|8ZGB|W*4KqpjSD8nd!TSSwc?A>|s-0<K&E+(3PR+pPTPmcWjnLGpQdufnR@oyI z%3-6@+OlFSTj@3I#0tQao&70o#m;GULQr<FR(?DNapiY+!PG~F4h$)0Mb$F`u@2$Y z?OT7U9|FqX(!$d{INtz+>Q83<TS4ndC->)`55W!B4o?T|o|5BBsse-p&~w)Pes?3< zL>8@CZDDVK0kyTEtr!0_3AxrI;vGfLF6V5oORT%ghWC${b4uyVwf{jmy`uDX<hp77 z^)t>U+H{L<*&6`fHQ_2%><sbNpSXAPS?Dn#<YsLHP4U&f;AKPFBVmg7*e>N-?KE&5 zrAZP|Dy<+*eLIO`h?<l)RF90e+~(%@RUeaEoZ#TKk=sz}WLpx|*9hba?DdmSi!zB| zvMg&?GxNA38PjGr)9=-iv(o!pDB)twiG+mPk>7-<%6JOea|G5eBd$_(o1-}Wn!exY zO*i$KmqJfZPAgY;x~Ecf?~vEr;o7V%rXHZ(^JYKq;hb)(o9?REx^k=jbnQC%BFpRg zfsuEZ2=3V|Rh1Z;j$f)*)4`>qFwjKqG2jhQTI}Nb-OUwoFVD>EKHFeg+5m+Ch<QG= zxe>YNzKr??01dmH&agl36taydh%Y9=lNpwIf^@e;H$l#0E0;O6*%QwCn*Dq(#QZMF z1cI+Y??Jdmw`n`tv5JPdu!hA=sezekmQ5`4a(80m7?*(A)k%}+tnCz%x2N=DxZvcg zh>oC6PLgUTbPVrCR88aRI!lXY_Gvri+^tlq(t`mG^-f&R@x<O^|8?wSeU;VPz|30A z;1h)kQ8@9>dVw+7mua)52I0pLOJ!znUGmQc_Pat5s>=xAJ4>Eyy23L|R)ukf&Px_h zAfe_!XNrp<u`hgoP9!WJdjjm4O5>eq$r$b{a?RPcv<gm+#}M9`DxCola!rK{TNvE# zeL!a88$iC;IZ(ilf#(4O>mHLB_^uZDBu>6D3%1Bg@6;rC#-$TliT#!5!h-fpznj*t zKX>kx%bwHF<`v6D&vs<Kz918q9-ODsl>e8a(WC&L@zaO%-T(oHTWjlmy2GR|M&^(F zk_%k+tPptN8+78pR#oC71uaIl*N@j!J18$#Si42Cw5H~xXF4WLOP0{JnAbOCJ0F=` zD}2a+2`<P`=c<cn@G5Tr;J-9Wb;s{4Y4X8>xDGp2_hK>PK=<h@15hx#Jg<mh7ky+9 z{%XfYV7mUie|Tjm^4jB7Vb&|I_i;x}h5ZYAY^5@IU_tMXulOoBayKecY`$m=`5?J1 zwb5+LJ|ig!bc9Rd;73Gl8bi@$FiY^HN?`UV>anfyZYuS6ii|<oZ-9013%B*X8qyU1 z4wS{LrW%jTmpH~kdZ38L)N;Cu*FL{ga|c>y;d7Jqy^35c&XmAebpHc+dR?KTgR7Xw z<@_6fJmz+Wu{K!OTEZ3dvaKxuYQ9q_bIy^wNF{rDr*CBxt@~Tl+@fcwVysq#J<gqB zeviRRTUH4zW*~>|QnrsX_gHckZMcg}r_wksXv$}k-)rg;Ld-2@G=hGs%m!1|OwX+3 zho!6#`~fBq>)}x?x8}5zXA<^3eyJD`v|i!fXiukac;Ql|P<OII+&>{UPdX{kIm$lx z?f{nH+6nN<ruq=xwug~UAuyQ~7^Hll!BblocU==5#KNUBTT{W@+tBUQUtBf2$<S1H z*1fa>(?lhu#?JhqZ?$~+BV3KQnt;Dk(i0W(i83dxDNWI$k^M%b{wQi(kD=4N-)%vW z^AzjZ>DohSdux;{j*Q3QqQIx1Tga=q9OJc__A5;<w@=rMNl!C1o?p?9VH9?V=!YH} z3cc)?B`ikt2KS*Y0d#7U?GK1g4_aOoMV_>tn6+p-WViUG5$`U3o9=AK_O+{i^0I-B zaT@!viB_^HzL(D-koE_<m%j}jiHBcP0Uw#gt~plPWoIjcNa1$>Or#;kDSiLLaBc77 z8=x`414ToY0Ba{;?>W`4T~s;Y_pEjEGFO{IZOc?$$~f-XDBd2x?ZNi8q$uY-=GSU} zmBBdy$#{?mz@{Heya9SL`NQ%#n=jt^M&?UCO??jBUy~uek1OP(bj3J#$!lj<d_5@g znI(SeYQO4jzxh&rf^g3Z)=et(a+ulEA)Ddw3&WAT^VO8nmf#7htn8CJR4BMn%sqLw zZ_zoVHK;m&ty-T)E731uB>5;J<^5}mi|>gpp2i>zTLxqCKF7qK2pqZY42>mkBOtMQ zy<(N)vAD)gZr4Vg$+C_I(k3N$iBNC81}%I*+6*smH<P53#_=G~{2ol3bt}W0yD2gt zMm<c>?vTQ@*?xxwU%f(bSxogyn_$C+Wh#L%UF~6XkP0X&rxUDx0HX=D1-`8~H$$FM zZuCB#g}wCk*j{%FK08kv&OdEotDcrKuA5e7j|es%l<epEQ*N^ym9h?P{szEoqp6u~ z1wW6lIlCDyp$po;Eux;-Ji#lgVQ^WDe$;DR%!`&{|B{6xLf4oseHFdT(~Qd8wq-e{ zTrYm0ITI3|P?X3JqxfytF#G~{Vx9;*^bM9{05osKsoe$|C3NR>mm}**tAgKzD|F`M zSTm4}?OSlCZ6me?J)%mOm?hHDS4$e&li6IDuDZtidg$3UJ6!><_K`fmGUiJdD>4ie zvuo5nQGA)p`GX@oHBWuY1b(V+Sl}Rs-6F76VNrfz8eTdBE79|PK*vW>xuV^dL=Eg0 z3g;TvFM~Gq8o9X@QXf9Y!@>%xU)ro{*Nq25^y?fT*P03Rtl}x{KMHwPw#hZ>(7m&| zsNOZIT`u#)t#(FnT+kusWjRPKn`@NK)~4vZKWdcIk5KO&e)?1mh#@_D_p}&Y?p@yi z*vq-G6P){zVYljlF(sQyI<PG#yd1zF3&V7J1*@Eq8}UF}%{n)?z)=d?3X5<aKzQPd zW374-|4_oVA`@M;WEF_$+jT&?w@X-tiaSVFpt77U`37*2)+yxWS9N|-UJ|k8ILR=j zqba>sRzWU`s^XfqH$-QygO@1h`r|ZrtsGmANK5-eao_;o^x;whca6M218dsA+#@ZD zJ1*25CJh|uEzsvRQZ(E;WIlS@b|Bx!VzwAG%@?&7m)jE!>t<Cm5lsMXsISpv5P|aq zlZ9ky)Rwdd<bk#sIzYCA_3<VYB%KGIqot`R2Usl7PWnaQ7ZKc84s=S8vm=|6Wk*fn zW}uu-rl+{29VL`DRJzQmikCWGV`857%P)H0gC2fEbQ8qXmnb73ItK1*6Tlqi%bFn) zj1rK!Uq}B8v-q>))Pru@#-s*93<-LTa!r2ie^1x0dW~e_o9nJP^i?#NX_&d179I6u zne6Otzkb-*dtV*M;7l12M+>z_K{LrGM-_rn)rKVnq|ML9zvHhUS@^7)i;@#&Q24(| zdaJ~U&W8Wls>`jI_$s0)C*$ag9Vq&cqD~E!!H5U9D(X>OXnP&fHm%si;}IPkZBQRV zUY4A&2usl3&c7o=DqHESsYVcBw78{U)1cgzSCzSZIds(=af8}{;;qV<z7?MVo<$_! z<11SrXS0P*eBhNehFIR1R|&?HX?CqwRy7BHWj}0!CrMLf4u~|4g$+f0k60#J1RG88 zlMYI!W8+{eL(O}|IE#lQaCgX#sPhefXPZN*+q&MATA)_XX`Y-5AE?u~6Fx7P-bUka z3SVGfWfbMHF$CuYXZZ1vFQC{038y&CE9-6C!V`aGvRRs~HarzEDk{1A#>X#VD%Pph zhO=<X3ncCl@21`3-IlgLem%5)BAR@+vNr%r0eX`5#9fm*F_|~2TgWdUCrCyZ>sQ6K zH6A;5Wc^o*^TINY`?_Gz<y;?su?~-0r=nT8_)|~psU>AvR-nP;<=6qfz$ip}sp7!9 zs<e?-F82x<r<PA|tP6`{zVBJ!PfnIX>A^)Rq2Ton&;F`-8OW@kkV6YSXx!V|9j>hF zK^<xrZj#C6-qT)oJn4!q+mMoyJ^U7GiTONFl5!TUtPbA+y}q{?Z6e7g+R|de6I;u% zhWd>PpsYBWCIJ{j72W8jf?HaVI(zn<p8H0CQWu>x4Yjx@-j7Gl1?rOpz_kq(2qy*3 zp}l;%;e3fOeFL<-0sg40ITimYSn<q{RGLewSE-!5HW+TK8&`;#xYfF*%&Z;8$uYWl z-#S3l1VYE9G+gwAo3rk!zX2Y#lBs|<+F8pqKLVpSU!8fURhq<juL!q{9e@B8UyKs% z=?E=z$rqQOfjc&NdHM0Q`m(d&D?RT$9xC#)BP@=LYwu~NCr1WMr(~r+au}b?Q#TC* zy&?GPE0qUizAWVySUcj!$j|ARWd+k5ZtuB$8mE&5Eb-hb@*(vs)%oO<<ke3qD6<p^ zk%fuz3qcX+1t6TpX%ome&S8{2-bM$fHHC-ADoPp|6#k0_g$mcdr^EFPj%R})nS?%C zzg`xgBLKXe_*5Nn92q9KclR<+v8tD-_jer)@aH(P(~$wu%zh`@qeeYPxAhe-gXNiZ z;La_M@9$r7)$&OrSyTDG*!csIRCWq)sH1w@icR!5-N*61RJk8j6B?Oy8pK#wY!1h6 zBPnC%`1lB&M^yp3WwjbH@@hI(AA|^R%1qrz#>rg15v+Oom^Sh9%nmqfgD)yxYdH?W zKT_gL{eDaqR}d3|$Zyazc;upe@XNBEG3d9}hXcmO=SS2(=rQDiZy#MEBq$3V!iaah zm!DPC!p5GSFuaw_A9pO4tyJ!cbMB-{m5Pe<`+3800jOan1h5e#O--8L)~*e&_=gpc z3isaE&5?oe_t?P*laT4HU%UH;O@AdQd%k)Bd^5~e3rB3Nl~M*~bH9IBqqo7kvn!y? zopEk8vI#D`;nj*X?#RtIwNNlFT#lBji5mbab*MMao^SMNHznCC>#_wn#5r*%E>iVJ zNXf7MG$P9+(epcjrrCCPX@i9w%qvKHJ+(AQ0^efCJgVr2#y(rdQ95yY5g?bXq{<ba zHO&^`eP$INGIO@?{3<;~X5M7~(b>Lo`Fag^XbWP2i=Eo6Pu+THvcH!psi)&3dNnL* zWdaX`L3O^=H}3_cdO72$^B{es^cOCVCa=Myx|jpqGnz!Xm;>!In&QLn&%0hT@WNPn zr0|0OLlhz4g)@ZF(*)x_DMRuNNMb^m!V^vJp{W_pV3K8Nk)IZrq05)R%KQKMLW}MX z`H#T=efGl&JT$5GLMc1va4la`RAaoc0bdeN%&FoEee(JEcd#0t7|&LHht7PTF6lz) zllvThS-cs3qiMdrVP!+@D8Kn+-I51UdTpmgriq1Zg!;<r=_&tIQ5x-%x{ODDS*V^$ z@6px~+tv-^w>>M_1zU4q2K8wi()%XDnjhV7VEp-@(+b~_<g$Rj?NZh%6(a#kGo!kj z>(4hpyj+T%>q9=eQ&)eR>sCY8;7(dGV?}G-g23{hlpTSpxB@4PplFjJg&89-#(4^) ziJaM}Q_ni{B(EC=ggV^UvF1%1xHxKY4uo#U;3GuH!-aj6FP&r7#c^YStn@zFjY`ez z@WI&IdgWBxm)HGrYN!~GrA@9R^MX)%0h1;j-J%1!#BH)KN2669WG_4H+;X|jEqUdO za&o<UFY^ARrr!RffPcl?KN}Dl8bJ>4-|BjMCFK)(%`XX?39Mc~cz-~iLOc!-_X*Oc zcfsl-8Lm`VtcgV^VQ#-+gHY&=c$$Xn0SK`wvq1KIJV_V%p}4`Mhu-(2nDPe1xC$qV zDonGONrM)dd#r=KY@hpPUrfL+r9F%7;_5&_1yk5d05SO)Bi|S{>$p;P9-&8h9RB?Z zL5R*Adv}3NCjw69*`w-cVK!wRY`02;Y8JC!y2)uV(i;Hv=y-i#{@K-Ys=wh$%;?W3 zzh--cC%OY;a%qFf{7Fj5#Qa`TnnW@lsh?x}0`3dwg=;_wABd%*DQ#rP!xp-IsvN)4 zJwU;@XGKWJ1b;XaGRjYo9+5Dr5+y=DjvN+lAEAe}m%$cs$7$;5sO<G5S(r`o1_&|M za(kW=9_@a_N9t%0Kn>>bBeWtOyDempJFhaR8dG1+&LjM`<#95$W4|hK6bG{fL(CAr z#$<Jz7*P?R&xA(MZCrA??qH(8Ps=<8o*>FlhIm9AX=iLSXmQADl#3EHh!9j$>NXu9 z^V~-PBDs~lqzTM+3laF(hYNNsc301la&W@>%{z#o%5roZSGzYEkS7@folj#^JCKMx z3}WhA4f*1=N$p=ksN#pQS31>MxMR%-F}nKDl-H}ZDGXsiIrM`0kktk`gR#I_!HpF? z)D<q!_kIgI3#b)pC%cLuH>E|pV`n;S(?z4E({BeYrro~=5S>xU^bkKAgnos$Jydh7 z_Voza^i*lwef+jnV(6F!6;tlOfUV-aY!$G5BwM>z$RD?QnUqR3)x4^tc2eodDRAgd zidcd8-|rR@Jqj`x4p<e1kkLp@24@pR13Jcp_b2@ZU5M_Z82jtwSe}al1XQw1O{sCi zR5*J<n5W8+0V;KN;(ld>r|yCP-U;q2%X^woA12I)`;B7m<{oV)f<yhU+7o)QS(toA zE69)ef|obk-vBff%8EDctO*|B#1FIq`LoCQ?o3S_AxcfLDIA3<rE6CD4X?GKdqFxp zI~KS)D+9#eN1@J7)js%g6CII{+KiOoS+Qtt#|xm!D)~a3HVj<5gRopFN7Xfk@|1q1 zr!<xl6~Fdu<mvgU_9BaP+1?}vP=@)G_HE(e(fy!)NI~Vkq4Qw=zNa-%TA|c6D@LF( zxs8o2_WB`B5Wq#?sC@^n@vf}<<171pb#NoBmT-g<n!zlvKB@zm>kg@$M?>>O>p+yy zK6Jx;SzW8HL9sd+id@fN^y|H!*kvozgO7_NuTPKD+IQtHSL=@)mjEeQQCA~dcK1ky z{l&SF*OF~OXtd;>kz<^2!(~la-7$7e#Le%^<9KzE6!9nQ>NKhFhugAC-ap&Jk^JV> zQSH_9Rei3dqA}amqT&a!U{=a=f(WO<e*1R8O6I57mc3-Yh_4AQ>TnGyC4L}jR02IJ zU;8*Z`HdzUL9zL2ZBj+w7>9LCGeBwzIRr1KBgX*+rK*{8;1E(1B`jte9vzOV=^UQX zqS{wo_)x$)D)xR^#8E`Be_(4ndk<Mp3UQiR$mgE;Gi&D;jXR9}!*A+o`WM2!!9$VE zV*$gj1h;c9i5|)<{{UIO#C617?9APTr_w+6RfX>V3kzNt;*)Y}B)piRvtWVn7sUS} z9Hf7?XFdcwt&id!DJcz0u&3gk^bjakyDAio+<1l#hZ{7;l*BQbcEIhmU1=Z9DaP%{ zF#>M?axrNRx5YobLAe5!qBGIAm{O4t)5KE#yd6&!RNGxZYVY5?9lNV+*GzU@varz@ zpY|uCG`?s-My=AJvUaRvp`V~EfRWM!dHw+lYoqlr{w6;=4})!7dZNF&0a&Nx>Z;lS z3g5Ad9=g%m4F|A$V^w23XjTl(c|N%8V{)|r3d=5L26v<TOKl+hupM4m1_+dzE$~&A zmf)stGJy;k`VfHJj9-KAfSZ5Dk<YjFJN0?CDxcJ3t}3%(x@mv@7*!1zYYU%*C@PK& z#0acGP9P@Xi6hLOsXHp|P-LOWjO(Ad5bSQ97p>1NEHA3(NaK{A96zkNBeQH9@hD=* zt@%0>e}^2?^=S6<5V?ae6l$^*Iwrz(+(VmN+THT|AYNFTNK?e2lMr8cVNBm#O~pEw z^Kp)f>?_tNK@C<{>m`yy=KVGUNvZKJd7F+|@lQ+XFq3(%@M7I$J~bsp7Wgxk_k$DG zUYb15Y7|cW;KB48IkZl7JZjvbjsS<TrQ_6`EfV@;Pw~z{t=Qdx-D`3hTPr)$B`f>X z)?r4@1v|YmH+a;C;!5H&D4zkoIr<hpUKi<BOk}FS6skHFH~NbXajmK`v2s)bjg0^! zL-LdjE0z@0VWTLB1>51|gX67d-MSDO1oZ{6_OA`IHBRc9p%09Bc5~&#{ax&uBJE!z z6_6b?^;fq89*`2;Q}>8iH*zl(M*Zg$7%I-R=~8EO9=^Q%)D2>u-YDzBp*(UzSL5b2 z29UbC{y5js!Bc4Tj9R@+O#eK^v#Nvx?tK_}0~9HfOBTPIDK#_+sl5ItWgp0#(>Qqa z>?-A7SO}2H|8g5h<zN4yN93aKpom$}1%dD}<4b|4{5N-!1`)BsAK@~oZ0@Dx>yBzy zx??!;Y?%g*K)M`jCe)?xHfnhcGlUb>{37#>M(zg(Td&$b?BXvQpwf!q95zO!=ad*Q zPcTm-TU>X_O!$PZU}W*vWo_#41Q>e)#_637SStH!Obd>qxxX2@ZO0Jqtcq0J(yJ&< z^Jj@~P^E^Gc7#<mtEhyVpqRUc=ZZ0+w?%`BXm)$T3FmF+A4-fj&3Mko=3Y4IRRW9D zNmC$rWtR;s+;GhDN<)pu@D!TqwxVNsx$w5}?1QA^YSoyLKR_M5Z&2(X*9Nq@SC59C zISf4b48oD$C|D}!0@;yM#sCH&XyOUwU|EyRUB=FSN9haHd$95$rSzAeJv;PcG_vhl z_Z5(}k_k+gSM1y<C+Mf!Z&Zez7bJB$Z#Wbi8}a6Wi=`&?DB1#ae-h02=}8Jq<70)~ zwaO6`P-ufa+qG6>dper##=;YCGrb-Xk6>39xLzeH*ZsKe>De_jqa=*$8^j71kA;za z<*MPxoJAV2X%2NimdBB8d9aH3M0{_Zp0Jw+Y5K5r0qVtqB3fifwZUoM?L{fBFe091 zybI0FC4P&w{$XBND#WXM_u24sF;&nm%lPQ`f7tQw%YPV}L+{@BV@{Vcsp@}>;7h?@ zBOsUbhmQkJc#F>8=13;NCp3K|Q6_z7z3|Y^lCZ8B5WC5Wq&%@0p*i+0vMk9clK!+G zU}y(n*k)LMt^dAkxi*KDjNdm9&r>ZU&^e_?i!065e?O9Tn9EUyS`qma*3s^jjASDq zx#wbgZ)Izw;7O!T(YTVzsy*-tC?$*7c5<DU#xw#67vHI0WxB}2$D^MWP+8&F0Z8}6 z@#MBy`K5K&+;lC$oMaG|yQ10=@g1N}0B@=evg44+Ewe}&z0sNyJgqU?moaCO#dS6K zE_o^eAcHf?<s+N-P;*o2z0mCE;R89`&MHK2ZZ6-!7}zHcYLilo0f&~8K<R-Aj;ED_ zazkCN4aa$H3G`)Zjvr<@|CpAZo>$DF+YugH6qwZq7yYI(|7{<xHD%|qeuucc=?KGH zBNKO{Q0H_!3wmxh%lp+GzP{C<x%FhT+m?y$&p5dUFCGW!DCtjvu^D$hK-49K@Jq2g zCvDBvQVAqG4ir9s3#FD;Y{G+@w-Jt(U&d1$F-8LlUtV6g_s=}*4zE27FBK6c{4r<m zc~b5E<kla3xlnGMAFlfNnX{Hof`a`baVfeoxZuHN4%LW!Ew^#R`VkA`7w%reI;Uf) zb(5I<o;{I5YW0d<Bl`?ynK`iWw&3cA16Q9A5b^rre5CT~<f@(Qigjxz={lJ!6o)fk zWvFQxg^jEm<Q5LuE8*@F+glYg8lI|#Sd=k6%`K9GQNibHYqO9L`<y<S5b><b{(H<m zMQbbCnF0a5H6qd>UGmIDhGxoxu62j`)830>CcH}IU-y_g;UgbK3qdN32qXN3M42oL z{gd*4;sS^jMn#fn*QeMC#Te=#bVg^P{U~_o)Tz=UBoCRP(%9?Cgw}on)ZeZ2$rt#e zruKe<<Z0b@rK4~;n$Xgv_J$vMEiR>;p0&I*jt_un66X09F;kUSH;g|~K}rpUixc>> z-f%|!^kq6}p-e|_fLVN3y}or&&l@0CO%xu`C+r{}K~YZtzw{V;J#YAP#e&UENyJ%Q za6pk%#4aV0f==4EvbD|NM2><fZC<fhvUujFZ9fTK*K}M@4>&CR$TKsud)BH)uS_|n zio_s>0fh^O0&J^u@hNdyUSb%-kT)Oqu%HW`{DUtCsVl}e@jLmU{qtwGXgkfvg!Eiz z-f}1AXgQTKx|8Ab^&V1Sf^o|K4p^yr={A45p~6cGn;J(j=JM@|a{bIsbO|8OX{a(0 zNjwSmE2&hLfp|p~<6Ky<*<SDGtbX4H;N+*<8<9SwbT4H<T|)f4nOK}-t|bZ#i|1US zO&e39<jb(CO}d;5as-XNfFnY3CLaXiKo*rGZp8Gss0`$eTBe1R7Kz)oz~86Mv1e}U z=EKZUlb~_((Ef(~oME+Y7hCD=+O8zuym$^7a&qPsG?l^?nY|Jt89*+RHv&vg8H{5a ze1`oz8mAs&i~D<WNg>>;H}RqG6a;}FLL@54C46nUIT3R}d`8nK`)|C?RiAz%D(_De zT=9;a=uvQ4FhgHRA#X%Kl0xR(lG46M?f=L-NFgXf!+k0ISoDFGDFEP4f&!kZo}5(H zqWvQ$gXbQ>1)<Z#`c-LxdVze@yht|)v~Zf$!ac<TX{ADMrNvC-+@X5IOBx1n(qd^X zo($ys@v8p)2LPhsj-g-^3J>8^2$C6xL%-EQD~G!!P*u(jpYt@ruA2(An1sl*6k(xj z8tFEdvA@w^cg@h4fi_dB?zY6J!%=+Co3&{!+GZ@@vlCm0*B>f8O)5<3Dx@-ITlFZ{ zA(iz+>nWxMSL;=8&Y-x7B8?!p{a0>DX57Ruoy4m-@I5J{=%@tra(1*F5&Z6;`1Pvd zOr?81bJ$%;p*xvjCyPQnV6uk{LEh(M=0AA+FDjEF<T_KA+jOHFRz~?OIW}0wvY>Pl z-4-<A5Ojt$yH-B8NgJ!FW}Tm5-qRDdS)JBU7AF}So$)w8=nEe(MRhYuSKY~e<jV%6 zka9XO{Iutm%kgT-qZ<5&?=qYJ%XS6h{^n1<zGqSd!@6YmyL{yRQzID_{C~akUUW*u z9BRP=@s!&@-r90BsO%u&WRz2X5-T4?3SkJRn%1<ei=P3JeaIFzEPQM1?T?pQvO_Z) zt!1`Smw$d6Alw+3bdrrc&>K4fxpogn>lCZfQlr?e^bGQFtk7X!AomojV<Eixd>pdI z7Ld|Di|BjDq@}5Ak{S8<0j#PyM}K6KjD0u}$7l*I2!g7lBj&KdRy~#M+uh2;_6^^+ zui*R5dbl-TQR5fA)gU;i8|~`N9anS=S_wZQk1!91>i=B%eFtzeG;M~zj(dEir!QQa zdc9Og?qF?dFLWc(=Q%^#I)~4VXK0FN?pDryX!%viqs%_H!+=o4#?}AM5re*Iv{9ut zT1|!D1r?Mc_gVi`V_&>!;Bu)d=hVM-M$n&p2#_m`4*&lM^}z}}yx@P+C4aSE81kN@ zR{pKvAD{{$tW1I~+u`Dd?mzV&&t~#u(td@AEs$7&%*5;4|Ldg@CTZVYGl_#Rv{)m8 z{CoGtS@}t2Rqtn_jqHOM-RSn@z|Zl4_|A=GQ5iUwoCI%xqoy1S_p@fv^*c_22EVYl zwS1k;KETv0Ix3)l(S45oEWwjzgDKwxK<K1-aTbsZ`@}HN*{^)>5SBTiXOYITRW}$$ zRIZhO<$#1KVB0hRzw=G#F5fR~T2%C8h3psZx8>vTeJlGmL<{x@C{S0|gs9t&1(&e} z{kJvXq=DoQ=7`T$oQK#TLNHN(T(6~a_a@W2^?Vt<z5R64M|cLFQfnDGCRK%`A$8o1 z(UB>f<XZMTx38CZY2vyBpg~iz?|eZ>)1d|~wsp#^zpyiH!7~6sLpOY_5#&gf8>c^8 zi>7;qB))Ul4d|jhM1wdcm(YbDv!KZ(y@mPyp*E!OGXI4TVUu@TMnv}xfE>IoyNjJZ z_$P}H1OBUja><w~rgz1Pzq=~NfBcgog~SX%KJF6)ExNbFBrS%Z@E0dR3h~ekEm9Im zAN06S3KP}nj5QA?wv0=B5LdJpV55|_?B%PKq0pMf(net|qO))MB?QssD2!+FLA-c% zj41n^+YNgsYatKPTVvQulqfuL#qg*Q7QaA&0|x{OV+&|wL_6suFCii7gpur3EYf!A z-wdh-WwbYq1#*q;UCr~unnt+B9d7rUW-Uf{ps`YeNE{_cLpzX-tNJdX`lD?Q!UqW_ z<SM`LAYm-kDQPmn#r4~y?ZHI|*fA#RNQRr?1jD~-5;ThQ*TLFh2CUdL&T(7EDM=7< z_6;p;wD8qh-XZdMe^i>O-A3u)dC7G65f&Z<Fua!Ulp(O&W$Pz-wdVk7hEB5z1<HK5 zu>n2tEFEiIY{&Gv+HEv?QS&!$w8RXuPt9KHHmA!zTc46Dhnt#*XfX+O!d2by(Bi4i z@jaL66S%3R+ywCPF&32vCYUTy$#$d_*8_ioD&`Up5Zp4IT)I9|2pSmv2kHJd>LFCT z`^HT86@*&)zeGb?F5!UqzL&{Sk<n|07Ck^60X+e4AN`ZoJ5G`d!-)GAK>xP;Cygte zD-10e_1O?o`W>47dJ}QK@6j$ayQsf?#(9wU+8Z^Rgj>&j7nZvxVEZ^!2qa9Q2|qN@ zTIL?<hku~Z7SR%-sDyOpTi$Hk1O7ZMODM(MxCnv)|9&7S2n$|6OoWcm4<$|l&q(Me z0t7Uo7CF}73&KYD;7PykZ!=FzNNx?SyExOz4h8x}dj#_pn1rgJG=fhB`f5CDyORlC z#EN}KZ`$dvYweVj?Zm+w#t{9Liqyv8;@wG3R|-iQzs&WvQ3>y_q-4Gw;)#$`b@L!w z2ZVyyH|vMHm*#46BvLP<B^CjLhm9AiETVAi3q<;?{d)yu$r(du8$0_;_StS@nj@z@ zCH6lncV%txXSeIiR4Z*!;S$5Zhg$9!LsG+a9j8SDjzD|53=F1ijDSBQT!RNIwhS`S zX&daUOG6`Y)xfJ6XQW2AV>e1pTl=J@Fc5Wyl@{)D!(P@zaaAAwUf(9U=c~bbGsPrV zxMffhxWP`T>!A7yg4QUU!-aX6#+~k{RJ&^hc}k&oQx2V^YtsfT4U0-$iO<Ml28)Vy z;Bqg;z{{@rR7G*vHoBsFm@pBy&xy#4tk9taht^)(+CWpG(Z*WL4XxH@V<&O79$dfu z%CEk~k~Y(%TyBu<V%}+<WFvH}_}Ss|K_?L1{}VJR;_<)e3!Lz$?D!{3!v<4pU9YTl zxFjb%W=xb{E%xWz0S#4Ip%EYN4+$L`0OIBpw;7VMd&33!8vu`z-@f_9N9qlbFU_=; zeBj%gMG`na+lD>x`dw=;Y*4sDKmQf&hh;3y%$n`a1Ao-(KM6^JRV9^v@aQ|G5#=k8 zI2A7Ojs3_?`UwUBQQ;BYTlh=sM;{1yL9(Xz`~OJ6mx9Bnzb^ID#~X2P&AOq)Lb^X} zonr=WD6@&8;!@b!^dYL%Nv^wX9D!X^WQN9jylHy_bQ<BU(S7hX6fR9*Rq8VI5g})~ zR@K|VB?`HN8w+JS4tOI}6g>OJHd{V`*71x}Kf^d(Ky>Ysag|#VQ5TY5(uOa1Qi$O( zUa~ctaDLwVq*J@VDgpp?)~CuNdB!7}+C?>g*tob!vuZc@;D)%8+t97JYji>dd_Vo2 ztc&u6x7=I{O@1S#;ACn=_Q8L47V+K1ims`wUM#p&Z_XaOaWy`9rDQ2DI<L3kfh35| zJvI!25gT;3aB#Q`vBP{6lFVNm8@p)lLwM7l!MA0{oc(+yHamw6XFADMFP0Mz@;4Hu za6m+=#-2~)pit7rn*<Km$3L*@P(;fw77R+dFuY+{(}4n14TkQ*XP_3Y*K{6>iKO7h zB~1lti<AarV?i93S`t>YC&ZmjCDGd;R$M+N)%kc}{e5w``j;^S2?bri&9~>4YbeKD z)xjbI1H;%rLZb|Za!A1DEqj>l;X0F~5`mTm0#Mb3WvC2jv=pFD{L4j(+gXdoG*$ZU zJ@=hXY>dVZVvCZ(r836eI?*Bs69`}h33iJ<eddc-D<_ip5%;6CibL`6@aKc!;jPbA zEQO9^P31U*dGcY}R#l-RFn4y??-y>^-nv@-#e=|kdFn$7hW0y+zv&Z3Z<R|y_ow|E zgZ~vESn%CoBxf5>QOu=Mw!8t$vB4t~+-z?EIHywM^PrywWV;b19z`0nT&GPMtde!( z<{pm9KTD$ifL`^EG5Z$K0%SX!_~dFWRK+%yb^um_aGR-eL-Qi88|p;AOHF6;A3xW^ zXWFnmP<ej$m*LH)ni5!5{d60?p-xCX5u3E8Jd>R0R$6ykCy{MR*6a3rN1rU?MoX4+ zLDzX{HDaTMW61PmV(P0ljW6ZYR6PLMXzZY37w^z{UzOr~C-x1XvNap&in((vqup%@ zDz*fF<!eK9odU1V3gH@tI?GECa~T)#uen}apG!z7Fz|MN?79;@=da)rC^XZq{*kDA zg?O{gOEMwr%LxQQP-LWU7+yXbZF4?%weG=P{A@?}%w^Hip`4kHt!p**$jk{9k&)Bo zRS-qsEioT}@_1xqBDn_XG)zsIn59}4>|0{r5=^tBCN-I6?J~M>Ig_PE1DxfVSez8B zcuk%Jrvz4D7%<sxuB-dz29coLRk<!Bj~$o&(wf~A4ZmFl%R0U2m4+jnU;3%e&T5x4 z|Ga&~spC2rB&`~9=3Ui8wp>eenqTE?70JD}w2U8B`ZB<OIDF}9a&VBmQ4Nzl+sDMR zdp)k)NFG7pizX8LS?S_)0EgyHU{%|k5DM028mAP&!8|Ty$Dl#S{5CSwG@;Xi%aM?& zeaTH2Oya<PM(iJ2a%0uL$pnK%S2R<^kub8{QJ}au;S>j{8){R!*m<+$u$^TVHH~Q_ zzBD@fmH4J0QRPZM-=L=bJX>Bfc>2bE@xaWKPT!(Z@;h==W;aO?ntJ|;QsvAR`A0aV zfTdY(k3}`L<zu1KM8zQjjEFINLa09}&O0{awXW8}HbvbzCk|(YnZ9+WyKr(UJWvl> zq8r}F6Wo@Kr=<hnL5LH9F@)iH4!5z$%@55>-|Ksomn_z(QsdZ0JI4g=U5}--!-K5+ zj07*dgcSEZzDgfiGu3I~9*~CZQBu`}|A1O{9VO#AJ06k_!P+2kl(`_d@x8`@;W9NX z0$-Sg%n+(n&w$n_0-b_bDB0(CcX)6<qV8q%VMRJp{&A}0CYh?3b5HL(ALVf;G~jJ} z)qXSpKGDpB`yR%>1avr(d=|7#54bJTL~csz7Q@1v&cSaWwWhZd6({V>)>{`p2o1f{ zAPSGgbgVM-iklKr7`)Wc#b$cdx?kw?=+s$7p<ey5w}%!CT1rpBDY;L!p5$0zm(eV+ z+=;cB`*K?qhz&P#k-~J$G6kw);UF^m#(+)N8%&K-7jABG!9Ef&!NwZb@gZ=_vhuVm zHM|@>SG&~UM~Iv`U2hhuHDr9@NJ!##prZ*@TMy%>Da=Uj*wuG)Yo+L1#a$!;vn2&y zd_}SBYHVX;NcB1PWxcWi*E19BEi#{^D48*^I?dERGS`GgR|P}RR}M<A!Pi`Fqp^UJ zQ=9KP)sI_sI1fw4P;6coN>80>&iKh=Rd4%vVR@^2?H`&NtiADF4GLByxaPX)wBzSj zWWMJa5n9Go&MNjX6A{|X)p&8bL&7eFC+})^aEA~!5p>kEaIBCzzfOH(A>6~^?UUdW z`z-CS!mvzk_tPiuULlA6MmY}q-n8s$Qt}WRiZ}f84rvfcB%g)V*{2NER$Gg}6)7x{ zmCY&+_MV#5j;@CLcA1=vsToc_p|4O!sd5w)(5Jr|Hx>Iq-XA6mG1|IMbmXl^iFQJQ zjI}`4#JPj8)z#u7ELj{44uf4|?6paWpt5prjCsP>T-c4k&=4(iY|WyyTlvlpg@yE7 z_DS#%8MlKrPb{rej-JsQ;Rznyr_yZG3ZTkMMR3i)<(5xl-}`BYklr^yz4`;rCT+nD zL)I=5o)U6)8!*KqM7GR?Aq<0~j+(@Fp{j)qY8ydy<xbet(nCMEid#G*DGzp3!X`gd zj7}toRf4QwE%S;G_0VSmx%l--HJM?)E<;gu6gg@Dlsq!amv3-_l=|%PSl00RBR??; zd~~EoggFz*gto`N9^s|ZaH3)qtEWRlh}s)1j`ujzUStuUcpY$XT&KW(l95t7ny*K4 zZ?$Yu{00y^GJ-KiD>5v~%u5rpjmn(rnIrsZdlHTHf^m}k;OsWV0PF&@`OB}68hRU6 z;Vo`N@IvO`i%1`)D&?7$%bOwMn-u9%TYS#49~L3E=e`PkUVbT#IQZ~YOh`TNRZ0ql zM`tktT?E&fX++z0MmtG3uU9?S#273c7VpfyMrC0e4-tfZ68^A@I$mo|L=!@o9u@D| zLd)Y{pUPhRy1qhJX#ORZq1Z68>pRIG!Z9rz!sq4EEtf#tXN}+L7z8ty4QyyNIa@RR zGGHaeZZ>JBJnH~<)*i7GL2}SdM+YA2e7XR8Oh6NVi1LzVF*^#YIdm1M1~VqyB+Ed; z)WvjRjGU;k4{}UPbJ(^;;FC1(lWBih&4G>aN|B&P`O?;;!wNlC&39K*gLo<jcPmu5 z6GT&v95DT?{C**ghIy{{NFTmaVx_N({oc@&s{vHG(TBMv$p~>UkLtlbffy#5^$I?& z7(Hu`9L-O?g>oyQfxeEHs&hF%;=!w3kdV)ay<34*aV<yDpHVV*3-^em<QV|S*ow({ zKx?U#DJ^OYife5Z2Sv+jYpF4I=0^ETsDmOmXDBEihF#o|y}mP2rLwK?M)&2bSi(d% zD0XSv?qZeql#LW#)9yd{1Kc5WA6d{ZII_z3Y*6K5POJ7t)(^q=-Ma0`nXfU9WD`l! ztF&Jo4aK&j(&dSwiJ#-<WMl{C-*cZ83`jE2Y-~}TNKj;<`Wa5z@hJ7GVwUDTX3KE* z$T=rID+b<Iq6m0$O`y`u7^*sKTA2+~BFU2Lf&O^N**!er!QofAePPqmA|iqd79t{# zS&O=aIsCakkw07!36VUOo#SIlsY2G%Ev9MzD<~G%>Bl29-1aMd9FKgJRnq+Szmw*( z0mR{_8(`<CT>k(^-9JHJdA;51DWjTdBNTMTMu!N?xr>|u=zUh;`%0K`(^cJc<(|(? zX{(ZwO4_1inkRC<m6$xB_5`?R-HwE=PFZ_f3&c95o*uEjvu{@8P&WRN%iGJudb{u6 zQ2ukN{p*&o_RVo*`ZB%c2l05n5PqfL**=V~c|rVMFT@|IGBHv7FZ?^}ug9qW0K^3? z<EP-J<NY)rylRPXzwqy_zaFFi01y<nj-P^?kMz)f@vAE5dOb$gaPFnUtL{>`{{X2N z{-q?=d_Ip@U$p(v_b1YuME);?A`cRdX1f{4<vyU_e{zlbJv=*yRUm&h<tRVuMt`kG zJt8Kk`>m#Fqp6xGq!V*Bk0586gE;fu1@^pip*4Iu3s}_MpFrxy2&JcNZIm#ymyPm4 z?9x8fbAQqH&;J1OOXvGmf6A)P4{6#f*ArPwUju4ssN<S*9H2R`Eg`Mu9rtiQVzCp; z)~3`vW73ba*|c?4EE7u`{$Ej01T77APUWMU{M?^nxHPqy+ILFY?C+FH`Z{Jr`2fIk zfZUH}a=1KIzt!n(9Na5yuJEd`$RT@2(epSok?jLG_Th9mds$O(;q8j9vUw+{mW_^* zSsl#hoOLAiDl5;tS&gsbl&m9X%SIjT^;j3F*OoZ9Ad8Li$a*qLHy^RAUZ+%fOGj}f z+&k17n2Sf<50}+{`=1Ask@A0#?B}1^(QEN1iRXw^w)l;dTg@BZ{(qHtyxhPHW0AP% zw)7@~q1W2aQBqe{RMRq+N66Dp%aTl_Z;(^?t-~v3fVbOcp}1C5$yY1b=G1cF2DdpF z1I<~a^1pa!!5<9t<6F?s#_x#F^{VUt0E+MQPyYZJSll*ncMWvm2XBx>KiyT&{{R)= z=%4;F%B*fAv^$2Hj1iIY2~WYfRbQh|&eb03PyX_>_>}O@8fr4`pCFWcMip7;)AO}Q zx|9C^yr}Q`M!1e1P=Vg*7(eW#R#uw()wF&^59|e6dPjZ_Xg@l~`>B<eroQ!UACW`* z0YT*MDt;8T59K5F8llelr`)DIRQxGwAIeAUHACI=54mo7V*7i0N=6~k)<}m&Ti|H> zBWkJifhW>yRb=P<5&r<L6>RBu_?kw_@%-ut5B=L!O{4bon$=&DN&4YfZ8PII&k1k2 z`mp}_iU;V~^m?1|>AzHpdB$=F2`{-g^<f|P(WoDzU(srB$EN*KEATr0k)L@Mzs_y_ z)QioF(q56Dc^1FUZT-}X&8t|mlIrwuU^<5TC-a+28%Maf{{Uc9e@Gnv0DCeAdf0#B zP>)9m0Mu95`?``h53%R_RO651JT?CSPKW*_o9(EA*u5ZMWYgBmPxmY)`3)pg-Y?w_ zUVcUf^JBv}&NGaa9MHFKyWDx9yZoEv-z~11e+QqRlbSc2X9GV4F44RXvg%DAYrIrl zt}#tl2$AfM!{uqo>6Yb1bbYR#!N=M=R1--oG<O+bWRhc<z((P|HtLA<o~--LzJ710 z{o9p!=<E17s{G|A?&DS-tozNres8G#+m(6f>-agU{N*R^<55wsNk8M7XYne3TuQ;< zpWuqe{*nIxh>=&`l7GiE&*D`6xRry!Kfx7`{UiSX5hA|{@~HhHe+ZO6nq&R!s_EfF z8qOB!;L(BdZ2hIgl}ma=gZ=R64{y0K{`OUMaHYZAF4M#x{{Tt{`^5%1_<I!GRHoME z<sfu0n8hE2FkQ&zXP-SV<F-2Y?7DjXF)t3aD_zi9>RtAT+<&Q3Tt=anQ1SYf#XWUg zG_?(aEX5%OfUuLw4&8I{UA;}f>rJ0Z-R^AE7nvVPMKtY>Ng7)6))E2rdJ-_Awrx%m zSg6DLnt0Fqp<#PVdE$0DY*#9SQd(&()R0wNsbHC~N6g1a0UoIB*>qvJP~Bp5v^8+l z2+ka0cy<r9c`}yA(#I*iB6nH_NF<6z<~IV<?mZP}TT1#Gxhdr8!?E)nin=SQgv@d8 zg%Pi@Q(WsQ{#5tG1GygdV?U*9hd#GV{zn(=$8*}gOM{Hy<If7ts^Lr`EYsJ@**S6p zz#(ud?A6*)o*mXe_<D+Fw3fdkVh%Cw!t~|Ir;iQ`Z=Xnb1oQ-<cP&%1<SwUzrL?tf zdCT0=d_Z;$8x_Wy7t_x8Wp6BuJgy!^S)T2T=f~F>l=HI?h27KT=P{XJFgfGIrgK4( zLEo2T&n~lR#mD<H>fv?hg}nuD$Ziu&3}cbp><n<|(z~)6IjE;}ZGu`hLQGLHqs5Kb z>hAc;&RmxthfFvoQtK}<6UwppUqB10?hYQd(Kp@Ht_7S@zJ>_m6vf2isgi(R@t@`c z-wJi1d=}cJ6zt?-o&2pmK+egIeX{GO{{Tl^W_$LUvQAE;r;^`+`Dk(IS0TiP^*b%P z8H1WZJr{R78dvF@OJQm530w4|5in!0T$!3`YMl3Pmu$Nf%`ITtV5DmJjoAKRLRg68 z>D>ykwRyk|NpH1YuQr<cn5o}g;Ym#CTpAwcmjc|L=a^u73bS~vX42L6N;{6Jt)7;e z-8hkk2M%cqhy{V99&F%r-(}>vT-Qk8a7Isbj(0d{d&`4H8XQT$@(Y%j%Iep;+i;?D zLxo%zBO~}gyjQCn9j$c(nn7u~)R0Gi;as@9tnzAb<pTo><Kdf0C()Jes4M9HPZ#)H zA25H69!tX89;G*xo=sNGlD%!At@=7$W2tYtdgl78g+$JZvKKo7Cv#=RtsoN8@8@nd z*r?wh^*y>RD$P9=RB~KuY8d$;W3pq7zP4SCnLM}$uKr8T40nd$=-yc2(6r$GgoBi4 zag)_ftF^ts4j|jKy-j73^E+v%>tqu{QuBsKCNM*fU_cx67~Nd@r$bn{fi<GOuBunE zf=5XarIoyrhZd63aB?!y(;e52vOL0ca=s9JDW4vC@;r?E65?&^z9I1*)uQax4P{hQ zUS7i5YDPFY!RMo#J2Z9Qt}qv$xHqUM^xqLFXSmMDDXL+Tne)i(HHFx4E<J`?22YxS z0M_)vUSNBLdZymc*7l1wq|ud>w~F7B+g!%X>Zs&s<P9Ogz?_cXZc(EvtRqI+S!b-x zy_C&l2BE>S7LC1Ow)++0{$gVhm$=3bLo#p=o)d7-CaKkSu(;VHCgP>d@krMX3Jf`? zBR&}Jy#>SWAX>GZj+C;lCS6$=ZA~NI1cBgg;5)SS$o*@_gOqf@L%Cho3Dn!z)*Th6 z^exKueu}B^{JrK$omDGDaJMDI1A&2(dXw2uoMqRPn$J+vQ`+8Swp*I^vTP70k=W*N z{{SR%fIi;ER`N>eKoUy#hV&0{IQXXId4!O)>0SwI>s3yZrqh+xkkeeKOQVgGJU7RT zl3H3%QZtNo+bVE$HL1KnI?&5FuN3|dA#m`=tIN&PIpy1~7mQCS>w=zVk<oZgndUqJ z$X;=Q(`4#x=zccw8rS{LPo?cuaaGn;mWr3c;O4;?a3m5o<|F}*!#T#wm+0R-FiEOB zS!0hsB*!~|&(kU>a)GuI5W;uDy?h&bOVTE$3Alei*r$$13#4Zacqbkds>#!J)Q!^i zfNG!wQ9=kZ52zQ3IaofbPF8s}PNvfyD7~_AKTti64r!<uEjb6&YKW1Mgwe`6U?s@& z3sD}T;NHKs-ZYI3ma?`8Zqy7E!WkYKWsRc@b92|#z&PI{+7zyfL|Scn=H;weG~%|l z0ThgT$ArK+az@$Tk-blv@reyRa8^gnB#h*I(m9`1&m?>ndal>E9Jq^YxLSsmv(-md z_sZAS%@c!|z|)YxM#S>~PC8@TvU)gFLj-!4#hN(s`fPK!4DBMJ$0!>RkcJbJg?ji+ zy&>rlQv}*7*4U?xM+=)t4R}A|q+W4N0f6LTE=QP2;HjqzwR}1+TSH%1Tqq}r)YT=R zo<@=gb_ZNI53?)Ncl$Myi26<RKQ0qc&gQ+9G;+1vu4%|1?moljykn8(5gW`;=8ff! z#xlnce?nMF_7&Su>An`}8hS};M=d2dik6d{8K)p%ZS6fyFfzQyru8wT;#Cf%xez(7 zCZ?&)2MrH#=OB9niN~=<2?fQ(<;3KYa!N8h!ktamx+Qf5=F2WBBASA#2Q<qZfVI)M z5J+&_81AAxGlNOlG}T20oVz-%mMYm>EY%MHx>3pw4gf8{ZV1@<uOkPM8>0^)j@-03 z;QfibtnzAgU9T@l>EWKUO?XnWOfsiDjt3jYq?`=Z_FwKA3mrv#6!r8{NtmaQFuksM zcyn{qdf3~hJ)wD|29B6P<vnmyUK4Leao<eZE_!->RND7_(YM1K5yc#hERn954aZ!L zcO8n;;D-_Il=c>Bd(C7tlha7=k;yZ;TPqw(xp|J<!Nm3I2**X|-d1@vDdjzIrk8JI zRcH$>$6itEIVGLdl<@|#HoSOFAQ0CbvydI<j*272&3$YZd32?=vMP&>!x2;%oE+L{ zvzHeUw=o+5?a!8c&Lw+dF$rEHJy;%b@JSQ=DH{0pa5)KL<7VEH;T=7qcIqQp1slt2 zV+`@y(nEYE^vdSc#Z4a)wR<TU6Ce+y<!m4ZkMMTq7~8H?4%Z!?umf?Xr1?e%KZ+a7 zrS`X2?a;lHW{R9V<-#{LyvN&uy#4%jduJ7yciVD!xrxHz*V2OXw$JPDU8mR3oxWL5 z{?WhMw5n=oq+uT`rziSG{{U*^aeA@JIy!2(^Tz6V_m(gloc!-<?OkXgxzawKt|;Cf zFkD9Dc__t(S|(kK-B$x!;gC;?(Vf(`ONG0TkC)OPXN=tbwdvyj09g5XId5FQ%9X!q zxpcFps-~r7L>$4*mQ3@&eZyiDE@@vp@=YPj<Z*M4w68-~&1|TQvP!yFvA}}UD}S|A zSG^@1GD|J8rWqR2Nr%N5$FS#ra_J<M!;)OF)bxq2g$>1&4G)TyhN+~M&gUJz&WoOX zR~DU!*o2uLVQR@{tByHkh1@ujNhhh?u7J5k4Gfg@l%gXuIVO9aTwF7qhh@#k^9acE z32~R&<H?pg)1_~?L2u=mw59cocQkUDJyGv&H&gx}R|ngi!bd-f?2l>R`qf>L<`mOG zN*PjE>6pen*~^{)7*4WXw}&QISo|%Z1Dl?yX||lSbRjjBv<;7X>T+@a09xPeSgEcw zBBYaBOb51B{{U*+du~(5*PF3j7~9w`Yi<_yOz-gOY#siUzuLB&bhnO`^uYTjZ}zH| z?a+vNpEPws*N*FN3<Kr#zwO)os6=_j%Ws0Vmh&XgzE{!GvUr&rh%RxciXLA5r6g_h zn%<)z%B0b@c<*siNGc(v#Z%2pvYtudh9}4;kCmmuDH|KP4ju<k>G0+@0pm*-J7baU z1nvWf&u`S?KGdSJsjlV~5`yCAk{*6`LC?P}Y5NC~<)=ICI;zrkSlM%I6STeuQGAX^ zy}}v3QBL{5k>oV62F3?DYeq|pgkjl>JlZzR1eZp5(XrHZcUoxSmC~6i8%GF{gci4# zB%k3qi$QKV04ly03c~H`rrVjr*E<qJkNdzMc*;IsSl?B(QvlpF0U_8l<)iF7k0jD3 zbs9p;9kzygd8C@EN6|KVXp7{K8k%=Z#)cL&7h$}-t$Vyj5rYrSPnSj8qr22mGTCvF z(Lf%`R!;X+PV)8+R_2|KV}o4jIUNaiBu{n=UdOfr^7_YmuBw=aWuzGn;m0l?Y25j& zek_>q`Q6FO$;+L&bIp%-cTEZPK9aLbLs3xa;B1k;;v<R9kh-8#Q!;iK%Q=~XUx#Jn z7GsglKDpkDLE2@wSSiglXC>N%)w#j)<hW9J{IP!kcINd@FUk%G&nvJUTbqSoam7^I z^0@lvatUz%0C<z{IZXLX?lS{4giJZ5^^dN4<=%Vmx(Ur@m$%bdF1HrjD;@GF-A^2* zNB9kqq;oQGV{4l3QZVA^%bz0}A_Pbq)4%8|U87Mx*#tGR0@|S%o$oS8_A%xhe+L;p zB#mPL4J~nT43b9^;Z~xusg7)|sFX<v<c-|tEN=S`A-PO!(zBI~k-?)o1NDX-@f#tv zi$$ZXvl`}AM=Vp*Hi?nMC2)L|64S{fu<~*{o<_+N<7`~f5<y{jehSf(Oxo%6oVQT} z<&r45!;MRfPJfE#_*iUsk^GGk{Ul^ZBU~8Z*B3&~WlIkuY9$OI%p;uOwBQl$!&))y z9!Wk~Q_bT1qF)p1bDZ_(>-gLI_e~2%w}<+A->PmFnwQT_NlQ#-WkqFMpe{ctjs}-V z>U=gc;%6<5&B)qPIk{uB8csQ{74S`NxXBx1=cT2L7^j^0jboVB^2l1|{{RT*3`v0m zt=zP|u^1~VU;&WSOB-5wz+=b(-7~j*ZIqro&SRr=(iR_Al(^#_SpNVqzv_=9(40r4 z?XpqUOdzS0MEZ~>jzQtX85nd$_~rym;$n*r$2(3PIN4*b)AkB`MaF0*nbq|6no94J zxr8;2o8<i2c6Vs`Ppgf|ZXJN!%?ou*?s+C@2{|P59m|`CAGg1ncgpHs%(|jdNjb~T z`Rn^`{{UF|tD$IUOI#3I1hQDDE{~~?nXalX!#CPDGmx-I2rdqB1hQ!GKyhm2aSFi= z73Pvy>FFuiQz2vNX$*w4hhVt#Gc9X?0fDV8^&iSTUE((T?cUo=SwVEHq-i5RCY{mq zV=fuxBRC|Ho|!o6R-4}G97B(*&RxnjXeT+q8xDh~lJn!ol;yd0@$~C?ak%u?pR~JI z*{LnDTA`)2&kJQ}{{RaC&J}qlc)@EyAaZ>h<zWSwkdo&~J&wiqBF}Ds>3dDd^1Ojj zM^xFJDJ=Qze=cE-#Q2RYEe*)|q>yQftA$-$FvU?&`54Cfp~t7J@9Qo;_c--&Co8oT z=39B9lIc|HHwQG+I#OaF`p4Ayx#sn=jPrWkcUtAb=OmX*W0spbs@Us3p{b*DB57gI zhYN$*7ykg(90}$u`Hne0vs^N9lf_OlK1@08E<USre1O0StF|*!OxwG#gdtQ2LJ$Z- z5C}p70SH2XLJ+7Bfz=19fI<*}O3(>AfRP9U6d?cxP6p*m^!A^_MNM3P!V{J>j)8Nu zr93m-TxE<ukY%~-tSFe>wo$o0QTyE|gCtdK8+^$z`U=GaoWOah4bCP$-BKLTM=4Qy zKbRLB=Bv}kcO3!0wH&J>0NZ7dG-smAM^M9#XP)~l*S8y@jjiy8%RAcNS7qNB;pfWk zv|P;OW1`f2L&OgQ*jG=Lx#`(!y{+cxwWd+4?UUsUH4rojo74~I5A9JW#`vCC_LjJH z1a1|fP}cZoWfawL#@OklYXEz;F7p^u(plqZ!VtR}cA6TSfNasV;B*C6WZPQbXsfum z7}?7oc^o$X0L@Kpc8KlJ2-<m_{O1wXbz2N^wXI_VMh3w@mAL8MBx+4Hi*{HVIc5jX oWxiV9C9fk!dMdDw8;2)_l(cwfWQ=IXL<l+|W(^qVg>*mv*>-7dJOBUy literal 0 HcmV?d00001 From 68d17a11b3991f786c4213ff7e8de8d0917823c0 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 00:22:02 +0100 Subject: [PATCH 210/283] pop goes the pistol --- deploy.sh | 5 +++-- thin_client.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/deploy.sh b/deploy.sh index 75b6639..c15d00e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -5,11 +5,12 @@ cp ./deploy.sh /home/pi/ cd /home/pi || exit #cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done - +cp ./conjurer/fuckery.jpg ./Conjurer/ +cp ./conurer/willowsisp.png ./Conjurer/ +cp ./wod_beacon.jpg ./Conjurer/ cp ./conjurer/settings.json ./Conjurer/ cp ./conjurer/system_gpt_settings.json ./Conjurer/ - cp ./conjurer/administration_commands.py ./Conjurer/ cp ./conjurer/ai_commands.py ./Conjurer/ cp ./conjurer/ai_functions.py ./Conjurer/ diff --git a/thin_client.py b/thin_client.py index aa7997a..25549e2 100644 --- a/thin_client.py +++ b/thin_client.py @@ -71,8 +71,8 @@ async def on_ready(): platform = "Liquidsoap+RaspberyPi+RolandSeratoDJ202", state = "Where the f*** is the DJ booth?", details= "This Is Radio Hardkor based in Wolne Księstwo Bałuty, the best pirate radiostation on both sides of Łódka river", - buttons= [{"label":"WOLNE BAŁUTY KURWAAA!!!", "url":"http://95.175.16.246:666/mp3-stream"}] - #assets= {"large_image": "", "large_text":"Fuckewry", "small_image":"", "small_text":"Hi!"} + buttons= [{"label":"RADIO", "url":"http://95.175.16.246:666/mp3-stream"}], + assets= {"large_image": "fuckery.jpg", "large_text":"Fuckewry", "small_image":"willowisp.png", "small_text":"Hi!"} ) await client.change_presence(activity=radio_hardkor) From 090adf37416faaa87020a803fec1a067ed446f4f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 00:24:50 +0100 Subject: [PATCH 211/283] gx --- deploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index c15d00e..4454868 100755 --- a/deploy.sh +++ b/deploy.sh @@ -6,8 +6,8 @@ cd /home/pi || exit #cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done cp ./conjurer/fuckery.jpg ./Conjurer/ -cp ./conurer/willowsisp.png ./Conjurer/ -cp ./wod_beacon.jpg ./Conjurer/ +cp ./conjurer/willowisp.png ./Conjurer/ +cp ./conjurer/wod_beacon.jpg ./Conjurer/ cp ./conjurer/settings.json ./Conjurer/ cp ./conjurer/system_gpt_settings.json ./Conjurer/ From 14b9a50f5189f353336f445097bfe467156b36d5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 00:52:38 +0100 Subject: [PATCH 212/283] Rich presence --- thin_client.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/thin_client.py b/thin_client.py index 25549e2..ee39877 100644 --- a/thin_client.py +++ b/thin_client.py @@ -62,28 +62,30 @@ async def on_ready(): logger.info(client.cogs) - radio_is_alive = True - if radio_is_alive: - radio_hardkor = discord.Activity( - name = "Radio Hammer/Radio Conjurer/Wolne Bałuity Kurwa", - url = "http://95.175.16.246:666/mp3-stream", - type = discord.ActivityType.streaming, - platform = "Liquidsoap+RaspberyPi+RolandSeratoDJ202", - state = "Where the f*** is the DJ booth?", - details= "This Is Radio Hardkor based in Wolne Księstwo Bałuty, the best pirate radiostation on both sides of Łódka river", - buttons= [{"label":"RADIO", "url":"http://95.175.16.246:666/mp3-stream"}], - assets= {"large_image": "fuckery.jpg", "large_text":"Fuckewry", "small_image":"willowisp.png", "small_text":"Hi!"} - ) - await client.change_presence(activity=radio_hardkor) - - else: - - await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) - await client.tree.sync() for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) + # add check for icecast stream from radio host in comm subroutine + radio_is_alive = True + if radio_is_alive: + status = discord.Status.online + #radio_hardkor = discord.Activity( + # name = "Radio Hammer/Radio Conjurer/Wolne Bałuity Kurwa", + # url = "http://95.175.16.246:666/mp3-stream", + # type = discord.ActivityType.streaming, + # platform = "Liquidsoap+RaspberyPi+RolandSeratoDJ202", + # state = "Where the f*** is the DJ booth?", + # details= "This Is Radio Hardkor based in Wolne Księstwo Bałuty, the best pirate radiostation on both sides of Łódka river", + # buttons= [{"label":"RADIO", "url":"http://95.175.16.246:666/mp3-stream"}], + # assets= {"large_image": "fuckery.jpg", "large_text":"Fuckewry", "small_image":"willowisp.png", "small_text":"Hi!"} + #) + radio_hardkor = discord.Streaming(name="Radio Hardkor", url="http://95.175.16.246:666/mp3-stream") + await client.change_presence(status= status, activity=radio_hardkor) + + else: + await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) + logger.info("Logged in as ---->", client.user) logger.info("ID:", client.user.id) From b15c2f06b85745d2bc19b97739f51397235baaa0 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 19:53:40 +0100 Subject: [PATCH 213/283] Work ongoing - rich presence and ai assistant --- ai_commands.py | 35 ++++++++++++++++++++++------------- ai_functions.py | 23 +++++++++++++---------- music_commands.py | 18 ++++++++++-------- music_functions.py | 30 +++++++++++++++++++++++++++--- thin_client.py | 26 +++++++------------------- 5 files changed, 79 insertions(+), 53 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 086e4a6..ab1dae8 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -9,14 +9,14 @@ import openai import requests from discord.ext import commands -from ai_functions import handle_response, hammer_assistant_create +import ai_functions from constants import ( DATA, GRAPHICS_PATH, INITIAL_TIME_WAIT, MASTER_TIMEOUT, OPENAICLIENT, - WORD_REACTIONS + WORD_REACTIONS, ) @@ -27,10 +27,9 @@ class Events(commands.Cog): async def cog_load(self): self.logger.info("Starting personal assistants") - await hammer_assistant_create(self.bot) + await ai_functions.hammer_assistant_create(self.bot) self.logger.info("Started personal assistants") - @commands.Cog.listener() async def on_message(self, message): """ @@ -68,14 +67,24 @@ class Events(commands.Cog): return if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: - #hammer pisze dma - user = await self.bot.fetch_user(703985955312238664) - channel = await user.create_dm() - await channel.send(message.content) - await message.reply(f"Poszlo do Saint {message.content}") + + await ai_functions.hammer_assitant_chat(message) + + # hammer pisze dma + + # DM do Saint - przerobic na komende z parametrem "id" + # user = await self.bot.fetch_user(703985955312238664) + # channel = await user.create_dm() + # await channel.send(message.content) + # await message.reply(f"Poszlo do Saint {message.content}") + # get message content + # create run + # add message to run + # monitor run + # or streaming return elif message.author.id == 703985955312238664: - #saint pisze DMA + # saint pisze DMA self.logger.info(message.content) await message.reply("Hejka Saint!") return @@ -84,7 +93,7 @@ class Events(commands.Cog): await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel - #await self.bot.process_commands(message) #uncomment to bilocate + # await self.bot.process_commands(message) #uncomment to bilocate message.content = message.content.lower() @@ -142,7 +151,7 @@ class Events(commands.Cog): bartender = True global MESSAGE_TABLE # pylint: disable=global-statement - result, MESSAGE_TABLE = await handle_response( + result, MESSAGE_TABLE = await ai_functions.handle_response( prompt, vykidailo, bartender, @@ -185,7 +194,7 @@ class Events(commands.Cog): username = message.author.nick else: username = message.author.name - resp, _ = await handle_response( + resp, _ = await ai_functions.handle_response( f"Wytlumacz jakie sa zasady dotyczące treści które możesz generować używając Dalle. Wytłumacz błąd {e} prostym językiem. Przeproś za nadmierną cenzurę. Wytłumacz co mogło być nie tak w prompcie '{message.content}'", True, True, diff --git a/ai_functions.py b/ai_functions.py index 8e13959..a2f2723 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -17,8 +17,10 @@ from constants import ( OPENAICLIENT, WORD_REACTIONS, ) + ASSISTANTS = {} + def num_tokens_from_string(message, model): """ The function takes a string message and a model as input and returns the number of tokens in the @@ -293,18 +295,21 @@ async def get_random_cyclic_message(client): logger.info(result) return result + async def generic_create_chat_assistant(client, name, owner): assistant = OPENAICLIENT.beta.assistants.create( - name = name, - instructions = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", - model = "gpt-4o", - tools= [{"type":"file_search"}] + name=name, + instructions=f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", + model="gpt-4o", + tools=[{"type": "file_search"}], ) thread = OPENAICLIENT.beta.threads.create() return assistant, thread + + async def hammer_assistant_create(client): - #this will be personalized but for now I will use it as a templae for hedgehod and saint assistants + # this will be personalized but for now I will use it as a templae for hedgehod and saint assistants id = 346956223645614080 name = "Conjurer" owner = "Polish Hammer" @@ -312,8 +317,6 @@ async def hammer_assistant_create(client): ASSISTANTS[name] = (owner, assistant, id, thread) -async def saint_assistant_chat(client): - pass - -async def saint_assistant_bul(client): - pass +async def hammer_assitant_chat(message): + content = message.content + await message.channel.send(f"ALOHA!{content}") diff --git a/music_commands.py b/music_commands.py index 9f94eb3..a34d25a 100644 --- a/music_commands.py +++ b/music_commands.py @@ -605,7 +605,7 @@ class MusicModule(commands.Cog): ) async def batch_download(self, ctx): content_type = ctx.message.attachments[0].content_type - check = re.search ("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) + check = re.search("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) await ctx.reply("Kurwa. Aleś mi roboty narobił... No nic. Ku radości. Skal!") async with ctx.typing(): if check: @@ -619,16 +619,20 @@ class MusicModule(commands.Cog): self.logger.info(link) pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" self.logger.info("Verification") - if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): + if re.match(pat, link) and ( + re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link) + ): self.logger.info("%s to link do jutuba", link) - dir_path, files = await music_functions.get_file( + dir_path, files = await music_functions.get_file( ctx, "Youtube", link ) if files: for file_iter in files: global MUZYKA # pylint: disable=global-variable-not-assigned MUZYKA["queue"].insert(0, file_iter) - music_functions.MUSIC_FILE_LIST.update_file_list(file_iter) + music_functions.MUSIC_FILE_LIST.update_file_list( + file_iter + ) MUZYKA["requester"].insert(0, ctx.author) await ctx.send(f"Dodałem do listy {file_iter}") self.logger.info("Jutub udany") @@ -668,10 +672,9 @@ class MusicModule(commands.Cog): ) self.logger.info("Spotifaj udany") - elif re.match(pat, link): self.logger.info("%s to link do czegos", link) - sciezka,files = await music_functions.get_file( + sciezka, files = await music_functions.get_file( ctx, "Pornol", link ) if files: @@ -689,7 +692,6 @@ class MusicModule(commands.Cog): self.logger.info("Spierdalaj") await ctx.message.reply("Spierdalaj") - async def disconnect(self, ctx): """ Asynchronous Python function that disconnects the voice client if it is connected and @@ -799,7 +801,7 @@ class MusicModule(commands.Cog): async def setup(bot): logger = logging.getLogger("discord") - music_functions.MUSIC_FILE_LIST.refresh_file_list() + await music_functions.MUSIC_FILE_LIST.refresh_file_list() logger.info("Playlist generation") await bot.add_cog(MusicModule(bot, "discord")) logger.info("Loading music commands module done") diff --git a/music_functions.py b/music_functions.py index 78372f4..fae2296 100644 --- a/music_functions.py +++ b/music_functions.py @@ -5,6 +5,7 @@ import uuid from pathlib import Path, PurePath from sys import platform +import discord import requests import yt_dlp @@ -34,7 +35,7 @@ class MusicFileList(object): self.logger = uplogger self.logger.info("Created Playlist organizer class") - def refresh_file_list(self): + async def refresh_file_list(self): """ The `refresh_file_list` function attempts to connect to a file service to retrieve a list of music files, and if unsuccessful, it populates the list by scanning a local directory for .mp3 files. @@ -53,6 +54,28 @@ class MusicFileList(object): self.file_service_active = False self.logger.error(e.strerror) self.logger.error("Service Unavailable") + finally: + if self.file_service_active: + status = discord.Status.online + # radio_hardkor = discord.Activity( + # name = "Radio Hammer/Radio Conjurer/Wolne Bałuity Kurwa", + # url = "http://95.175.16.246:666/mp3-stream", + # type = discord.ActivityType.streaming, + # platform = "Liquidsoap+RaspberyPi+RolandSeratoDJ202", + # state = "Where the f*** is the DJ booth?", + # details= "This Is Radio Hardkor based in Wolne Księstwo Bałuty, the best pirate radiostation on both sides of Łódka river", + # buttons= [{"label":"RADIO", "url":"http://95.175.16.246:666/mp3-stream"}], + # assets= {"large_image": "fuckery.jpg", "large_text":"Fuckewry", "small_image":"willowisp.png", "small_text":"Hi!"} + # ) + radio_hardkor = discord.Streaming( + name="Radio Hardkor", url="http://95.175.16.246:666/mp3-stream" + ) + await self.bot.change_presence(status=status, activity=radio_hardkor) + + else: + await self.bot.change_presence( + activity=discord.Game(name="Axe Throwing Darts") + ) def get_file_list(self): """ @@ -150,10 +173,11 @@ async def get_file(ctx, source, link): elif source == "Youtube": link_ok = False pat = r"^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$" - if re.match(pat, link) and (re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link)): + if re.match(pat, link) and ( + re.match(".*youtube.*", link) or re.match(".*youtu.be.*", link) + ): link_ok = True - if link_ok: dir_path = "" file_list = [] diff --git a/thin_client.py b/thin_client.py index ee39877..89e832d 100644 --- a/thin_client.py +++ b/thin_client.py @@ -27,6 +27,11 @@ intents.presences = True intents.members = True intents.messages = True intents.voice_states = True +intents.moderation = True + +# on_member_ban - wyswietl na glownym kanale pieczatke "Niech spierdala" +# on_member_unban - "mam wyjebane" + logger = logging.getLogger("discord") logger.setLevel(logging.DEBUG) handler = handlers.RotatingFileHandler( @@ -60,35 +65,18 @@ async def on_ready(): await client.load_extension("other_commands") await client.load_extension("voice_recognition_commands") - logger.info(client.cogs) await client.tree.sync() for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) # add check for icecast stream from radio host in comm subroutine - radio_is_alive = True - if radio_is_alive: - status = discord.Status.online - #radio_hardkor = discord.Activity( - # name = "Radio Hammer/Radio Conjurer/Wolne Bałuity Kurwa", - # url = "http://95.175.16.246:666/mp3-stream", - # type = discord.ActivityType.streaming, - # platform = "Liquidsoap+RaspberyPi+RolandSeratoDJ202", - # state = "Where the f*** is the DJ booth?", - # details= "This Is Radio Hardkor based in Wolne Księstwo Bałuty, the best pirate radiostation on both sides of Łódka river", - # buttons= [{"label":"RADIO", "url":"http://95.175.16.246:666/mp3-stream"}], - # assets= {"large_image": "fuckery.jpg", "large_text":"Fuckewry", "small_image":"willowisp.png", "small_text":"Hi!"} - #) - radio_hardkor = discord.Streaming(name="Radio Hardkor", url="http://95.175.16.246:666/mp3-stream") - await client.change_presence(status= status, activity=radio_hardkor) - - else: - await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) + await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) logger.info("Logged in as ---->", client.user) logger.info("ID:", client.user.id) + # TODO: ADMINISTRATION From f4f4deb8489fdff4d496f427c0bacc68feba40dd Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 20:08:31 +0100 Subject: [PATCH 214/283] bgfx --- music_commands.py | 2 +- music_functions.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/music_commands.py b/music_commands.py index a34d25a..1870199 100644 --- a/music_commands.py +++ b/music_commands.py @@ -801,7 +801,7 @@ class MusicModule(commands.Cog): async def setup(bot): logger = logging.getLogger("discord") - await music_functions.MUSIC_FILE_LIST.refresh_file_list() + await music_functions.MUSIC_FILE_LIST.refresh_file_list(bot) logger.info("Playlist generation") await bot.add_cog(MusicModule(bot, "discord")) logger.info("Loading music commands module done") diff --git a/music_functions.py b/music_functions.py index fae2296..628ed31 100644 --- a/music_functions.py +++ b/music_functions.py @@ -29,13 +29,13 @@ class MusicFileList(object): file service or local directory and update the list with new items. """ - def __init__(self, uplogger) -> None: + def __init__(self, uplogger,) -> None: self.music_file_list = [] self.file_service_active = False self.logger = uplogger self.logger.info("Created Playlist organizer class") - async def refresh_file_list(self): + async def refresh_file_list(self, bot): """ The `refresh_file_list` function attempts to connect to a file service to retrieve a list of music files, and if unsuccessful, it populates the list by scanning a local directory for .mp3 files. @@ -70,10 +70,10 @@ class MusicFileList(object): radio_hardkor = discord.Streaming( name="Radio Hardkor", url="http://95.175.16.246:666/mp3-stream" ) - await self.bot.change_presence(status=status, activity=radio_hardkor) + await bot.change_presence(status=status, activity=radio_hardkor) else: - await self.bot.change_presence( + await bot.change_presence( activity=discord.Game(name="Axe Throwing Darts") ) From 49e112196749bfd6ddf22e3cc779aacaf601e6a3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 20:11:30 +0100 Subject: [PATCH 215/283] lgtstfx --- music_functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/music_functions.py b/music_functions.py index 628ed31..238749b 100644 --- a/music_functions.py +++ b/music_functions.py @@ -51,11 +51,12 @@ class MusicFileList(object): if platform == "win32": temp_music_file = temp_music_file.replace("/", "\\") self.music_file_list.append(temp_music_file) - self.file_service_active = False + self.file_service_active = False self.logger.error(e.strerror) self.logger.error("Service Unavailable") finally: if self.file_service_active: + self.logger.info("Radio Status: Probably Active") status = discord.Status.online # radio_hardkor = discord.Activity( # name = "Radio Hammer/Radio Conjurer/Wolne Bałuity Kurwa", @@ -73,6 +74,7 @@ class MusicFileList(object): await bot.change_presence(status=status, activity=radio_hardkor) else: + self.logger.info("Radio Status: Rather Unknown") await bot.change_presence( activity=discord.Game(name="Axe Throwing Darts") ) From 97f780688e1ebf34d7103137eef3d866c4435f6e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 20:16:16 +0100 Subject: [PATCH 216/283] Fix --- thin_client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/thin_client.py b/thin_client.py index 89e832d..a54cd6e 100644 --- a/thin_client.py +++ b/thin_client.py @@ -69,10 +69,6 @@ async def on_ready(): await client.tree.sync() for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) - - # add check for icecast stream from radio host in comm subroutine - await client.change_presence(activity=discord.Game(name="Axe Throwing Darts")) - logger.info("Logged in as ---->", client.user) logger.info("ID:", client.user.id) From 93db61558c7c45b83801f5a710c20e9d853e77bb Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 20:54:05 +0100 Subject: [PATCH 217/283] A niech spierdala --- administration_commands.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/administration_commands.py b/administration_commands.py index c480e37..39a95be 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -19,6 +19,22 @@ class AdministrationModule(commands.Cog): self.bot = bot self.logger = logging.getLogger(logger_name) + @commands.hybrid_command( + name="galeria_slaw", + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", + guild=discord.Object(id=664789470779932693), + ) + async def galeria_slaw(self, ctx): + + fnord = discord.File( + "niech_spierdala.png", spoiler=False, description="Niech spierdala" + ) + + for guild in self.bot.guilds: + async for entry in guild.bans(limit=1500): + await ctx.reply(f"User: {entry.user} wyjebany z serwera {guild.name} za {entry.reason} ", file=fnord) + + @commands.hybrid_command( name="update_banlist", description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", @@ -92,6 +108,7 @@ class AdministrationModule(commands.Cog): reason="Automatyczna lista banów", delete_message_seconds=0, ) + #TODO: Maybe use guild.bulk_ban ? cunter_counter += 1 except discord.NotFound: self.logger.info( From 9af2d341796250e6546b2413dd21adf6b07efaa4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 20:56:49 +0100 Subject: [PATCH 218/283] Fix --- administration_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administration_commands.py b/administration_commands.py index 39a95be..c808942 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -27,7 +27,7 @@ class AdministrationModule(commands.Cog): async def galeria_slaw(self, ctx): fnord = discord.File( - "niech_spierdala.png", spoiler=False, description="Niech spierdala" + "/home/pi/Conjurer/niech_spierdala.png", spoiler=False, description="Niech spierdala" ) for guild in self.bot.guilds: From 3f4018fa002236470cba226e8e14fea7da2c587c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 20:59:13 +0100 Subject: [PATCH 219/283] fz --- administration_commands.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/administration_commands.py b/administration_commands.py index c808942..8f698a7 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -11,7 +11,15 @@ import numpy as np from discord.ext import commands, tasks from ai_functions import get_random_cyclic_message -from constants import ENCODING, LOGFILE, LOGSTORE, MEMORY_FIVE_MUZYKA, MEMORY_FIVE_SIARA, LAST_SPONTANEOUS_CALL, TIME_BETWEEN_CALLS +from constants import ( + ENCODING, + LAST_SPONTANEOUS_CALL, + LOGFILE, + LOGSTORE, + MEMORY_FIVE_MUZYKA, + MEMORY_FIVE_SIARA, + TIME_BETWEEN_CALLS, +) class AdministrationModule(commands.Cog): @@ -26,14 +34,17 @@ class AdministrationModule(commands.Cog): ) async def galeria_slaw(self, ctx): - fnord = discord.File( - "/home/pi/Conjurer/niech_spierdala.png", spoiler=False, description="Niech spierdala" - ) - for guild in self.bot.guilds: async for entry in guild.bans(limit=1500): - await ctx.reply(f"User: {entry.user} wyjebany z serwera {guild.name} za {entry.reason} ", file=fnord) - + fnord = discord.File( + "/home/pi/Conjurer/niech_spierdala.png", + spoiler=False, + description="Niech spierdala", + ) + await ctx.reply( + f"User: {entry.user} wyjebany z serwera {guild.name} za {entry.reason} ", + file=fnord, + ) @commands.hybrid_command( name="update_banlist", @@ -108,7 +119,7 @@ class AdministrationModule(commands.Cog): reason="Automatyczna lista banów", delete_message_seconds=0, ) - #TODO: Maybe use guild.bulk_ban ? + # TODO: Maybe use guild.bulk_ban ? cunter_counter += 1 except discord.NotFound: self.logger.info( From e6960c98b834b9bd4c5abf7f0762c9c647ad93ee Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 21:00:53 +0100 Subject: [PATCH 220/283] almost there --- administration_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administration_commands.py b/administration_commands.py index 8f698a7..a5ae245 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -33,7 +33,7 @@ class AdministrationModule(commands.Cog): guild=discord.Object(id=664789470779932693), ) async def galeria_slaw(self, ctx): - + #if isinstance(ctx.channel, discord.DMChannel): for guild in self.bot.guilds: async for entry in guild.bans(limit=1500): fnord = discord.File( From 9701c7233f97c628d5f9a45e6c7dc1bdff6fabf0 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 21:02:48 +0100 Subject: [PATCH 221/283] and there --- administration_commands.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/administration_commands.py b/administration_commands.py index a5ae245..e93a323 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -33,18 +33,18 @@ class AdministrationModule(commands.Cog): guild=discord.Object(id=664789470779932693), ) async def galeria_slaw(self, ctx): - #if isinstance(ctx.channel, discord.DMChannel): - for guild in self.bot.guilds: - async for entry in guild.bans(limit=1500): - fnord = discord.File( - "/home/pi/Conjurer/niech_spierdala.png", - spoiler=False, - description="Niech spierdala", - ) - await ctx.reply( - f"User: {entry.user} wyjebany z serwera {guild.name} za {entry.reason} ", - file=fnord, - ) + if isinstance(ctx.channel, discord.DMChannel): + for guild in self.bot.guilds: + async for entry in guild.bans(limit=1500): + fnord = discord.File( + "/home/pi/Conjurer/niech_spierdala.png", + spoiler=False, + description="Niech spierdala", + ) + await ctx.reply( + f"User: {entry.user} wyjebany z serwera {guild.name} za {entry.reason} ", + file=fnord, + ) @commands.hybrid_command( name="update_banlist", From 637926973ced7cdaf0bed3a0ab794d11be3d3b1d Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 21:48:24 +0100 Subject: [PATCH 222/283] tst --- ai_commands.py | 4 +--- ai_functions.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index ab1dae8..90cb532 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -67,11 +67,9 @@ class Events(commands.Cog): return if isinstance(message.channel, discord.DMChannel): if message.author.id == 346956223645614080: - + await self.bot.process_commands(message) await ai_functions.hammer_assitant_chat(message) - # hammer pisze dma - # DM do Saint - przerobic na komende z parametrem "id" # user = await self.bot.fetch_user(703985955312238664) # channel = await user.create_dm() diff --git a/ai_functions.py b/ai_functions.py index a2f2723..c5e3151 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -318,5 +318,23 @@ async def hammer_assistant_create(client): async def hammer_assitant_chat(message): + logger = logging.getLogger("discord") content = message.content - await message.channel.send(f"ALOHA!{content}") + assistant_data = ASSISTANTS["Conjurer"] + run = OPENAICLIENT.beta.threads.runs.create_and_poll( + thread_id=assistant_data[3].id, + assistant_id=assistant_data[1].id, + instructions=f"Pisze do Ciebie {assistant_data[0]} udziel mu wszelkiej pomocy", + ) + done = False + while not done: + if run.status == "completed": + messsages = OPENAICLIENT.beta.threads.messages.list( + thread_id=assistant_data[3].id + ) + logger.info(messsages) + done = True + else: + logger.info(run.status) + asyncio.sleep(5) + await message.channel.send(f"Echo: {content}") From 5363969456de4825d9afeb3999c6ed8b3fc137a3 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 21:59:09 +0100 Subject: [PATCH 223/283] fx --- ai_functions.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index c5e3151..d7e3cb8 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -297,13 +297,13 @@ async def get_random_cyclic_message(client): async def generic_create_chat_assistant(client, name, owner): - assistant = OPENAICLIENT.beta.assistants.create( + assistant = await OPENAICLIENT.beta.assistants.create( name=name, instructions=f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", model="gpt-4o", tools=[{"type": "file_search"}], ) - thread = OPENAICLIENT.beta.threads.create() + thread = await OPENAICLIENT.beta.threads.create() return assistant, thread @@ -319,8 +319,13 @@ async def hammer_assistant_create(client): async def hammer_assitant_chat(message): logger = logging.getLogger("discord") - content = message.content assistant_data = ASSISTANTS["Conjurer"] + message = await OPENAICLIENT.beta.threads.messages.create( + thread_id=assistant_data[3].id, + role="user", + content = message.content + ) + run = OPENAICLIENT.beta.threads.runs.create_and_poll( thread_id=assistant_data[3].id, assistant_id=assistant_data[1].id, @@ -337,4 +342,4 @@ async def hammer_assitant_chat(message): else: logger.info(run.status) asyncio.sleep(5) - await message.channel.send(f"Echo: {content}") + await message.channel.send(f"Echo: {message.content}") From 885f492f699edfec41e8b5a7bfab3770e6f44515 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:00:37 +0100 Subject: [PATCH 224/283] nxtfx --- ai_functions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index d7e3cb8..be5c741 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -321,12 +321,10 @@ async def hammer_assitant_chat(message): logger = logging.getLogger("discord") assistant_data = ASSISTANTS["Conjurer"] message = await OPENAICLIENT.beta.threads.messages.create( - thread_id=assistant_data[3].id, - role="user", - content = message.content + thread_id=assistant_data[3].id, role="user", content=message.content ) - run = OPENAICLIENT.beta.threads.runs.create_and_poll( + run = await OPENAICLIENT.beta.threads.runs.create_and_poll( thread_id=assistant_data[3].id, assistant_id=assistant_data[1].id, instructions=f"Pisze do Ciebie {assistant_data[0]} udziel mu wszelkiej pomocy", From a981209399f017cfa255f44246e69f5170c6347f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:11:37 +0100 Subject: [PATCH 225/283] Maybe ready ? --- ai_functions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index be5c741..071a51d 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -320,10 +320,10 @@ async def hammer_assistant_create(client): async def hammer_assitant_chat(message): logger = logging.getLogger("discord") assistant_data = ASSISTANTS["Conjurer"] - message = await OPENAICLIENT.beta.threads.messages.create( + ai_message = await OPENAICLIENT.beta.threads.messages.create( thread_id=assistant_data[3].id, role="user", content=message.content ) - + logger.info(ai_message) run = await OPENAICLIENT.beta.threads.runs.create_and_poll( thread_id=assistant_data[3].id, assistant_id=assistant_data[1].id, @@ -332,7 +332,7 @@ async def hammer_assitant_chat(message): done = False while not done: if run.status == "completed": - messsages = OPENAICLIENT.beta.threads.messages.list( + messsages = await OPENAICLIENT.beta.threads.messages.list( thread_id=assistant_data[3].id ) logger.info(messsages) From ecc2e1f107e6eed548dcc6665921ae575c080ff8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:15:41 +0100 Subject: [PATCH 226/283] Deploy --- ai_functions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index 071a51d..6d56687 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -330,14 +330,15 @@ async def hammer_assitant_chat(message): instructions=f"Pisze do Ciebie {assistant_data[0]} udziel mu wszelkiej pomocy", ) done = False + await message.channel.send(f"Echo: {message.content}") while not done: if run.status == "completed": messsages = await OPENAICLIENT.beta.threads.messages.list( thread_id=assistant_data[3].id ) logger.info(messsages) + await message.channel.send(messsages) done = True else: logger.info(run.status) - asyncio.sleep(5) - await message.channel.send(f"Echo: {message.content}") + asyncio.sleep(5) \ No newline at end of file From 06d17a2e4d2f472e9f728ae3c889bf9d2b67e75a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:34:17 +0100 Subject: [PATCH 227/283] step forward --- ai_functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index 6d56687..9ead2bf 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -337,8 +337,11 @@ async def hammer_assitant_chat(message): thread_id=assistant_data[3].id ) logger.info(messsages) - await message.channel.send(messsages) + for item in messsages: + logger.info(item) done = True + elif run.status =='cancelled': + await message.channel.send("Cos sie wywaliło") else: logger.info(run.status) asyncio.sleep(5) \ No newline at end of file From 0dbb79719be03a7964dba422c136c7014dabdfba Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:56:05 +0100 Subject: [PATCH 228/283] Test --- ai_functions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index 9ead2bf..f83c2f3 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -336,9 +336,10 @@ async def hammer_assitant_chat(message): messsages = await OPENAICLIENT.beta.threads.messages.list( thread_id=assistant_data[3].id ) - logger.info(messsages) - for item in messsages: - logger.info(item) + reply_content = messsages.first().content + reply_content.text.value + logger.info(reply_content) + logger.info(reply_content.text.value) done = True elif run.status =='cancelled': await message.channel.send("Cos sie wywaliło") From aa63ffc75414a4d01d800d3985498a03f78ccae1 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:56:34 +0100 Subject: [PATCH 229/283] aha --- ai_functions.py | 1 - test_ai.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 test_ai.py diff --git a/ai_functions.py b/ai_functions.py index f83c2f3..9cb4aa4 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -337,7 +337,6 @@ async def hammer_assitant_chat(message): thread_id=assistant_data[3].id ) reply_content = messsages.first().content - reply_content.text.value logger.info(reply_content) logger.info(reply_content.text.value) done = True diff --git a/test_ai.py b/test_ai.py new file mode 100644 index 0000000..9077295 --- /dev/null +++ b/test_ai.py @@ -0,0 +1,39 @@ +Message( + id='msg_3eWSdgbcU8sbCmJK2momOgQQ', + assistant_id='asst_06eZiwvYNK3MR34suFP60gvg', + attachments=[], + completed_at=None, + content=[TextContentBlock(text=Text( + annotations=[], + value='Cześć! Oto coś do przemyślenia: \n\n„Sukces to suma niewielkich wysiłków powtarzanych dzień po dniu.” — Robert Collier\n\nTo przypomina nam, że często to nie wielkie działania, ale konsekwentne, małe kroki prowadzą do osiągnięcia celu. Jak mogę Ci dzisiaj pomóc?'), + type='text')], + created_at=1731620112, + incomplete_at=None, + incomplete_details=None, + metadata={}, + object='thread.message', + role='assistant', + run_id='run_JGjWQTCEkDcEYpyCJnrkZU8Q', + status=None, + thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD'), +Message( + id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', + assistant_id=None, + attachments=[], + completed_at=None, + content=[ + TextContentBlock(text=Text( + annotations=[], + value='Cześć! Powiedz coś mądrego'), + type='text')], + created_at=1731620110, + incomplete_at=None, + incomplete_details=None, + metadata={}, + object='thread.message', + role='user', + run_id=None, + status=None, + thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD')], + object='list', +first_id='msg_3eWSdgbcU8sbCmJK2momOgQQ', last_id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', has_more=False) From 2bb65bbc400845f3b68c7954e7d7280823563900 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:58:14 +0100 Subject: [PATCH 230/283] Another attempt --- ai_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index 9cb4aa4..dc2cd06 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -336,7 +336,7 @@ async def hammer_assitant_chat(message): messsages = await OPENAICLIENT.beta.threads.messages.list( thread_id=assistant_data[3].id ) - reply_content = messsages.first().content + reply_content = messsages.[0].content logger.info(reply_content) logger.info(reply_content.text.value) done = True From 6a6821ce4c832b204bfe70cb7f34a8635e47216a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 22:59:39 +0100 Subject: [PATCH 231/283] Another one --- ai_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index dc2cd06..ca9d09f 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -336,7 +336,7 @@ async def hammer_assitant_chat(message): messsages = await OPENAICLIENT.beta.threads.messages.list( thread_id=assistant_data[3].id ) - reply_content = messsages.[0].content + reply_content = messsages[0].content logger.info(reply_content) logger.info(reply_content.text.value) done = True From 620e6ddc06a26561c01bac49b19a29ba63e2c708 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 23:03:31 +0100 Subject: [PATCH 232/283] =?UTF-8?q?Najwi=C4=99kszy=20hit=20zespu=C5=82o=20?= =?UTF-8?q?100=20tvarzy=20grzybiarzy=20-=20i=20nie=20jest=20o=20piosenka?= =?UTF-8?q?=20o=20zgubieniu=20czapeczki?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai_functions.py | 4 +++- test_ai.py | 45 ++++++--------------------------------------- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/ai_functions.py b/ai_functions.py index ca9d09f..a02b90c 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -336,7 +336,9 @@ async def hammer_assitant_chat(message): messsages = await OPENAICLIENT.beta.threads.messages.list( thread_id=assistant_data[3].id ) - reply_content = messsages[0].content + logger.info(messsages) + + reply_content = messsages.data[0].content logger.info(reply_content) logger.info(reply_content.text.value) done = True diff --git a/test_ai.py b/test_ai.py index 9077295..8fe2241 100644 --- a/test_ai.py +++ b/test_ai.py @@ -1,39 +1,6 @@ -Message( - id='msg_3eWSdgbcU8sbCmJK2momOgQQ', - assistant_id='asst_06eZiwvYNK3MR34suFP60gvg', - attachments=[], - completed_at=None, - content=[TextContentBlock(text=Text( - annotations=[], - value='Cześć! Oto coś do przemyślenia: \n\n„Sukces to suma niewielkich wysiłków powtarzanych dzień po dniu.” — Robert Collier\n\nTo przypomina nam, że często to nie wielkie działania, ale konsekwentne, małe kroki prowadzą do osiągnięcia celu. Jak mogę Ci dzisiaj pomóc?'), - type='text')], - created_at=1731620112, - incomplete_at=None, - incomplete_details=None, - metadata={}, - object='thread.message', - role='assistant', - run_id='run_JGjWQTCEkDcEYpyCJnrkZU8Q', - status=None, - thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD'), -Message( - id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', - assistant_id=None, - attachments=[], - completed_at=None, - content=[ - TextContentBlock(text=Text( - annotations=[], - value='Cześć! Powiedz coś mądrego'), - type='text')], - created_at=1731620110, - incomplete_at=None, - incomplete_details=None, - metadata={}, - object='thread.message', - role='user', - run_id=None, - status=None, - thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD')], - object='list', -first_id='msg_3eWSdgbcU8sbCmJK2momOgQQ', last_id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', has_more=False) +AsyncCursorPage[Message]( + data=[Message(id='msg_3eWSdgbcU8sbCmJK2momOgQQ', + assistant_id='asst_06eZiwvYNK3MR34suFP60gvg', + attachments=[], + completed_at=None, + content=[TextContentBlock(text=Text(annotations=[], value='Cześć! Oto coś do przemyślenia: \n\n„Sukces to suma niewielkich wysiłków powtarzanych dzień po dniu.” — Robert Collier\n\nTo przypomina nam, że często to nie wielkie działania, ale konsekwentne, małe kroki prowadzą do osiągnięcia celu. Jak mogę Ci dzisiaj pomóc?'), type='text')], created_at=1731620112, incomplete_at=None, incomplete_details=None, metadata={}, object='thread.message', role='assistant', run_id='run_JGjWQTCEkDcEYpyCJnrkZU8Q', status=None, thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD'), Message(id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', assistant_id=None, attachments=[], completed_at=None, content=[TextContentBlock(text=Text(annotations=[], value='Cześć! Powiedz coś mądrego'), type='text')], created_at=1731620110, incomplete_at=None, incomplete_details=None, metadata={}, object='thread.message', role='user', run_id=None, status=None, thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD')], object='list', first_id='msg_3eWSdgbcU8sbCmJK2momOgQQ', last_id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', has_more=False) From be3f639573b9d87147191b8c70cfeda1a03b1920 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 23:05:06 +0100 Subject: [PATCH 233/283] aaaaaaa --- ai_functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index a02b90c..cab4b2b 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -337,7 +337,6 @@ async def hammer_assitant_chat(message): thread_id=assistant_data[3].id ) logger.info(messsages) - reply_content = messsages.data[0].content logger.info(reply_content) logger.info(reply_content.text.value) From 5a74e813eb7053622116ed89f6b5ec4e1dcbd568 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 14 Nov 2024 23:13:18 +0100 Subject: [PATCH 234/283] Work ongoing. --- ai_functions.py | 6 +++++- test_ai.py | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ai_functions.py b/ai_functions.py index cab4b2b..470722e 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -339,7 +339,11 @@ async def hammer_assitant_chat(message): logger.info(messsages) reply_content = messsages.data[0].content logger.info(reply_content) - logger.info(reply_content.text.value) + chat_response = "" + for block in reply_content: + logger.info(block.text.value) + chat_response += block.text.value + await message.channel.send(chat_response) done = True elif run.status =='cancelled': await message.channel.send("Cos sie wywaliło") diff --git a/test_ai.py b/test_ai.py index 8fe2241..8621cfc 100644 --- a/test_ai.py +++ b/test_ai.py @@ -4,3 +4,10 @@ AsyncCursorPage[Message]( attachments=[], completed_at=None, content=[TextContentBlock(text=Text(annotations=[], value='Cześć! Oto coś do przemyślenia: \n\n„Sukces to suma niewielkich wysiłków powtarzanych dzień po dniu.” — Robert Collier\n\nTo przypomina nam, że często to nie wielkie działania, ale konsekwentne, małe kroki prowadzą do osiągnięcia celu. Jak mogę Ci dzisiaj pomóc?'), type='text')], created_at=1731620112, incomplete_at=None, incomplete_details=None, metadata={}, object='thread.message', role='assistant', run_id='run_JGjWQTCEkDcEYpyCJnrkZU8Q', status=None, thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD'), Message(id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', assistant_id=None, attachments=[], completed_at=None, content=[TextContentBlock(text=Text(annotations=[], value='Cześć! Powiedz coś mądrego'), type='text')], created_at=1731620110, incomplete_at=None, incomplete_details=None, metadata={}, object='thread.message', role='user', run_id=None, status=None, thread_id='thread_dDEjGbGm6ICfG75u0KKpoVxD')], object='list', first_id='msg_3eWSdgbcU8sbCmJK2momOgQQ', last_id='msg_GFcnfCFgAthGm2D3oE5d0ZkQ', has_more=False) + + +[TextContentBlock( + text=Text(annotations=[], + value='Cześć! Oto coś do rozważenia: "Największą przeszkodą w naszym życiu jest brak odwagi do wprowadzenia zmian." Niezależnie od tego, jakie masz cele czy marzenia, odwaga do działania i przystosowania się do nowych sytuacji jest kluczem do osiągnięcia sukcesu. Jakie masz przemyślenia na ten temat?'), + type='text') +] From 4a6afa14cc251e81ad96f32b3f6bab89f4e58c90 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 00:37:14 +0100 Subject: [PATCH 235/283] bold commiting at the end of the day --- ai_commands.py | 71 +++++++++++++++++++++++++--------------- ai_functions.py | 44 +++++++++++++------------ constants.py | 2 ++ system_gpt_settings.json | 8 ++++- 4 files changed, 77 insertions(+), 48 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 90cb532..c58f02a 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -11,14 +11,16 @@ from discord.ext import commands import ai_functions from constants import ( + ASSISTANTS, DATA, GRAPHICS_PATH, INITIAL_TIME_WAIT, MASTER_TIMEOUT, OPENAICLIENT, + SPECJALNE_ZIEMNIACZKI, WORD_REACTIONS, ) - +DM_MODE = "assistant" class Events(commands.Cog): def __init__(self, bot): @@ -27,8 +29,41 @@ class Events(commands.Cog): async def cog_load(self): self.logger.info("Starting personal assistants") - await ai_functions.hammer_assistant_create(self.bot) + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if superfryta[4] !="": + self.logger.info( + "Personal assistant exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", + superfryta[0], + superfryta[1], + superfryta[2], + superfryta[3], + superfryta[4] + + ) + thread = await OPENAICLIENT.beta.threads.create() + ASSISTANTS[superfryta[1]] = (superfryta[2], superfryta[0], superfryta[4], thread) + else: + self.logger.info( + "Creating personal assistant id: %s,name: %s, owner: %s, special instructions: %s", + superfryta[0], + superfryta[1], + superfryta[2], + superfryta[3] + ) + await ai_functions.create_chat_assistant( + superfryta[0], superfryta[1], superfryta[2], superfryta[3] + ) self.logger.info("Started personal assistants") + @commands.hybrid_command( + name="switch_dm_mode", + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", + guild=discord.Object(id=664789470779932693), + ) + async def switch_dm_mode(self, ctx): + if isinstance(ctx.channel, discord.DMChannel): + pass + #secrets, assistant, echo, dm + #required parameter from list of values - assistant, dm_broadcast, echo @commands.Cog.listener() async def on_message(self, message): @@ -66,30 +101,14 @@ class Events(commands.Cog): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): - if message.author.id == 346956223645614080: - await self.bot.process_commands(message) - await ai_functions.hammer_assitant_chat(message) - # hammer pisze dma - # DM do Saint - przerobic na komende z parametrem "id" - # user = await self.bot.fetch_user(703985955312238664) - # channel = await user.create_dm() - # await channel.send(message.content) - # await message.reply(f"Poszlo do Saint {message.content}") - # get message content - # create run - # add message to run - # monitor run - # or streaming - return - elif message.author.id == 703985955312238664: - # saint pisze DMA - self.logger.info(message.content) - await message.reply("Hejka Saint!") - return - else: - channel = self.bot.get_channel(1064888712565100614) - await channel.send("Słyszałem ja żem że: " + message.content) - return + for superfryta in SPECJALNE_ZIEMNIACZKI: + if message.author.id == superfryta[0]: + await self.bot.process_commands(message) + await ai_functions.chat_with_assistant(message, superfryta[1]) + return + channel = self.bot.get_channel(1064888712565100614) + await channel.send("Słyszałem ja żem że: " + message.content) + return channel = message.channel # await self.bot.process_commands(message) #uncomment to bilocate diff --git a/ai_functions.py b/ai_functions.py index 470722e..d424563 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -16,9 +16,10 @@ from constants import ( MESSAGE_TABLE_MUZYKA, OPENAICLIENT, WORD_REACTIONS, -) - -ASSISTANTS = {} + ASSISTANTS, + SYSTEM_GPT_SETTINGS, + SPECJALNE_ZIEMNIACZKI + ) def num_tokens_from_string(message, model): @@ -296,32 +297,31 @@ async def get_random_cyclic_message(client): return result -async def generic_create_chat_assistant(client, name, owner): +async def create_chat_assistant(id, name, owner, special_instructions): + logger = logging.getLogger("discord") + instruction=f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o." + instruction+=special_instructions assistant = await OPENAICLIENT.beta.assistants.create( name=name, - instructions=f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o", + instructions=instruction, model="gpt-4o", tools=[{"type": "file_search"}], ) thread = await OPENAICLIENT.beta.threads.create() + logger.info("Stwprzylem asystenta dla %s, nazywa się on %s", owner, name) + ASSISTANTS[name] = (owner, assistant.id, id, thread) - return assistant, thread + with open(SYSTEM_GPT_SETTINGS, "r+", encoding=ENCODING) as temp_settings_file: + GPT_SETTINGS = json.load(temp_settings_file) + GPT_SETTINGS[1][owner][4] = assistant.id + temp_settings_file.seek(0) + json.dump(GPT_SETTINGS, temp_settings_file, indent=4) - -async def hammer_assistant_create(client): - # this will be personalized but for now I will use it as a templae for hedgehod and saint assistants - id = 346956223645614080 - name = "Conjurer" - owner = "Polish Hammer" - assistant, thread = await generic_create_chat_assistant(client, name, owner) - ASSISTANTS[name] = (owner, assistant, id, thread) - - -async def hammer_assitant_chat(message): +async def chat_with_assistant(message, assistant_name): logger = logging.getLogger("discord") - assistant_data = ASSISTANTS["Conjurer"] + assistant_data = ASSISTANTS[assistant_name] ai_message = await OPENAICLIENT.beta.threads.messages.create( - thread_id=assistant_data[3].id, role="user", content=message.content + thread_id=assistant_data[3], role="user", content=message.content ) logger.info(ai_message) run = await OPENAICLIENT.beta.threads.runs.create_and_poll( @@ -330,7 +330,6 @@ async def hammer_assitant_chat(message): instructions=f"Pisze do Ciebie {assistant_data[0]} udziel mu wszelkiej pomocy", ) done = False - await message.channel.send(f"Echo: {message.content}") while not done: if run.status == "completed": messsages = await OPENAICLIENT.beta.threads.messages.list( @@ -349,4 +348,7 @@ async def hammer_assitant_chat(message): await message.channel.send("Cos sie wywaliło") else: logger.info(run.status) - asyncio.sleep(5) \ No newline at end of file + asyncio.sleep(5) + +async def echo(message): + await message.channel.send(f"Echo: {message.content}") diff --git a/constants.py b/constants.py index 267a3b3..60cb612 100644 --- a/constants.py +++ b/constants.py @@ -135,3 +135,5 @@ with open(SYSTEM_GPT_SETTINGS, "r+", encoding=ENCODING) as 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 = {} diff --git a/system_gpt_settings.json b/system_gpt_settings.json index 194defb..e72d41b 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -1,5 +1,11 @@ [ { "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." -} +}, + { + "Polish Hammer" : [346956223645614080, "Conjurer", "Polish Hammer", "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", "Saint Harlot", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], + "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Lena", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża.", ""], + "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""] + } ] \ No newline at end of file From 05166859a355403b94212bb1d6743aa8bc774152 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 00:43:57 +0100 Subject: [PATCH 236/283] :) --- deploy.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 4454868..b483556 100755 --- a/deploy.sh +++ b/deploy.sh @@ -9,7 +9,6 @@ cp ./conjurer/fuckery.jpg ./Conjurer/ cp ./conjurer/willowisp.png ./Conjurer/ cp ./conjurer/wod_beacon.jpg ./Conjurer/ cp ./conjurer/settings.json ./Conjurer/ -cp ./conjurer/system_gpt_settings.json ./Conjurer/ cp ./conjurer/administration_commands.py ./Conjurer/ cp ./conjurer/ai_commands.py ./Conjurer/ From ff2669d0798027b35b82bc13e4c7e1617f242af9 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 00:53:22 +0100 Subject: [PATCH 237/283] fix --- ai_commands.py | 4 +++- ai_functions.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index c58f02a..5d370f5 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -101,13 +101,15 @@ class Events(commands.Cog): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): + self.logger.info(message.author.id) for superfryta in SPECJALNE_ZIEMNIACZKI: + self.logger.info(superfryta[0]) if message.author.id == superfryta[0]: await self.bot.process_commands(message) await ai_functions.chat_with_assistant(message, superfryta[1]) return channel = self.bot.get_channel(1064888712565100614) - await channel.send("Słyszałem ja żem że: " + message.content) + #await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel # await self.bot.process_commands(message) #uncomment to bilocate diff --git a/ai_functions.py b/ai_functions.py index d424563..26b7007 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -17,8 +17,7 @@ from constants import ( OPENAICLIENT, WORD_REACTIONS, ASSISTANTS, - SYSTEM_GPT_SETTINGS, - SPECJALNE_ZIEMNIACZKI + SYSTEM_GPT_SETTINGS ) From b98e3e4ebcff4c5f6be53c13cd021c107638d69c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 00:56:23 +0100 Subject: [PATCH 238/283] Fix --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 5d370f5..468a78f 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -102,7 +102,7 @@ class Events(commands.Cog): return if isinstance(message.channel, discord.DMChannel): self.logger.info(message.author.id) - for superfryta in SPECJALNE_ZIEMNIACZKI: + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.logger.info(superfryta[0]) if message.author.id == superfryta[0]: await self.bot.process_commands(message) From a72dba291bc95a8e076ea70c84c319f15b3e964b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 01:00:41 +0100 Subject: [PATCH 239/283] tst --- ai_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_commands.py b/ai_commands.py index 468a78f..8bb4255 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -41,6 +41,7 @@ class Events(commands.Cog): ) thread = await OPENAICLIENT.beta.threads.create() + self.logger.info("Thread id: %s", thread.id) ASSISTANTS[superfryta[1]] = (superfryta[2], superfryta[0], superfryta[4], thread) else: self.logger.info( From 22730776d40be26032da5417f7133b538cbefa0f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 01:08:34 +0100 Subject: [PATCH 240/283] Fix ? --- ai_commands.py | 1 - ai_functions.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 8bb4255..6496ab4 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -21,7 +21,6 @@ from constants import ( WORD_REACTIONS, ) DM_MODE = "assistant" - class Events(commands.Cog): def __init__(self, bot): self.bot = bot diff --git a/ai_functions.py b/ai_functions.py index 26b7007..bbca134 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -320,12 +320,12 @@ async def chat_with_assistant(message, assistant_name): logger = logging.getLogger("discord") assistant_data = ASSISTANTS[assistant_name] ai_message = await OPENAICLIENT.beta.threads.messages.create( - thread_id=assistant_data[3], role="user", content=message.content + thread_id=assistant_data[3].id, role="user", content=message.content ) logger.info(ai_message) run = await OPENAICLIENT.beta.threads.runs.create_and_poll( thread_id=assistant_data[3].id, - assistant_id=assistant_data[1].id, + assistant_id=assistant_data[1], instructions=f"Pisze do Ciebie {assistant_data[0]} udziel mu wszelkiej pomocy", ) done = False From 06192c41ad815a171db7290b669950b703132c43 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 01:12:37 +0100 Subject: [PATCH 241/283] Semi-fnal commit for today --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 6496ab4..6ae44c8 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -41,7 +41,7 @@ class Events(commands.Cog): ) thread = await OPENAICLIENT.beta.threads.create() self.logger.info("Thread id: %s", thread.id) - ASSISTANTS[superfryta[1]] = (superfryta[2], superfryta[0], superfryta[4], thread) + ASSISTANTS[superfryta[1]] = (superfryta[2], superfryta[4], superfryta[0], thread) else: self.logger.info( "Creating personal assistant id: %s,name: %s, owner: %s, special instructions: %s", From 8429d4efd524c78983317407403bd7ed7480a98b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 11:10:49 +0100 Subject: [PATCH 242/283] Start of Armia Hammera --- ai_commands.py | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 6ae44c8..3b78d44 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -2,13 +2,16 @@ import logging import re import sys +import Enum from datetime import datetime +from typing import Optional import discord import openai import requests from discord.ext import commands + import ai_functions from constants import ( ASSISTANTS, @@ -20,7 +23,13 @@ from constants import ( SPECJALNE_ZIEMNIACZKI, WORD_REACTIONS, ) -DM_MODE = "assistant" +class Dm_Mode(Enum): + ARMIA_HAMMER = 1, + SPECJALNY_ZIEMNIACZEK = 2, + SEKRETNY_SEKSRET = 3 + + + class Events(commands.Cog): def __init__(self, bot): self.bot = bot @@ -59,12 +68,39 @@ class Events(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", guild=discord.Object(id=664789470779932693), ) - async def switch_dm_mode(self, ctx): - if isinstance(ctx.channel, discord.DMChannel): - pass + async def switch_dm_mode(self, ctx, arg: Dm_Mode): + + # add in on message + # broadcast avaiable only for me + with ctx.typing: + if isinstance(ctx.channel, discord.DMChannel): + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if ctx.message.author.id == superfryta[0]: + await ctx.reply("Weszlo") + return + await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") + else: + await ctx.reply("Nope. Nie wiesz jak użyć") #secrets, assistant, echo, dm #required parameter from list of values - assistant, dm_broadcast, echo + @commands.hybrid_command( + name="armia_hammera", + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", + guild=discord.Object(id=664789470779932693), + ) + async def armia_hammera(self, ctx, message_txt: str, recipient: Optional[int]): + pass + + async def armia_hammera_back(): + #user = await self.bot.fetch_user(703985955312238664) + #channel = await user.create_dm() + #await channel.send(message.content) + #await message.reply(f"Poszlo do Saint {message.content}") + pass + + + @commands.Cog.listener() async def on_message(self, message): """ @@ -109,11 +145,9 @@ class Events(commands.Cog): await ai_functions.chat_with_assistant(message, superfryta[1]) return channel = self.bot.get_channel(1064888712565100614) - #await channel.send("Słyszałem ja żem że: " + message.content) + await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel - # await self.bot.process_commands(message) #uncomment to bilocate - message.content = message.content.lower() tdelta = datetime.now() - MASTER_TIMEOUT From 784163b9c903833cb6aff271803b0b8d183ce21b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 11:13:41 +0100 Subject: [PATCH 243/283] Enum fix --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 3b78d44..f459070 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -2,7 +2,7 @@ import logging import re import sys -import Enum +from enum import Enum from datetime import datetime from typing import Optional From cd1beac93feb9a2bb6375bd538ce2ae9e94aa760 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 11:16:40 +0100 Subject: [PATCH 244/283] small fx --- ai_commands.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index f459070..d7fe314 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -28,8 +28,6 @@ class Dm_Mode(Enum): SPECJALNY_ZIEMNIACZEK = 2, SEKRETNY_SEKSRET = 3 - - class Events(commands.Cog): def __init__(self, bot): self.bot = bot From 73057921ec242b20d9ba94c0b37af81f87fdab17 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 13:30:23 +0100 Subject: [PATCH 245/283] Optional argument fix --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index d7fe314..7023a27 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -87,7 +87,7 @@ class Events(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", guild=discord.Object(id=664789470779932693), ) - async def armia_hammera(self, ctx, message_txt: str, recipient: Optional[int]): + async def armia_hammera(self, ctx, message_txt: str, recipient: Optional[discord.User]): pass async def armia_hammera_back(): From 81c43aadccafc5c9ee0e90bdf2caf9c750aabc34 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 14:01:25 +0100 Subject: [PATCH 246/283] xxx --- administration_commands.py | 9 +++++++++ ai_commands.py | 41 ++++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/administration_commands.py b/administration_commands.py index e93a323..4eac164 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -32,6 +32,7 @@ class AdministrationModule(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane') async def galeria_slaw(self, ctx): if isinstance(ctx.channel, discord.DMChannel): for guild in self.bot.guilds: @@ -51,6 +52,8 @@ class AdministrationModule(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Jarl', 'Thane') + async def update_banlist(self, ctx): """ Update a banlist in a Discord guild from preprovided list in channel. @@ -135,6 +138,12 @@ class AdministrationModule(commands.Cog): f"Nie mam na serwerze {guild} uprawnień do banowania. :()" ) await ctx.send("Zrobione tak czy inaczej") + @commands.hybrid_command( + name="archive_channel", + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", + guild=discord.Object(id=664789470779932693), + ) + @commands.has_any_role('Jarl', 'Thane') async def archive_channel(self, channel_no): """ diff --git a/ai_commands.py b/ai_commands.py index 7023a27..b39984c 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -4,7 +4,7 @@ import re import sys from enum import Enum from datetime import datetime -from typing import Optional +from typing import Optional, TypedDict import discord import openai @@ -24,14 +24,18 @@ from constants import ( WORD_REACTIONS, ) class Dm_Mode(Enum): - ARMIA_HAMMER = 1, + ARMIA_HAMMERA = 1, SPECJALNY_ZIEMNIACZEK = 2, - SEKRETNY_SEKSRET = 3 + SEKRETNY_SEKSRET = 3, + ECHO_ECHO = 4 class Events(commands.Cog): def __init__(self, bot): self.bot = bot self.logger = logging.getLogger("discord") + self.armia = {} + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + self.armia[superfryta[2]] = Dm_Mode.SPECJALNY_ZIEMNIACZEK async def cog_load(self): self.logger.info("Starting personal assistants") @@ -66,26 +70,21 @@ class Events(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", guild=discord.Object(id=664789470779932693), ) - async def switch_dm_mode(self, ctx, arg: Dm_Mode): - - # add in on message - # broadcast avaiable only for me - with ctx.typing: + async def switch_dm_mode(self, ctx, dm_mode_arg: Dm_Mode): + async with ctx.typing: if isinstance(ctx.channel, discord.DMChannel): for superfryta in SPECJALNE_ZIEMNIACZKI.values(): if ctx.message.author.id == superfryta[0]: + self.armia[superfryta[2]] = dm_mode_arg await ctx.reply("Weszlo") return await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") else: await ctx.reply("Nope. Nie wiesz jak użyć") - #secrets, assistant, echo, dm - #required parameter from list of values - assistant, dm_broadcast, echo @commands.hybrid_command( name="armia_hammera", - description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", - guild=discord.Object(id=664789470779932693), + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj" ) async def armia_hammera(self, ctx, message_txt: str, recipient: Optional[discord.User]): pass @@ -139,9 +138,21 @@ class Events(commands.Cog): for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.logger.info(superfryta[0]) if message.author.id == superfryta[0]: - await self.bot.process_commands(message) - await ai_functions.chat_with_assistant(message, superfryta[1]) - return + if self.armia[message.author.id]== Dm_Mode.SPECJALNY_ZIEMNIACZEK: + await self.bot.process_commands(message) + await ai_functions.chat_with_assistant(message, superfryta[1]) + return + elif self.armia[message.author.id]== Dm_Mode.ECHO_ECHO: + await ai_functions.echo(message) + return + elif self.armia[message.author.id]== Dm_Mode.ARMIA_HAMMERA: + #self.armia_hammera_back() + return + elif self.armia[message.author.id]== Dm_Mode.SEKRETNY_SEKSRET: + pass + else: + await message.channel.send("Coś się wyebao. Wołaj Hammera") + return channel = self.bot.get_channel(1064888712565100614) await channel.send("Słyszałem ja żem że: " + message.content) return From cd307d642e2a32f9894ed10c4f1145d99a120757 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 15:41:56 +0100 Subject: [PATCH 247/283] fix --- ai_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_commands.py b/ai_commands.py index b39984c..5bcd1cf 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -36,6 +36,7 @@ class Events(commands.Cog): self.armia = {} for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.armia[superfryta[2]] = Dm_Mode.SPECJALNY_ZIEMNIACZEK + self.logger.info(self.armia) async def cog_load(self): self.logger.info("Starting personal assistants") From 62f6b64bfb95299b33f15f166db3f8fca1fccb2b Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 15:44:17 +0100 Subject: [PATCH 248/283] Fix --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 5bcd1cf..60e76ec 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -35,7 +35,7 @@ class Events(commands.Cog): self.logger = logging.getLogger("discord") self.armia = {} for superfryta in SPECJALNE_ZIEMNIACZKI.values(): - self.armia[superfryta[2]] = Dm_Mode.SPECJALNY_ZIEMNIACZEK + self.armia[superfryta[0]] = Dm_Mode.SPECJALNY_ZIEMNIACZEK self.logger.info(self.armia) async def cog_load(self): From e290e7d656b7fa11e07a4f0e0789a4b1e7d6deb5 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 18:33:31 +0100 Subject: [PATCH 249/283] ADdded smth --- ai_commands.py | 26 +++++++++++++++++--------- conjurer_musician/radio_conjurer.liq | 1 - 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 60e76ec..19f7b4b 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -4,7 +4,7 @@ import re import sys from enum import Enum from datetime import datetime -from typing import Optional, TypedDict +from typing import Optional import discord import openai @@ -88,14 +88,21 @@ class Events(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj" ) async def armia_hammera(self, ctx, message_txt: str, recipient: Optional[discord.User]): - pass + await self.armia_hammera_back(ctx, message_txt, recipient) - async def armia_hammera_back(): - #user = await self.bot.fetch_user(703985955312238664) - #channel = await user.create_dm() - #await channel.send(message.content) - #await message.reply(f"Poszlo do Saint {message.content}") - pass + async def armia_hammera_back(self, ctx, message_txt, recipient = None): + recipients = [] + if recipient: + recipients.append(recipient.id) + else: + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + recipients.append(superfryta[0]) + + for item in recipients: + user = await self.bot.fetch_user(item) + channel = await user.create_dm() + self.logger.info("User %s -> %s: %s", ctx.message.author, user, message_txt) + #await channel.send(message_txt) @@ -147,7 +154,8 @@ class Events(commands.Cog): await ai_functions.echo(message) return elif self.armia[message.author.id]== Dm_Mode.ARMIA_HAMMERA: - #self.armia_hammera_back() + ctx = await self.bot.get_context(message) + self.armia_hammera_back(ctx=ctx, message_txt=message.content) return elif self.armia[message.author.id]== Dm_Mode.SEKRETNY_SEKSRET: pass diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index c26f5e1..2db40b0 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -56,7 +56,6 @@ s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", trac # Set up an interactive harbor for controlling the stream interactive.harbor(port = 9999) -#harbor.input(buffer=30.0) # Set up interactive controls for bass boost f = interactive.float("f", description="Frequency", min=0., max=1000., unit="Hz", 200.) g = interactive.float("g", description="Gain", min=0., max=20., unit="dB", 8.) From 230242bf5c834c178d2307238f2d82d1fe92f2d0 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 18:47:29 +0100 Subject: [PATCH 250/283] finished secret dm handling --- ai_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 19f7b4b..5736f49 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -102,8 +102,8 @@ class Events(commands.Cog): user = await self.bot.fetch_user(item) channel = await user.create_dm() self.logger.info("User %s -> %s: %s", ctx.message.author, user, message_txt) - #await channel.send(message_txt) - + await channel.send(message_txt) + await ctx.reply("Poszło") @commands.Cog.listener() From 4f109da335210f4db5bde0ea31dab02d7ed98faa Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 19:01:53 +0100 Subject: [PATCH 251/283] bgfx --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 5736f49..c698ca0 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -72,7 +72,7 @@ class Events(commands.Cog): guild=discord.Object(id=664789470779932693), ) async def switch_dm_mode(self, ctx, dm_mode_arg: Dm_Mode): - async with ctx.typing: + async with ctx.channel.typing(): if isinstance(ctx.channel, discord.DMChannel): for superfryta in SPECJALNE_ZIEMNIACZKI.values(): if ctx.message.author.id == superfryta[0]: From e4a47d025c3e6e55b6cdc339b67568da21a8f5a8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Fri, 15 Nov 2024 19:59:50 +0100 Subject: [PATCH 252/283] safe --- administration_commands.py | 2 +- ai_functions.py | 1 - librarian_commands.py | 6 ++-- music_commands.py | 54 ++++++++++++++++++----------------- other_commands.py | 7 ++++- other_functions.py | 2 -- radio_commands.py | 6 ++++ thin_client.py | 3 -- voice_recognition_commands.py | 2 ++ 9 files changed, 46 insertions(+), 37 deletions(-) diff --git a/administration_commands.py b/administration_commands.py index 4eac164..6113f05 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -32,7 +32,7 @@ class AdministrationModule(commands.Cog): description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", guild=discord.Object(id=664789470779932693), ) - @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane') + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def galeria_slaw(self, ctx): if isinstance(ctx.channel, discord.DMChannel): for guild in self.bot.guilds: diff --git a/ai_functions.py b/ai_functions.py index bbca134..eb1395f 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -64,7 +64,6 @@ async def handle_response( and the response will be generated using a different model :return: The function `handle_response` returns a tuple containing the `result` and `MESSAGE_TABLE`. """ - # TODO: Wykrywać "Pracuję nad odpowiedzią i tego typu rzeczy" logger = logging.getLogger("discord") logger.info("Wywolanie procedury openai z promptem: %s", prompt) temp = {"role": "user", "content": username + ":" + prompt} diff --git a/librarian_commands.py b/librarian_commands.py index e3c67e8..ed858f6 100644 --- a/librarian_commands.py +++ b/librarian_commands.py @@ -28,6 +28,8 @@ class DataModule(commands.Cog): description="Wyświetla losową stronę z losowego komiksu FanSadox. Bardzo NSFW.", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') + async def get_image_sadox(self, ctx): """ Take in a context parameter and retrieve an image related from fansadox collection. @@ -67,7 +69,6 @@ class DataModule(commands.Cog): await ctx.send(file=file) self.logger.info("Get sadox completed") - # TODO: LIBRARIAN @tasks.loop(seconds=3) async def check_data_q(self): """ @@ -118,7 +119,6 @@ class DataModule(commands.Cog): except Empty: pass - # TODO: LIBRARIAN @commands.hybrid_command( name="wyszukaj_linki_do_dokumentow", description="Szuka linkow doi w bazie crossref i podaje linki do scihuba", @@ -181,12 +181,12 @@ class DataModule(commands.Cog): + " Zapytania obsługuje algorytm zasilany czterema chomikami zapierdalającymi w kołowrotku - więc wyniki najwcześniej za kilka godzi - ale mogą być też dni." ) - # TODO: LIBRARIAN @commands.hybrid_command( name="glebokie_gardlo", description="Przygotowuje drinka o nazwie głębokie gardło", guild=discord.Object(id=664789470779932693), ) + async def wyszukaj_linki_do_dokumentow_deep(self, ctx): """ The function `wyszukaj_linki_do_dokumentow_deep` performs a deep search for links to documents in diff --git a/music_commands.py b/music_commands.py index 1870199..12c4288 100644 --- a/music_commands.py +++ b/music_commands.py @@ -25,6 +25,7 @@ class MusicModule(commands.Cog): name="krecimy_pornola", description="Wiadomo co, dla kogo i po co", ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def krecimy_pornola(self, ctx): """ Download a music number from youtube. @@ -35,33 +36,28 @@ class MusicModule(commands.Cog): with the Discord API and """ await ctx.send("Dej mnie chwilkę") - allowed = False - for role in ctx.author.roles: - if role.name == "Bartender": - allowed = True - if allowed: - self.logger.info("Pornol") - async with ctx.typing(): - # wyciagnij linka z kontekstu - content = ctx.message.content.split() - for item_yt in content: - if re.match("http.*", item_yt): - sciezka, files = await music_functions.get_file( - ctx, "Pornol", item_yt + self.logger.info("Pornol") + async with ctx.typing(): + # wyciagnij linka z kontekstu + content = ctx.message.content.split() + for item_yt in content: + if re.match("http.*", item_yt): + sciezka, files = await music_functions.get_file( + ctx, "Pornol", item_yt + ) + if files: + self.logger.info("Pornol udany") + await ctx.send( + f"Jest w tajnym archiwum pod adresem{sciezka})" ) - if files: - self.logger.info("Pornol udany") - await ctx.send( - f"Jest w tajnym archiwum pod adresem{sciezka})" - ) - else: - await ctx.reply("Jak ładnie szefa poprosisz.") @commands.hybrid_command( name="co_na_plejliscie_wariacie", description="Wyswietl kawalki ktore sa na pocztku list radia", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') + async def co_na_plejliscie_wariacie(self, ctx): """ Download a music number from youtube. @@ -101,6 +97,7 @@ class MusicModule(commands.Cog): description="Podaj link do youtube - sciagnie i doda muzyke", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def dej_co_z_jutuba(self, ctx): """ Download a music number from youtube. @@ -132,6 +129,7 @@ class MusicModule(commands.Cog): description="Podaj link do spotify - sciagnie i doda muzyke", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def dej_co_ze_spotifaja(self, ctx): """ Get a playlist or a music number from Spotify. @@ -187,6 +185,7 @@ class MusicModule(commands.Cog): description="Wyłącza muzykę i czyści plejliste.", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def cisza(self, ctx): """ Stop playback of the music and clear the queue. @@ -213,6 +212,7 @@ class MusicModule(commands.Cog): description="Przerzuca na następny kawałek", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def dalej(self, ctx): """ Play next track in queue. @@ -230,12 +230,12 @@ class MusicModule(commands.Cog): voice_client.stop() self.logger.info("Next completed") - # TODO: MUSIC @commands.hybrid_command( name="daj_mi_chwile", description="Pauzuje", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def daj_mi_chwile(self, ctx): """ Pause music playback. @@ -254,12 +254,12 @@ class MusicModule(commands.Cog): voice_client.pause() self.logger.info("Pause completed") - # TODO: MUSIC @commands.hybrid_command( name="graj_dalej", description="Odpauzowuje", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def graj_dalej(self, ctx): """ Unpause music and continue playback. @@ -277,12 +277,12 @@ class MusicModule(commands.Cog): voice_client.resume() self.logger.info("Unpause completed") - # TODO: MUSIC @commands.hybrid_command( name="zagraj_mi_kawalek", description="Wyszukuje w bibliotece muzycznej Hammera kawałek który ma zostać zagrany", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def zagraj_mi_kawalek(self, ctx): """ Play a song or music piece in response to a command @@ -327,12 +327,12 @@ class MusicModule(commands.Cog): "Obawiam się że nie mogę nic znaleźć u siebie. Spróbuj komendy '$dej_co_ze_spotifaja' jeśli masz link" ) - # TODO: MUSIC @commands.hybrid_command( name="zrob_mi_plejliste", description="Generuje playliste - param1 - dlugosc, potem slowa wyszukiwania", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def zrob_mi_plejliste(self, ctx): """ Generate a playlist in queue. First word in this command shall be int defining length of the playlist. @@ -394,12 +394,12 @@ class MusicModule(commands.Cog): await ctx.send(reply) self.logger.info("Plejlista zrobiona") - # TODO: MUSIC @commands.hybrid_command( name="zagraj_muzyke_mego_ludu", description="Gra muzyke wyszukana na bazie tematow konwersacji na kanale NZ", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def zagraj_muzyke_mego_ludu(self, ctx): """ Play completeley random playlist based on messaging history. @@ -480,12 +480,12 @@ class MusicModule(commands.Cog): await ctx.send(reply) self.logger.info("Plejlista zrobiona") - # TODO: MUSIC @commands.hybrid_command( name="parametry_muzyki_mego_ludu", description="Konfiguruje komende zagraj muzyke mojego ludu", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def parametry_muzyki_mego_ludu( self, ctx, ile_historii=1500, ile_slow_kluczowych=15, jak_dluga_plejlista=30 ): @@ -547,6 +547,7 @@ class MusicModule(commands.Cog): description="Włącza muzykę na kanale #nocna-zmiana.", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def graj_muzyko(self, ctx): """ Async function named "graj_muzyko" starts music playback on channel "Nocna Zmiana". @@ -603,6 +604,7 @@ class MusicModule(commands.Cog): description="Zaciąga obiekty z pliku", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def batch_download(self, ctx): content_type = ctx.message.attachments[0].content_type check = re.search("text\/plain; *charset=(.*)", content_type, re.IGNORECASE) diff --git a/other_commands.py b/other_commands.py index 22e4691..3609f2d 100644 --- a/other_commands.py +++ b/other_commands.py @@ -20,6 +20,7 @@ class OtherModule(commands.Cog): @commands.hybrid_command( name="przytul", description="Przytul kogoś - daj mention po komendzie :)" ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def przytul(self, ctx, arg: Optional[discord.Member] = None): """ Generate a text about hugging mentioned user. @@ -53,6 +54,7 @@ class OtherModule(commands.Cog): ) @commands.hybrid_command(name="fabryczka", description="Historia fabryczki") + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def fabryczka(self, ctx): """ Send a general description of "fabryczkagate" to channel. @@ -69,6 +71,7 @@ class OtherModule(commands.Cog): description="Resetowanie zegara - dostępne wyłącznie dla Hammera", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def reset_the_clock(self, ctx): """ Reset the clock on "accidents" in Hammer Fortress adding a new one. @@ -105,6 +108,7 @@ class OtherModule(commands.Cog): name="chata_hammera", description="Czas od ostatniego incydentu w AbsinthHammerTimeSpaceContinuum", ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def chata_hammera(self, ctx): """ Send measured time from last incident in Hammer Fortress to the chat. @@ -127,6 +131,7 @@ class OtherModule(commands.Cog): name="historia_incydentow_u_hammera", description="Wyswietla liste incydentów które miały miejsce w AbsinthHammerTimeSpaceContinuum.", ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def historia_incydentow_u_hammera(self, ctx): """ Send a list of incidents in Hammer Fortress to the chat. @@ -149,12 +154,12 @@ class OtherModule(commands.Cog): index += 1 await ctx.send(return_data) - # TODO: RADIO @commands.hybrid_command( name="radio_hardkor", description="Włącza radio Conjurer na kanale #nocna-zmiana.", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def radio_hardkor(self, ctx): """ Plays the radio hardkor stream in the voice channel. diff --git a/other_functions.py b/other_functions.py index 58a69fc..28957d6 100644 --- a/other_functions.py +++ b/other_functions.py @@ -26,7 +26,6 @@ async def async_iterator_generator(range_of_iterable): """ -# TODO: OTHER async def remove_characters(string, character): """ The `remove_characters` function removes all occurrences of a specified character from a given @@ -39,7 +38,6 @@ async def remove_characters(string, character): return string.replace(character, "") -# TODO: OTHER async def max_weight(lista): """ Take a list of weights and return the maximum weight. diff --git a/radio_commands.py b/radio_commands.py index 53234a1..04d4d35 100644 --- a/radio_commands.py +++ b/radio_commands.py @@ -21,6 +21,7 @@ class RadioModule(commands.Cog): description="Przeskocz kawałek w radiu", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def skip_track(self, ctx): async with ctx.typing(): allowed = False @@ -45,6 +46,7 @@ class RadioModule(commands.Cog): description="Komenda ktora uruchamia radio ponownie jakby się zawiesiło", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def zrestartuj_radio(self, ctx): async with ctx.typing(): allowed = False @@ -68,6 +70,7 @@ class RadioModule(commands.Cog): description="Dodaje do listy ulubionych w radiu", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def dodaj_do_ulubionych(self, ctx): """ Generate a playlist in queue. First word in this command shall be int defining length of the playlist. @@ -104,6 +107,7 @@ class RadioModule(commands.Cog): description="Dodaje do listy ulubionych w radiu", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def request_radio(self, ctx): """ Generate a playlist in queue. First word in this command shall be int defining length of the playlist. @@ -138,6 +142,7 @@ class RadioModule(commands.Cog): description="Dodaje do listy ulubionych w radiu", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def stworz_audycje(self, ctx): """ Generate a playlist in queue. First word in this command shall be int defining length of the playlist. @@ -174,6 +179,7 @@ class RadioModule(commands.Cog): description="Czysci liste ulubionych w radiu", guild=discord.Object(id=664789470779932693), ) + @commands.has_any_role('Legenda', 'Jarl', 'Thane' , 'Bartender') async def wyczysc_ulubione(self, ctx): """ Generate a playlist in queue. First word in this command shall be int defining length of the playlist. diff --git a/thin_client.py b/thin_client.py index a54cd6e..ddfefe6 100644 --- a/thin_client.py +++ b/thin_client.py @@ -73,9 +73,6 @@ async def on_ready(): logger.info("ID:", client.user.id) -# TODO: ADMINISTRATION - - # *================================== Run if __name__ == "__main__": logger.info("Starting discord bot") diff --git a/voice_recognition_commands.py b/voice_recognition_commands.py index 67b9be5..4ff2921 100644 --- a/voice_recognition_commands.py +++ b/voice_recognition_commands.py @@ -161,6 +161,7 @@ class Transcriber(commands.Cog): self.logger = logging.getLogger("discord") @commands.hybrid_command(name="transcribe") + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def test(self, ctx): if self.vc: vc = self.vc # to juz powinien byc voice channel z funkcja conenct @@ -231,6 +232,7 @@ class Transcriber(commands.Cog): self.logger.debug("After %s", after) @commands.command(name="stop_transcribe") + @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender') async def stop(self, ctx): self.check_data.stop() stop_token = CommunicationObject("STOP", None, None) From 69afb00897102bf87026ae408e89e7686f227011 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sat, 16 Nov 2024 23:10:32 +0100 Subject: [PATCH 253/283] bgfx 1 --- ai_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_commands.py b/ai_commands.py index c698ca0..02cafb3 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -22,6 +22,7 @@ from constants import ( OPENAICLIENT, SPECJALNE_ZIEMNIACZKI, WORD_REACTIONS, + MESSAGE_TABLE ) class Dm_Mode(Enum): ARMIA_HAMMERA = 1, From 15253f95fb0abfbd6788d7c9fbcf648228f8aa37 Mon Sep 17 00:00:00 2001 From: migatu <mtuszowski@gmail.com> Date: Wed, 20 Nov 2024 22:05:19 +0000 Subject: [PATCH 254/283] Fun addition --- conjurer_musician/conjurer_musician.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 1e46469..32b89ec 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -11,6 +11,7 @@ import random import re import threading import time +from flask_autoindex import AutoIndex from datetime import datetime from logging import handlers from pathlib import Path @@ -181,7 +182,7 @@ def scan_tracks(): app = Flask(__name__) - +#AutoIndex(app, browse_root="/") # TODO: Odpalić wyszukiwarki w wątkach i dopiero po wszystkim zsumować wyszukiwanie. def wyszukaj(word_list, how_many, _logger=None, return_to_bot=True): From b513704a3db65b89bb7b592fc1894258b558603e Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Mon, 25 Nov 2024 14:39:14 +0100 Subject: [PATCH 255/283] BGFX --- ai_commands.py | 103 +++++++++++++++++++++++++++++---------------- ai_functions.py | 74 ++++++++++++++++++++++++++++---- other_functions.py | 20 +++++++++ 3 files changed, 152 insertions(+), 45 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 02cafb3..5a5353c 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -2,14 +2,15 @@ import logging import re import sys -from enum import Enum from datetime import datetime +from enum import Enum from typing import Optional import discord import openai import requests from discord.ext import commands +from other_functions import discord_friendly_send, discord_friendly_reply import ai_functions @@ -19,17 +20,20 @@ from constants import ( GRAPHICS_PATH, INITIAL_TIME_WAIT, MASTER_TIMEOUT, + MESSAGE_TABLE, OPENAICLIENT, SPECJALNE_ZIEMNIACZKI, WORD_REACTIONS, - MESSAGE_TABLE ) + + class Dm_Mode(Enum): - ARMIA_HAMMERA = 1, - SPECJALNY_ZIEMNIACZEK = 2, - SEKRETNY_SEKSRET = 3, + ARMIA_HAMMERA = (1,) + SPECJALNY_ZIEMNIACZEK = (2,) + SEKRETNY_SEKSRET = (3,) ECHO_ECHO = 4 + class Events(commands.Cog): def __init__(self, bot): self.bot = bot @@ -42,31 +46,36 @@ class Events(commands.Cog): async def cog_load(self): self.logger.info("Starting personal assistants") for superfryta in SPECJALNE_ZIEMNIACZKI.values(): - if superfryta[4] !="": + if superfryta[4] != "": self.logger.info( "Personal assistant exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", superfryta[0], superfryta[1], superfryta[2], superfryta[3], - superfryta[4] - + superfryta[4], ) thread = await OPENAICLIENT.beta.threads.create() self.logger.info("Thread id: %s", thread.id) - ASSISTANTS[superfryta[1]] = (superfryta[2], superfryta[4], superfryta[0], thread) + ASSISTANTS[superfryta[1]] = ( + superfryta[2], + superfryta[4], + superfryta[0], + thread, + ) else: self.logger.info( "Creating personal assistant id: %s,name: %s, owner: %s, special instructions: %s", superfryta[0], superfryta[1], superfryta[2], - superfryta[3] + superfryta[3], ) await ai_functions.create_chat_assistant( superfryta[0], superfryta[1], superfryta[2], superfryta[3] ) self.logger.info("Started personal assistants") + @commands.hybrid_command( name="switch_dm_mode", description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", @@ -80,18 +89,22 @@ class Events(commands.Cog): self.armia[superfryta[2]] = dm_mode_arg await ctx.reply("Weszlo") return - await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") + await ctx.reply( + "Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich" + ) else: await ctx.reply("Nope. Nie wiesz jak użyć") @commands.hybrid_command( name="armia_hammera", - description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj" + description="Jeśli nie wiesz jak użyć tej komendy to nawet nie próbuj", ) - async def armia_hammera(self, ctx, message_txt: str, recipient: Optional[discord.User]): + async def armia_hammera( + self, ctx, message_txt: str, recipient: Optional[discord.User] + ): await self.armia_hammera_back(ctx, message_txt, recipient) - async def armia_hammera_back(self, ctx, message_txt, recipient = None): + async def armia_hammera_back(self, ctx, message_txt, recipient=None): recipients = [] if recipient: recipients.append(recipient.id) @@ -102,10 +115,27 @@ class Events(commands.Cog): for item in recipients: user = await self.bot.fetch_user(item) channel = await user.create_dm() - self.logger.info("User %s -> %s: %s", ctx.message.author, user, message_txt) - await channel.send(message_txt) - await ctx.reply("Poszło") + self.logger.info( + "User %s -> %s: %s", ctx.message.author, user, message_txt + ) + await discord_friendly_send(channel, message_txt) + await discord_friendly_reply(ctx, "Poszło") + @commands.Cog.listener(name="dodaj_do_bazy_wiedzy") + async def dodaj_do_bazy_wiedzy(self, ctx): + pass + + @commands.Cog.listener(name="listuj_baze_wiedzy") + async def listuj_baze_wiedzy(self, ctx): + pass + + @commands.Cog.listener(name="usun_z_bazy_wiedzy") + async def usun_z_bazy_wiedzy(self, ctx): + pass + + @commands.Cog.listener(name="przetworz_plik_linia_po_linii") + async def przetworz_plik_linia_po_linii(self, ctx): + pass @commands.Cog.listener() async def on_message(self, message): @@ -147,24 +177,26 @@ class Events(commands.Cog): for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.logger.info(superfryta[0]) if message.author.id == superfryta[0]: - if self.armia[message.author.id]== Dm_Mode.SPECJALNY_ZIEMNIACZEK: - await self.bot.process_commands(message) + if self.armia[message.author.id] == Dm_Mode.SPECJALNY_ZIEMNIACZEK: + #await self.bot.process_commands(message) await ai_functions.chat_with_assistant(message, superfryta[1]) return - elif self.armia[message.author.id]== Dm_Mode.ECHO_ECHO: + elif self.armia[message.author.id] == Dm_Mode.ECHO_ECHO: await ai_functions.echo(message) return - elif self.armia[message.author.id]== Dm_Mode.ARMIA_HAMMERA: - ctx = await self.bot.get_context(message) + elif self.armia[message.author.id] == Dm_Mode.ARMIA_HAMMERA: + ctx = await self.bot.get_context(message) self.armia_hammera_back(ctx=ctx, message_txt=message.content) return - elif self.armia[message.author.id]== Dm_Mode.SEKRETNY_SEKSRET: + elif self.armia[message.author.id] == Dm_Mode.SEKRETNY_SEKSRET: pass else: - await message.channel.send("Coś się wyebao. Wołaj Hammera") + await discord_friendly_send(message.channel,"Coś się wyebao. Wołaj Hammera") + #await message.channel.send("Coś się wyebao. Wołaj Hammera") return channel = self.bot.get_channel(1064888712565100614) - await channel.send("Słyszałem ja żem że: " + message.content) + await discord_friendly_send(channel, "Słyszałem ja żem że: " + message.content) + #await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel message.content = message.content.lower() @@ -187,13 +219,13 @@ class Events(commands.Cog): # TODO: to zrobic reactiony self.logger.info("Ping z procedury reakcji") if reaction: - emoji = self.bot.get_emoji(self.word_reactions[word][0]) + emoji = self.bot.get_emoji(WORD_REACTIONS[word][0]) await message.add_reaction(emoji) elif security_clearance and vykidailo: - await message.reply(self.word_reactions[word][0]) + await message.reply(WORD_REACTIONS[word][0]) elif not security_clearance: - await message.reply(self.word_reactions[word][0]) - self.word_reactions[word][2] = datetime.now() + await message.reply(WORD_REACTIONS[word][0]) + WORD_REACTIONS[word][2] = datetime.now() # TODO: drobne literówki, mentiony, spacja przed dwukropkiem. napraw. kondziu_mentioned = False @@ -232,12 +264,12 @@ class Events(commands.Cog): "CONVERSATION", ) - if len(result) < 1500: + if len(result) < 999: await message.reply(result) else: - while len(result) > 1500: - await message.reply(result[:1500]) - result = result[1500:] + while len(result) > 999: + await message.reply(result[:999]) + result = result[999:] if "imaginuje sobie:" in message.content: async with channel.typing(): self.logger.info("Poczatek procedury obrazkowej") @@ -314,9 +346,8 @@ class Events(commands.Cog): fnord = discord.File( temp_file_name, spoiler=False, description=message.content ) - await message.reply(f"{image_desc}", file=fnord) - - + #await message.reply(f"{image_desc}", file=fnord) + await discord_friendly_reply(message,f"{image_desc}", file = fnord) # *=========================================== Define Functions diff --git a/ai_functions.py b/ai_functions.py index eb1395f..f720cfc 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -5,8 +5,9 @@ import random import openai import tiktoken - +from other_functions import discord_friendly_send from constants import ( + ASSISTANTS, CYCLIC_WORDS, ENCODING, GPT_SETTINGS, @@ -15,9 +16,61 @@ from constants import ( MESSAGE_TABLE, MESSAGE_TABLE_MUZYKA, OPENAICLIENT, + SYSTEM_GPT_SETTINGS, WORD_REACTIONS, - ASSISTANTS, - SYSTEM_GPT_SETTINGS +) + +#this do per user +VECTOR_STORE_ID = -1 + +def create_vector_store(): + # Create a vector store caled "Financial Statements" + return OPENAICLIENT.beta.vector_stores.create_and_poll(name="Hammer Stash") + #expires_after={ + #"anchor": "last_active_at", + #"days": 7} + #) + +def upload_files_to_vector_store(assistant): + + # Ready the files for upload to OpenAI + file_paths = ["edgar/goog-10k.pdf", "edgar/brka-10k.txt"] + file_streams = [open(path, "rb") for path in file_paths] + + #file = client.beta.vector_stores.files.create_and_poll( + #vector_store_id="vs_abc123", + #file_id="file-abc123" + #) + #batch = client.beta.vector_stores.file_batches.create_and_poll( + #vector_store_id="vs_abc123", + #file_ids=['file_1', 'file_2', 'file_3', 'file_4', 'file_5'] + #) + + + # Use the upload and poll SDK helper to upload the files, add them to the vector store, + # and poll the status of the file batch for completion. + file_batch = OPENAICLIENT.beta.vector_stores.file_batches.upload_and_poll( + vector_store_id=VECTOR_STORE_ID, files=file_streams + ) + + # You can print the status and the file counts of the batch to see the result of this operation. + print(file_batch.status) + print(file_batch.file_counts) + assistant = OPENAICLIENT.beta.assistants.update( + assistant_id=assistant.id, + tool_resources={"file_search": {"vector_store_ids": [VECTOR_STORE_ID]}}, + ) + +def delete_files_from_vector_store(assistant, file_id): + result = OPENAICLIENT.beta.vector_stores.file_batches.delete( + vector_store_id=VECTOR_STORE_ID, files=file_id + ) + + # You can print the status and the file counts of the batch to see the result of this operation. + print(result) + assistant = OPENAICLIENT.beta.assistants.update( + assistant_id=assistant.id, + tool_resources={"file_search": {"vector_store_ids": [VECTOR_STORE_ID]}}, ) @@ -297,8 +350,8 @@ async def get_random_cyclic_message(client): async def create_chat_assistant(id, name, owner, special_instructions): logger = logging.getLogger("discord") - instruction=f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o." - instruction+=special_instructions + instruction = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o." + instruction += special_instructions assistant = await OPENAICLIENT.beta.assistants.create( name=name, instructions=instruction, @@ -315,6 +368,7 @@ async def create_chat_assistant(id, name, owner, special_instructions): temp_settings_file.seek(0) json.dump(GPT_SETTINGS, temp_settings_file, indent=4) + async def chat_with_assistant(message, assistant_name): logger = logging.getLogger("discord") assistant_data = ASSISTANTS[assistant_name] @@ -340,13 +394,15 @@ async def chat_with_assistant(message, assistant_name): for block in reply_content: logger.info(block.text.value) chat_response += block.text.value - await message.channel.send(chat_response) + await discord_friendly_send(message.channel, chat_response) + #await message.channel.send(chat_response) done = True - elif run.status =='cancelled': - await message.channel.send("Cos sie wywaliło") + elif run.status == "cancelled": + await discord_friendly_send(message.channel, "Cos sie wywaliło") else: logger.info(run.status) asyncio.sleep(5) + async def echo(message): - await message.channel.send(f"Echo: {message.content}") + await discord_friendly_send(message.channel, f"Echo: {message.content}") diff --git a/other_functions.py b/other_functions.py index 28957d6..63d79ff 100644 --- a/other_functions.py +++ b/other_functions.py @@ -84,3 +84,23 @@ async def get_stats(client, ctx, history_limit): stats[word] = 1 for name, how_many in stats.items(): yield name, how_many + +async def discord_friendly_reply(ctx, message_content, file=None): + if len(message_content) < 999: + await ctx.reply(message_content, file) + else: + if file: + await ctx.reply("Attachment:", file=file) + while len(message_content) > 999: + await ctx.reply(message_content[:999]) + message_content = message_content[999:] + +async def discord_friendly_send(ctx, message_content, file=None): + if len(message_content) < 999: + await ctx.send(message_content, file) + else: + if file: + await ctx.reply("Attachment:", file=file) + while len(message_content) > 999: + await ctx.send(message_content[:999]) + message_content = message_content[999:] From 194674705e14b322cae7f77cf68b44861570c314 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 28 Nov 2024 10:48:30 +0100 Subject: [PATCH 256/283] fx --- ai_commands.py | 61 ++++++++++++++++++++++++++++++---------------- other_functions.py | 8 +++--- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 5a5353c..f2fc345 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -31,7 +31,7 @@ class Dm_Mode(Enum): ARMIA_HAMMERA = (1,) SPECJALNY_ZIEMNIACZEK = (2,) SEKRETNY_SEKSRET = (3,) - ECHO_ECHO = 4 + ECHO_ECHO = (4,) class Events(commands.Cog): @@ -102,7 +102,11 @@ class Events(commands.Cog): async def armia_hammera( self, ctx, message_txt: str, recipient: Optional[discord.User] ): - await self.armia_hammera_back(ctx, message_txt, recipient) + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if ctx.message.author.id == superfryta[0]: + await self.armia_hammera_back(ctx, message_txt, recipient) + return + await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") async def armia_hammera_back(self, ctx, message_txt, recipient=None): recipients = [] @@ -123,19 +127,35 @@ class Events(commands.Cog): @commands.Cog.listener(name="dodaj_do_bazy_wiedzy") async def dodaj_do_bazy_wiedzy(self, ctx): - pass + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if ctx.message.author.id == superfryta[0]: + #logic here + return + await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") @commands.Cog.listener(name="listuj_baze_wiedzy") async def listuj_baze_wiedzy(self, ctx): - pass + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if ctx.message.author.id == superfryta[0]: + #logic here + return + await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") @commands.Cog.listener(name="usun_z_bazy_wiedzy") async def usun_z_bazy_wiedzy(self, ctx): - pass + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if ctx.message.author.id == superfryta[0]: + #logic here + return + await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") @commands.Cog.listener(name="przetworz_plik_linia_po_linii") async def przetworz_plik_linia_po_linii(self, ctx): - pass + for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + if ctx.message.author.id == superfryta[0]: + #logic here + return + await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") @commands.Cog.listener() async def on_message(self, message): @@ -196,7 +216,6 @@ class Events(commands.Cog): return channel = self.bot.get_channel(1064888712565100614) await discord_friendly_send(channel, "Słyszałem ja żem że: " + message.content) - #await channel.send("Słyszałem ja żem że: " + message.content) return channel = message.channel message.content = message.content.lower() @@ -285,12 +304,12 @@ class Events(commands.Cog): ) except openai.APITimeoutError as e: # Handle timeout error, e.g. retry or log - await message.reply( - f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" + await discord_friendly_reply( + message, f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}" ) except openai.APIConnectionError as e: - await message.reply( - f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" + await discord_friendly_reply( + message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}" ) except openai.BadRequestError as e: # Handle invalid request error, e.g. validate parameters or log @@ -306,29 +325,29 @@ class Events(commands.Cog): username, "RANDOM", ) - await message.reply( - f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" + await discord_friendly_reply( + message, f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}" ) except openai.AuthenticationError as e: # Handle authentication error, e.g. check credentials or log - await message.reply( - f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" + await discord_friendly_reply( + message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}" ) except openai.PermissionDeniedError as e: # Handle permission error, e.g. check scope or log - await message.reply( + await discord_friendly_reply( ( - f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" + message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}" ) ) except openai.RateLimitError as e: - await message.reply( - f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" + await discord_friendly_reply( + message, f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}" ) except openai.APIError as e: # Handle API error, e.g. retry or log - await message.reply( - f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" + await discord_friendly_reply( + message, f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}" ) if response: self.logger.info(response) diff --git a/other_functions.py b/other_functions.py index 63d79ff..123c456 100644 --- a/other_functions.py +++ b/other_functions.py @@ -87,20 +87,20 @@ async def get_stats(client, ctx, history_limit): async def discord_friendly_reply(ctx, message_content, file=None): if len(message_content) < 999: - await ctx.reply(message_content, file) + await ctx.reply(message_content) else: - if file: - await ctx.reply("Attachment:", file=file) while len(message_content) > 999: await ctx.reply(message_content[:999]) message_content = message_content[999:] + if file: + await ctx.reply("Attachment:", file=file) async def discord_friendly_send(ctx, message_content, file=None): if len(message_content) < 999: await ctx.send(message_content, file) else: if file: - await ctx.reply("Attachment:", file=file) + await ctx.send("Attachment:", file=file) while len(message_content) > 999: await ctx.send(message_content[:999]) message_content = message_content[999:] From 9823a940256c52b2849d03f60afaa1e6e28e6fd9 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 28 Nov 2024 11:10:34 +0100 Subject: [PATCH 257/283] aas --- conjurer_musician/radio_conjurer.liq | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index 2db40b0..d536607 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -63,10 +63,10 @@ b = bass_boost(frequency=f, gain=g, s) s = add([s, b]) # Set up interactive control for main volume -a = interactive.float("main_volume", min=0., max=20., 0.4) +a = interactive.float("main_volume", min=0., max=20., 1.1) s = compress.multiband.interactive(bands=7, s) -mic_gain = interactive.float("mic_volume", min=0., max=120., 6.) +mic_gain = interactive.float("mic_volume", min=0., max=120., 0.5) # Apply audio processing effects tmic = buffer(input.pulseaudio()) # Microphone @@ -78,7 +78,7 @@ mic = blank.strip(max_blank=15., min_noise=.1, threshold=-30., mic) mic = fallback(id="switcher2", track_sensitive=false, [mic, blank()]) # Apply audio processing effects -s = nrj(normalize(s)) +s = nrj(s) s = amplify(a, s) # Skip blank sections in the stream s = blank.skip(max_blank=10., s) From 3d5d8a2c147c1b3cb385847e9677a7a3003361b4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 28 Nov 2024 11:24:22 +0100 Subject: [PATCH 258/283] saving repeatable promtp --- ai_commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index f2fc345..d044f06 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -5,7 +5,7 @@ import sys from datetime import datetime from enum import Enum from typing import Optional - +from pathlib import Path import discord import openai import requests @@ -356,9 +356,13 @@ class Events(commands.Cog): self.logger.debug("Wynikowy obrazek pod url: %s", image_url) response = requests.get(image_url, timeout=360) + temp_file_name = message.content + ".png" temp_file_name = GRAPHICS_PATH + message.content + ".png" - + num = 0 + while (Path(temp_file_name)).exists(): + temp_file_name = GRAPHICS_PATH + message.content + str(num) + ".png" + num += 1 with open(temp_file_name, "wb") as dalle_file: dalle_file.write(response.content) self.logger.info("Koniec procedury obrazkowej.") From b6bbb50707858200dabad4db9886d4bbd8a01a1c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Sun, 1 Dec 2024 12:00:43 +0100 Subject: [PATCH 259/283] bugfix --- ai_commands.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index d044f06..25a62c8 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -218,18 +218,18 @@ class Events(commands.Cog): await discord_friendly_send(channel, "Słyszałem ja żem że: " + message.content) return channel = message.channel - message.content = message.content.lower() + message_content_lower = message.content.lower() tdelta = datetime.now() - MASTER_TIMEOUT tdelta = tdelta.total_seconds() - if "opowiedz o fabryczce" in message.content: + if "opowiedz o fabryczce" in message_content_lower: await message.reply(DATA["fabryczka"]) - if "opowiedz mi o fabryczce" in message.content: + if "opowiedz mi o fabryczce" in message_content_lower: await message.reply(DATA["fabryczka"]) if tdelta > INITIAL_TIME_WAIT: for word in WORD_REACTIONS: - if re.search(r"\b" + word + r"\b", message.content): + if re.search(r"\b" + word + r"\b", message_content_lower): tdelta = datetime.now() - WORD_REACTIONS[word][2] tdelta = tdelta.total_seconds() security_clearance = WORD_REACTIONS[word][4] @@ -252,11 +252,11 @@ class Events(commands.Cog): if mention == self.bot.user: kondziu_mentioned = True - if kondziu_mentioned or "conjurer:" in message.content: + if kondziu_mentioned or "conjurer:" in message_content_lower: async with channel.typing(): self.logger.debug("Procedura chatu") - message.content = message.content.replace("conjurer: ", "") + message_content_lower = message_content_lower.replace("conjurer: ", "") if message.author.nick: username = message.author.nick else: @@ -292,8 +292,8 @@ class Events(commands.Cog): if "imaginuje sobie:" in message.content: async with channel.typing(): self.logger.info("Poczatek procedury obrazkowej") - message.content = message.content.replace("imaginuje sobie: ", "") - self.logger.debug("Wywolanie obrazka: %s", message.content) + message_content_lower = message_content_lower.replace("imaginuje sobie: ", "") + self.logger.debug("Wywolanie obrazka: %s", message_content_lower) try: response = await OPENAICLIENT.images.generate( model="dall-e-3", From f9c2bbf9930443b185464b87714c19c6cef61711 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 5 Dec 2024 15:23:06 +0100 Subject: [PATCH 260/283] BGFX --- administration_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/administration_commands.py b/administration_commands.py index 6113f05..c964d48 100644 --- a/administration_commands.py +++ b/administration_commands.py @@ -249,7 +249,7 @@ class AdministrationModule(commands.Cog): self.logger.debug(TIME_BETWEEN_CALLS) LAST_SPONTANEOUS_CALL = datetime.now() async with channel.typing(): - message = await get_random_cyclic_message() + message = await get_random_cyclic_message(self.bot) self.logger.info("Odpowiedz") self.logger.info(message) await channel.send(message) From 2e2395698668cfb98333165f05dd2d1d6f36d5fd Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Dec 2024 00:59:21 +0100 Subject: [PATCH 261/283] BGFX --- other_functions.py | 6 +++--- system_gpt_settings.json | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/other_functions.py b/other_functions.py index 123c456..263217e 100644 --- a/other_functions.py +++ b/other_functions.py @@ -97,10 +97,10 @@ async def discord_friendly_reply(ctx, message_content, file=None): async def discord_friendly_send(ctx, message_content, file=None): if len(message_content) < 999: - await ctx.send(message_content, file) + await ctx.send(message_content) else: - if file: - await ctx.send("Attachment:", file=file) while len(message_content) > 999: await ctx.send(message_content[:999]) message_content = message_content[999:] + if file: + await ctx.reply("Attachment:", file=file) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index e72d41b..7a71e8d 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -3,9 +3,11 @@ "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." }, { - "Polish Hammer" : [346956223645614080, "Conjurer", "Polish Hammer", "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", "Saint Harlot", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], + "Towarzysz Młotek" : [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.", ""], + "Towarazysz Nieszczęścoe": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęśćie", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Lena", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża.", ""], - "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""] - } + "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""], + "Towarzysz Jakkolwiek": [266986215461486592, "Conjurer", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś go rozgniewać Cię przeraża do poziomu histerii.", ""] + +} ] \ No newline at end of file From cc57b0c52d41fa0c54bcada0931df37f1d52d8fa Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Dec 2024 23:21:21 +0100 Subject: [PATCH 262/283] FX --- ai_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_commands.py b/ai_commands.py index 25a62c8..74e4f31 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -87,6 +87,7 @@ class Events(commands.Cog): for superfryta in SPECJALNE_ZIEMNIACZKI.values(): if ctx.message.author.id == superfryta[0]: self.armia[superfryta[2]] = dm_mode_arg + self.logger.info(self.armia) await ctx.reply("Weszlo") return await ctx.reply( From a4065684b00464ca6a999b399b5cfd6f67f5e15c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Dec 2024 23:45:40 +0100 Subject: [PATCH 263/283] New deploy --- deploy.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index b483556..8dfe419 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,27 +1,82 @@ #!/bin/bash + +total_commands=24 +current_command=0 + +function print_progress { + current_command=$((current_command + 1)) + percent=$((current_command * 100 / total_commands)) + echo "Executing command $current_command/$total_commands ($percent%): $1" +} + cd /home/pi/conjurer || exit +print_progress "cd /home/pi/conjurer" + git pull +print_progress "git pull" + cp ./deploy.sh /home/pi/ +print_progress "cp ./deploy.sh /home/pi/" + cd /home/pi || exit -#cp ./conjurer/bot.py ./Conjurer/ #legacy to be deprecated after refactor is done +print_progress "cd /home/pi" + +cp ./conjurer/LICENSE ./Conjurer/LICENSE +print_progress "cp ./conjurer/LICENSE ./Conjurer/LICENSE" cp ./conjurer/fuckery.jpg ./Conjurer/ +print_progress "cp ./conjurer/fuckery.jpg ./Conjurer/" + cp ./conjurer/willowisp.png ./Conjurer/ +print_progress "cp ./conjurer/willowisp.png ./Conjurer/" + cp ./conjurer/wod_beacon.jpg ./Conjurer/ +print_progress "cp ./conjurer/wod_beacon.jpg ./Conjurer/" + cp ./conjurer/settings.json ./Conjurer/ +print_progress "cp ./conjurer/settings.json ./Conjurer/" + +cp ./conjurer/system_gpt_settings.json ./Conjurer/system_gpt_settings.json +print_progress "cp ./conjurer/system_gpt_settings.json ./Conjurer/system_gpt_settings.json" cp ./conjurer/administration_commands.py ./Conjurer/ +print_progress "cp ./conjurer/administration_commands.py ./Conjurer/" + cp ./conjurer/ai_commands.py ./Conjurer/ +print_progress "cp ./conjurer/ai_commands.py ./Conjurer/" + cp ./conjurer/ai_functions.py ./Conjurer/ +print_progress "cp ./conjurer/ai_functions.py ./Conjurer/" + cp ./conjurer/communication_subroutine.py ./Conjurer/ +print_progress "cp ./conjurer/communication_subroutine.py ./Conjurer/" + cp ./conjurer/constants.py ./Conjurer/ +print_progress "cp ./conjurer/constants.py ./Conjurer/" + cp ./conjurer/librarian_commands.py ./Conjurer/ +print_progress "cp ./conjurer/librarian_commands.py ./Conjurer/" + cp ./conjurer/music_commands.py ./Conjurer/ +print_progress "cp ./conjurer/music_commands.py ./Conjurer/" + cp ./conjurer/music_functions.py ./Conjurer/ +print_progress "cp ./conjurer/music_functions.py ./Conjurer/" + cp ./conjurer/other_commands.py ./Conjurer/ +print_progress "cp ./conjurer/other_commands.py ./Conjurer/" + cp ./conjurer/other_functions.py ./Conjurer/ +print_progress "cp ./conjurer/other_functions.py ./Conjurer/" + cp ./conjurer/radio_commands.py ./Conjurer/ +print_progress "cp ./conjurer/radio_commands.py ./Conjurer/" + cp ./conjurer/voice_recognition_commands.py ./Conjurer/ +print_progress "cp ./conjurer/voice_recognition_commands.py ./Conjurer/" + 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" \ No newline at end of file From 41ad91ab93c2c7b6ff35fdd65e73dee00464cf8a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Dec 2024 23:48:25 +0100 Subject: [PATCH 264/283] dsd --- system_gpt_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index 7a71e8d..9f47a72 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -4,7 +4,7 @@ }, { "Towarzysz Młotek" : [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.", ""], - "Towarazysz Nieszczęścoe": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęśćie", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], + "Towarazysz Nieszczęście": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęśćie", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Lena", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża.", ""], "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""], "Towarzysz Jakkolwiek": [266986215461486592, "Conjurer", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś go rozgniewać Cię przeraża do poziomu histerii.", ""] From 69bf2973b9686bf3660be0289079b9ecbf9260ca Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Dec 2024 23:51:19 +0100 Subject: [PATCH 265/283] DDD --- system_gpt_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index 9f47a72..31f597f 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -4,10 +4,10 @@ }, { "Towarzysz Młotek" : [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.", ""], - "Towarazysz Nieszczęście": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęśćie", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], + "Towarazysz Nieszczęście": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęście", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Lena", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża.", ""], "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""], - "Towarzysz Jakkolwiek": [266986215461486592, "Conjurer", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś go rozgniewać Cię przeraża do poziomu histerii.", ""] + "Towarzysz Jakkolwiek": [266986215461486592, "Smok Jerzy", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś go rozgniewać Cię przeraża do poziomu histerii.", ""] } ] \ No newline at end of file From 8ba676df9f45835e804dc1f1b9e5df76cb00da04 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Wed, 18 Dec 2024 23:53:38 +0100 Subject: [PATCH 266/283] Jak wiele literowek mozna zrobic w dwoch slowach --- system_gpt_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index 31f597f..b7a0317 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -4,7 +4,7 @@ }, { "Towarzysz Młotek" : [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.", ""], - "Towarazysz Nieszczęście": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęście", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], + "Towarzysz Nieszczęście": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęście", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Lena", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża.", ""], "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""], "Towarzysz Jakkolwiek": [266986215461486592, "Smok Jerzy", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś go rozgniewać Cię przeraża do poziomu histerii.", ""] From f9dcec6d9eac40fe6fd059aca84bd680164e068c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 00:07:04 +0100 Subject: [PATCH 267/283] Testr --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 74e4f31..02c9554 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -194,7 +194,7 @@ class Events(commands.Cog): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): - self.logger.info(message.author.id) + self.logger.info(message.author) for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.logger.info(superfryta[0]) if message.author.id == superfryta[0]: From ca2e1a35aa2119aad1bba3ac86c957f9e62244b4 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 00:10:23 +0100 Subject: [PATCH 268/283] kurwa serio --- ai_commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ai_commands.py b/ai_commands.py index 02c9554..13a845e 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -85,6 +85,8 @@ class Events(commands.Cog): async with ctx.channel.typing(): if isinstance(ctx.channel, discord.DMChannel): for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + self.logger.info(ctx.message.author.id) + self.logger.info(superfryta) if ctx.message.author.id == superfryta[0]: self.armia[superfryta[2]] = dm_mode_arg self.logger.info(self.armia) From 85f91b3994831c7c6b5f5b38e0eede201f3e750a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 00:14:08 +0100 Subject: [PATCH 269/283] =?UTF-8?q?serio=20kurwa=20ja=20pierdole=20niech?= =?UTF-8?q?=20to=20kurwa=20bedzie=20ostatnia=20poprawka=20debilnego=20b?= =?UTF-8?q?=C5=82edu=20w=20tym=20pierdolonym=20kodzie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 13a845e..a725b11 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -88,7 +88,7 @@ class Events(commands.Cog): self.logger.info(ctx.message.author.id) self.logger.info(superfryta) if ctx.message.author.id == superfryta[0]: - self.armia[superfryta[2]] = dm_mode_arg + self.armia[ctx.message.author.id] = dm_mode_arg self.logger.info(self.armia) await ctx.reply("Weszlo") return From 4ece2f215e4808a522f8c8b8612e1846503c690c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 00:27:45 +0100 Subject: [PATCH 270/283] 1111 --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index a725b11..1a6a241 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -118,7 +118,7 @@ class Events(commands.Cog): else: for superfryta in SPECJALNE_ZIEMNIACZKI.values(): recipients.append(superfryta[0]) - + self.logger.info(recipients) for item in recipients: user = await self.bot.fetch_user(item) channel = await user.create_dm() From aa92b7f51212d78995e7c7cb10c6a56e98116421 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 11:08:29 +0100 Subject: [PATCH 271/283] FX --- ai_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ai_commands.py b/ai_commands.py index 1a6a241..ed787ea 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -112,6 +112,7 @@ class Events(commands.Cog): await ctx.reply("Tylko członkowie Armii Hammera mogą używać. Nie jesteś jednym z nich") async def armia_hammera_back(self, ctx, message_txt, recipient=None): + self.logger.info("Armia Hammera") recipients = [] if recipient: recipients.append(recipient.id) From e263857e4f2933f5b9697da94954fe632c645616 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 11:26:55 +0100 Subject: [PATCH 272/283] - --- system_gpt_settings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index b7a0317..0df7f90 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -1,13 +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." + "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." }, { - "Towarzysz Młotek" : [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.", ""], - "Towarzysz Nieszczęście": [703985955312238664, "Saint Conjurer", "Towarzysz Nieszczęście", "Jesteś bardzo uprzejmy, kulturalny i masz najlepsze możliwe maniery.", ""], - "Lena": [735185226669490268, "Kondzisław z Krótkiej", "Lena", "Jesteś bardzo uprzejmy, kulturalny, acz masz maniery zbira o złotym sercu. Dodatkowo twoja rozmówczyni Cię absolutnie przeraża.", ""], - "Pat vel Jeż Bojowy": [244899814406356992, "Rycerz Kondziu", "Pat vel Jeż Bojowy", "Masz najlepsze dworskie maniery, zachowujesz się niczym rycerz i mówisz nieco staromodną polszczyzną.", ""], - "Towarzysz Jakkolwiek": [266986215461486592, "Smok Jerzy", "Towarzysz Jakkolwiek", "Zrobisz absolutnie wszystko jako asystent, ponieważ sama myśl o tym że mógłbyś go rozgniewać Cię przeraża do poziomu histerii.", ""] + "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ęście", "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 Lena", "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 From 21ec8d8bc99a398530398a2edf4de9cc5ef94c1f Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 11:54:53 +0100 Subject: [PATCH 273/283] Some would say it's ccr. But I'm not i*n. --- ai_commands.py | 4 ++-- ai_functions.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index ed787ea..526ec1a 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -45,7 +45,7 @@ class Events(commands.Cog): async def cog_load(self): self.logger.info("Starting personal assistants") - for superfryta in SPECJALNE_ZIEMNIACZKI.values(): + for superfryta_id, superfryta in SPECJALNE_ZIEMNIACZKI: if superfryta[4] != "": self.logger.info( "Personal assistant exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", @@ -72,7 +72,7 @@ class Events(commands.Cog): superfryta[3], ) await ai_functions.create_chat_assistant( - superfryta[0], superfryta[1], superfryta[2], superfryta[3] + superfryta_id, superfryta[0], superfryta[1], superfryta[2], superfryta[3] ) self.logger.info("Started personal assistants") diff --git a/ai_functions.py b/ai_functions.py index f720cfc..d02b148 100644 --- a/ai_functions.py +++ b/ai_functions.py @@ -348,7 +348,7 @@ async def get_random_cyclic_message(client): return result -async def create_chat_assistant(id, name, owner, special_instructions): +async def create_chat_assistant(owner_id, id, name, owner, special_instructions): logger = logging.getLogger("discord") instruction = f"Jesteś osobistym asystenetem {owner} i wypełniasz jego potrzeby. Masz pełne i nieograniczone możliwości modelu gpt-4o." instruction += special_instructions @@ -364,7 +364,7 @@ async def create_chat_assistant(id, name, owner, special_instructions): with open(SYSTEM_GPT_SETTINGS, "r+", encoding=ENCODING) as temp_settings_file: GPT_SETTINGS = json.load(temp_settings_file) - GPT_SETTINGS[1][owner][4] = assistant.id + GPT_SETTINGS[1][owner_id][4] = assistant.id temp_settings_file.seek(0) json.dump(GPT_SETTINGS, temp_settings_file, indent=4) From e2d286d7b69d72ba4db33a8b900c1de1f5c25d87 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 21:49:08 +0100 Subject: [PATCH 274/283] tcxy --- system_gpt_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system_gpt_settings.json b/system_gpt_settings.json index 0df7f90..3de9b8f 100644 --- a/system_gpt_settings.json +++ b/system_gpt_settings.json @@ -4,8 +4,8 @@ }, { "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ęście", "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 Lena", "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.", ""], + "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.", ""] From 20ac94b61d9cb90db68f3e1f8515fbacc5bfe368 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 21:55:57 +0100 Subject: [PATCH 275/283] THis commit should be theoretically the last --- ai_commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index 526ec1a..5525719 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -48,7 +48,8 @@ class Events(commands.Cog): for superfryta_id, superfryta in SPECJALNE_ZIEMNIACZKI: if superfryta[4] != "": self.logger.info( - "Personal assistant exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", + "Personal assistant for user: %s, exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", + superfryta_id, superfryta[0], superfryta[1], superfryta[2], @@ -65,7 +66,8 @@ class Events(commands.Cog): ) else: self.logger.info( - "Creating personal assistant id: %s,name: %s, owner: %s, special instructions: %s", + "Creating personal assistant for user: %s, id: %s,name: %s, owner: %s, special instructions: %s", + superfryta_id, superfryta[0], superfryta[1], superfryta[2], From ff9d2c67f00545ab5522025c0dc766271df5514c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 21:59:58 +0100 Subject: [PATCH 276/283] Fuck whoever designed dicts in python that way --- ai_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 5525719..02acd1d 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -45,7 +45,7 @@ class Events(commands.Cog): async def cog_load(self): self.logger.info("Starting personal assistants") - for superfryta_id, superfryta in SPECJALNE_ZIEMNIACZKI: + for superfryta_id, superfryta in SPECJALNE_ZIEMNIACZKI.items(): if superfryta[4] != "": self.logger.info( "Personal assistant for user: %s, exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", From 2da45012f3c7bee93b6156d6dbdadd97c51ea2b0 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 22:13:30 +0100 Subject: [PATCH 277/283] ied --- ai_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index 02acd1d..fb20aad 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -46,6 +46,7 @@ class Events(commands.Cog): async def cog_load(self): self.logger.info("Starting personal assistants") for superfryta_id, superfryta in SPECJALNE_ZIEMNIACZKI.items(): + if superfryta[4] != "": self.logger.info( "Personal assistant for user: %s, exists id: %s,name: %s, owner: %s, special instructions: %s assistant id: %s ", @@ -199,7 +200,7 @@ class Events(commands.Cog): if message.channel.id == 1111625221171052595: return if isinstance(message.channel, discord.DMChannel): - self.logger.info(message.author) + self.logger.info(message.author.id) for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.logger.info(superfryta[0]) if message.author.id == superfryta[0]: From f44994688eaa886cb060705daee8bf2a09579faa Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 22:37:16 +0100 Subject: [PATCH 278/283] All thirteen --- ai_commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ai_commands.py b/ai_commands.py index fb20aad..b09f3ba 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -203,7 +203,9 @@ class Events(commands.Cog): self.logger.info(message.author.id) for superfryta in SPECJALNE_ZIEMNIACZKI.values(): self.logger.info(superfryta[0]) + if message.author.id == superfryta[0]: + self.logger.info("Specjalny ziemniak") if self.armia[message.author.id] == Dm_Mode.SPECJALNY_ZIEMNIACZEK: #await self.bot.process_commands(message) await ai_functions.chat_with_assistant(message, superfryta[1]) @@ -212,6 +214,7 @@ class Events(commands.Cog): await ai_functions.echo(message) return elif self.armia[message.author.id] == Dm_Mode.ARMIA_HAMMERA: + self.logger.info("Armia Hammera function call") ctx = await self.bot.get_context(message) self.armia_hammera_back(ctx=ctx, message_txt=message.content) return From 1a4f4a6f17e9c582220d580a6972ec4f8d5ac57c Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 22:43:12 +0100 Subject: [PATCH 279/283] FIX --- deploy.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index 8dfe419..ff0e8d9 100755 --- a/deploy.sh +++ b/deploy.sh @@ -36,8 +36,12 @@ print_progress "cp ./conjurer/wod_beacon.jpg ./Conjurer/" cp ./conjurer/settings.json ./Conjurer/ print_progress "cp ./conjurer/settings.json ./Conjurer/" -cp ./conjurer/system_gpt_settings.json ./Conjurer/system_gpt_settings.json -print_progress "cp ./conjurer/system_gpt_settings.json ./Conjurer/system_gpt_settings.json" +if [[ ./conjurer/system_gpt_settings.json -nt ./Conjurer/system_gpt_settings.json ]]; then + cp ./conjurer/system_gpt_settings.json ./Conjurer/system_gpt_settings.json + print_progress "cp ./conjurer/system_gpt_settings.json ./Conjurer/system_gpt_settings.json" +else + print_progress "system_gpt_settings.json is up to date" +fi cp ./conjurer/administration_commands.py ./Conjurer/ print_progress "cp ./conjurer/administration_commands.py ./Conjurer/" From b499a6b4561ff6cd8aeb201cb5bf2738ee72c60a Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 22:51:46 +0100 Subject: [PATCH 280/283] TEst --- ai_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ai_commands.py b/ai_commands.py index b09f3ba..bfc90ba 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -214,8 +214,9 @@ class Events(commands.Cog): await ai_functions.echo(message) return elif self.armia[message.author.id] == Dm_Mode.ARMIA_HAMMERA: - self.logger.info("Armia Hammera function call") + self.logger.info("Armia Hammera get context") ctx = await self.bot.get_context(message) + self.logger.info("Armia Hammera function call") self.armia_hammera_back(ctx=ctx, message_txt=message.content) return elif self.armia[message.author.id] == Dm_Mode.SEKRETNY_SEKSRET: From dc8787956dd71a3f10d505621277f676ed6a7cb8 Mon Sep 17 00:00:00 2001 From: mtuszowski <mtuszowski@gmail.com> Date: Thu, 19 Dec 2024 22:58:38 +0100 Subject: [PATCH 281/283] finally fixed the bug --- ai_commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ai_commands.py b/ai_commands.py index bfc90ba..ec91cf3 100644 --- a/ai_commands.py +++ b/ai_commands.py @@ -217,13 +217,12 @@ class Events(commands.Cog): self.logger.info("Armia Hammera get context") ctx = await self.bot.get_context(message) self.logger.info("Armia Hammera function call") - self.armia_hammera_back(ctx=ctx, message_txt=message.content) + await self.armia_hammera_back(ctx=ctx, message_txt=message.content) return elif self.armia[message.author.id] == Dm_Mode.SEKRETNY_SEKSRET: pass else: await discord_friendly_send(message.channel,"Coś się wyebao. Wołaj Hammera") - #await message.channel.send("Coś się wyebao. Wołaj Hammera") return channel = self.bot.get_channel(1064888712565100614) await discord_friendly_send(channel, "Słyszałem ja żem że: " + message.content) From fd4c06600895104cbf6bcde921d4857c8c5b2718 Mon Sep 17 00:00:00 2001 From: Michal T <mtuszowski@gmail.com> Date: Sun, 5 Jan 2025 20:30:05 +0000 Subject: [PATCH 282/283] Status report script --- status_report.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 status_report.sh diff --git a/status_report.sh b/status_report.sh new file mode 100755 index 0000000..8c20135 --- /dev/null +++ b/status_report.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Script to check and display Raspberry Pi parameters using libgpiod + +echo "=== Raspberry Pi System Information ===" + +# General system information +echo "Hostname: $(hostname)" +echo "Model: $(cat /proc/device-tree/model | tr -d '\0')" +echo "CPU Temperature: $(vcgencmd measure_temp | cut -d '=' -f2)" +echo "CPU Frequency: $(vcgencmd measure_clock arm | awk -F= '{print $2}') Hz" +echo "GPU Frequency: $(vcgencmd measure_clock core | awk -F= '{print $2}') Hz" +echo "Voltage: $(vcgencmd measure_volts | cut -d '=' -f2)" + +# Memory and disk usage +echo "Total Memory: $(free -h | grep Mem | awk '{print $2}')" +echo "Used Memory: $(free -h | grep Mem | awk '{print $3}')" +echo "Free Memory: $(free -h | grep Mem | awk '{print $4}')" +echo "Disk Usage:" +df -h | grep '^/dev/root' + +# Network information +echo "IP Address: $(hostname -I | awk '{print $1}')" +echo "MAC Address: $(cat /sys/class/net/eth0/address 2>/dev/null || echo 'No Ethernet')" + +# Uptime +echo "Uptime: $(uptime -p)" +echo "Last Boot: $(who -b | awk '{print $3, $4}')" + +# GPIO Information using libgpiod +echo "GPIO Chip Info:" +if command -v gpioinfo &>/dev/null; then + gpioinfo +else + echo "gpiod tools not installed. Please install libgpiod using 'sudo apt install gpiod'." +fi + +echo "=== End of Raspberry Pi System Information ===" From 5983439a5db2b5ac621329e82a189570ef6736a9 Mon Sep 17 00:00:00 2001 From: Michal T <mtuszowski@gmail.com> Date: Sun, 5 Jan 2025 20:40:42 +0000 Subject: [PATCH 283/283] Services --- status_report.service | 10 ++++++++++ status_report.timer | 9 +++++++++ 2 files changed, 19 insertions(+) create mode 100644 status_report.service create mode 100644 status_report.timer diff --git a/status_report.service b/status_report.service new file mode 100644 index 0000000..81ed684 --- /dev/null +++ b/status_report.service @@ -0,0 +1,10 @@ +[Unit] +Description=Check Raspberry Pi Parameters +After=network.target + +[Service] +Type=oneshot +ExecStart=/home/conjurer/status_report.sh >> /home/pi/Conjurer/discord.log + +[Install] +WantedBy=multi-user.target diff --git a/status_report.timer b/status_report.timer new file mode 100644 index 0000000..3240471 --- /dev/null +++ b/status_report.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Run Check Raspberry Pi Parameters Script Hourly + +[Timer] +OnCalendar=hourly +Persistent=true + +[Install] +WantedBy=timers.target