aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-02-04 16:19:22 +0100
committersalmonik <raisersoft@proton.me>2026-02-04 16:19:22 +0100
commit72abc43db8f732743c65d6f6a820369ab8665766 (patch)
tree094fafdb0baf2e06a02045b1b2c0be88931a379a
parentrefactor music (diff)
downloadsenator-sperma-72abc43db8f732743c65d6f6a820369ab8665766.tar.gz
senator-sperma-72abc43db8f732743c65d6f6a820369ab8665766.zip
more asynchronous work and other minor improvements
-rw-r--r--cogs/ai.py4
-rw-r--r--cogs/compression.py76
-rw-r--r--cogs/conversion.py181
-rw-r--r--cogs/downloader.py253
-rw-r--r--cogs/jarvis.py218
-rw-r--r--cogs/math.py7
-rw-r--r--cogs/tts.py138
-rw-r--r--cogs/vclogs.py29
-rw-r--r--cogs/voice.py78
-rw-r--r--cogs/web.py89
-rw-r--r--main.py15
-rw-r--r--requirements.txt2
12 files changed, 623 insertions, 467 deletions
diff --git a/cogs/ai.py b/cogs/ai.py
index f975bbb..14e077d 100644
--- a/cogs/ai.py
+++ b/cogs/ai.py
@@ -9,7 +9,7 @@ from discord.ext import commands
9import logging 9import logging
10import requests 10import requests
11from gtts import gTTS 11from gtts import gTTS
12from cogs.music.play_utils import PlayUtils 12from cogs.music.play_commands import PlayCommands
13import io 13import io
14 14
15logger = logging.getLogger(__name__) 15logger = logging.getLogger(__name__)
@@ -65,7 +65,7 @@ class AI(commands.Cog):
65 tts_buffer = io.BytesIO() 65 tts_buffer = io.BytesIO()
66 tts_obj.write_to_fp(tts_buffer) 66 tts_obj.write_to_fp(tts_buffer)
67 tts_buffer.seek(0) 67 tts_buffer.seek(0)
68 await PlayUtils.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True) 68 await PlayCommands.play(ctx, discord.FFmpegPCMAudio(tts_buffer), delete_after_play=True)
69 69
70 else: 70 else:
71 await ctx.send_followup(response_text) 71 await ctx.send_followup(response_text)
diff --git a/cogs/compression.py b/cogs/compression.py
index d235c6d..1fcefb5 100644
--- a/cogs/compression.py
+++ b/cogs/compression.py
@@ -5,7 +5,7 @@
5 5
6import mimetypes 6import mimetypes
7import os 7import os
8import subprocess 8import asyncio
9from typing import List, Optional 9from typing import List, Optional
10from os import PathLike 10from os import PathLike
11import discord 11import discord
@@ -32,6 +32,38 @@ class Compression(commands.Cog):
32 if not os.path.exists(self.temp_dir): 32 if not os.path.exists(self.temp_dir):
33 os.makedirs(self.temp_dir) 33 os.makedirs(self.temp_dir)
34 34
35 def _process_video(self, file_path: str, output_path: str) -> str:
36 """Process video compression"""
37 command = f"ffmpeg -y -i {file_path} -c:v libx264 -crf 51 -b:v 100 -vf 'fps=10,scale=trunc(iw*0.5/2)*2:trunc(ih*0.5/2)*2' -preset ultrafast -c:a libopus -b:a 500 {output_path}"
38 return command
39
40 def _process_audio(self, file_path: str, output_path: str) -> str:
41 """Process audio compression"""
42 if output_path.lower().endswith('.mp3'):
43 output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '')
44 if not output_path.lower().endswith('.ogg'):
45 output_path = os.path.splitext(output_path)[0] + '.ogg'
46 command = f"ffmpeg -y -i {file_path} -c:a libopus -b:a 500 -ar 12000 -ac 1 {output_path}"
47 return command
48
49 def _process_image(self, file_path: str, output_path: str) -> tuple[str, str]:
50 """Process image compression"""
51 ext = os.path.splitext(file_path)[1].lower()
52 if ext in [".jpg", ".jpeg"]:
53 command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}"
54 elif ext == ".gif":
55 command = f"magick {file_path} -coalesce -resize 30% -colors 32 -strip -layers Optimize {output_path}"
56 else:
57 output_path = os.path.splitext(output_path)[0] + ".jpg"
58 command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}"
59 return command, output_path
60
61 async def _execute_compression(self, command: str) -> bool:
62 """Execute compression command"""
63 proc = await asyncio.create_subprocess_exec(command)
64 await proc.wait()
65 return proc.returncode == 0
66
35 async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]: 67 async def _compress_attachment(self, attachment: discord.Attachment) -> Optional[discord.File]:
36 """Compress a single attachment and return the compressed file""" 68 """Compress a single attachment and return the compressed file"""
37 file_path = PathLike[str] 69 file_path = PathLike[str]
@@ -46,32 +78,23 @@ class Compression(commands.Cog):
46 file_type = filetype(file_path) 78 file_type = filetype(file_path)
47 logger.info(f"Compression started for: {file_type}") 79 logger.info(f"Compression started for: {file_type}")
48 80
49 if file_type == "Video": 81 command = self._get_compression_command(file_type, file_path, output_path)
50 command = f"ffmpeg -y -i {file_path} -c:v libx264 -crf 51 -b:v 100 -vf 'fps=10,scale=trunc(iw*0.5/2)*2:trunc(ih*0.5/2)*2' -preset ultrafast -c:a libopus -b:a 500 {output_path}" 82 if command is None:
51 elif file_type == "Audio":
52 if output_path.lower().endswith('.mp3'):
53 output_path = os.path.splitext(output_path)[0] + os.path.splitext(output_path)[1].replace('.mp3', '')
54 if not output_path.lower().endswith('.ogg'):
55 output_path = os.path.splitext(output_path)[0] + '.ogg'
56 command = f"ffmpeg -y -i {file_path} -c:a libopus -b:a 500 -ar 12000 -ac 1 {output_path}"
57 elif file_type == "Image":
58 ext = os.path.splitext(file_path)[1].lower()
59 if ext in [".jpg", ".jpeg"]:
60 command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}"
61 elif ext == ".gif":
62 command = f"magick {file_path} -coalesce -resize 30% -colors 32 -strip -layers Optimize {output_path}"
63 else:
64 output_path = os.path.splitext(output_path)[0] + ".jpg"
65 command = f"magick {file_path} -strip -resize 30% -quality 5 {output_path}"
66 else:
67 logger.info(f"Unsupported file type: {file_type}") 83 logger.info(f"Unsupported file type: {file_type}")
68 os.remove(file_path) 84 os.remove(file_path)
69 return None 85 return None
70 86
87 # Update output_path if it was modified
88 if file_type == "Image":
89 command, output_path = command
90 elif file_type == "Audio":
91 # output_path is already modified in _process_audio
92 pass
93
71 # Run the compression 94 # Run the compression
72 subprocess.run(command, check=True, shell=True) 95 success = await self._execute_compression(command)
73 96
74 if os.path.exists(output_path): 97 if success and os.path.exists(output_path):
75 logger.info(f"Successfully compressed: {attachment.filename}") 98 logger.info(f"Successfully compressed: {attachment.filename}")
76 return discord.File(output_path, filename=f"compressed_{attachment.filename}") 99 return discord.File(output_path, filename=f"compressed_{attachment.filename}")
77 return None 100 return None
@@ -84,6 +107,17 @@ class Compression(commands.Cog):
84 # Clean up temporary files 107 # Clean up temporary files
85 if 'file_path' in locals() and os.path.exists(file_path): 108 if 'file_path' in locals() and os.path.exists(file_path):
86 os.remove(file_path) 109 os.remove(file_path)
110
111 def _get_compression_command(self, file_type: str, file_path: str, output_path: str):
112 """Get the appropriate compression command based on file type"""
113 if file_type == "Video":
114 return self._process_video(file_path, output_path)
115 elif file_type == "Audio":
116 return self._process_audio(file_path, output_path)
117 elif file_type == "Image":
118 return self._process_image(file_path, output_path)
119 else:
120 return None
87 121
88 async def _compress_attachments(self, attachments: List[discord.Attachment]) -> List[discord.File]: 122 async def _compress_attachments(self, attachments: List[discord.Attachment]) -> List[discord.File]:
89 """Compress multiple attachments and return list of compressed files""" 123 """Compress multiple attachments and return list of compressed files"""
diff --git a/cogs/conversion.py b/cogs/conversion.py
index 11f4f1b..f61d30f 100644
--- a/cogs/conversion.py
+++ b/cogs/conversion.py
@@ -8,8 +8,10 @@ import requests
8from discord.ext import commands 8from discord.ext import commands
9import logging 9import logging
10import os 10import os
11import subprocess 11import asyncio
12import aiofiles
12import mimetypes 13import mimetypes
14import subprocess
13 15
14logger = logging.getLogger(__name__) 16logger = logging.getLogger(__name__)
15 17
@@ -28,108 +30,119 @@ class Conversion(commands.Cog):
28 def __init__(self, bot): 30 def __init__(self, bot):
29 self.bot = bot 31 self.bot = bot
30 32
31 @commands.slash_command(name="konwersja", description="konwertuj plik na inny format") 33 async def _setup_file_paths(self, plik, output_format):
32 @discord.option( 34 """Setup input and output file paths"""
33 "plik",
34 description="plik do konwersji",
35 required=True,
36 type=discord.Attachment
37 )
38 @discord.option(
39 "format",
40 description="format konwersji",
41 required=True,
42 type=str
43 )
44 async def convert(self, ctx, plik, output_format, args):
45 await ctx.defer()
46 # Save files to the temp directory inside the bot's root directory
47 bot_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 35 bot_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
48 temp_dir = os.path.join(bot_dir, "temp") 36 temp_dir = os.path.join(bot_dir, "temp")
49 if not os.path.exists(temp_dir): 37 if not os.path.exists(temp_dir):
50 os.makedirs(temp_dir) 38 os.makedirs(temp_dir)
39
51 file_path = os.path.join(temp_dir, plik.filename) 40 file_path = os.path.join(temp_dir, plik.filename)
52 filename_noext = os.path.splitext(plik.filename)[0] 41 filename_noext = os.path.splitext(plik.filename)[0]
53 await plik.save(file_path) 42 await plik.save(file_path)
54 43
55 input_path = file_path 44 input_path = file_path
56 output_path = os.path.join(temp_dir, f"{filename_noext}k.{output_format}") 45 output_path = os.path.join(temp_dir, f"{filename_noext}k.{output_format}")
57 file_type = filetype(file_path) 46 return input_path, output_path
58 logger.info(f"Conversion started for: {file_type}") 47
59 if file_type == "Video" or file_type == "Audio": 48 def _build_conversion_command(self, file_type, input_path, output_path, args):
60 # Execute ffmpeg command to compress video 49 """Build the appropriate conversion command based on file type"""
61 ffmpeg_command = f"ffmpeg -i {input_path} {args} {output_path}" 50 if file_type in ["Video", "Audio"]:
51 return f"ffmpeg -i {input_path} {args} {output_path}"
62 elif file_type == "Image": 52 elif file_type == "Image":
63 img_command = f"magick {input_path} {args} {output_path}" 53 return f"magick {input_path} {args} {output_path}"
64 ffmpeg_command = img_command
65 else: 54 else:
66 await ctx.respond("Plik jest nieobsługiwany", ephemeral=True) 55 return None
67 logger.info("Unsupported file type")
68 return
69 56
57 async def _upload_to_copyparty(self, output_path):
58 """Upload file to copyparty and return download URL"""
70 try: 59 try:
71 subprocess.run(ffmpeg_command, check=True, shell=True) 60 async with aiofiles.open(output_path, 'rb') as f:
72 # Upload the compressed file 61 files = {
73 if os.path.getsize(output_path) > 1024*1024*10: 62 'f': (output_path, f, 'application/octet-stream')
74 try: 63 }
64 headers = {
65 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'
66 }
67 params = {
68 'j': '1',
69 'ck': 'b2'
70 }
71
72 base_url = "https://catbox.2137697.xyz/sperma/"
73 response = requests.post(base_url, files=files, headers=headers, params=params)
74
75 if response.status_code in [200, 201]:
76 result = response.json()
77 if 'fileurl' in result:
78 return result['fileurl']
79 else:
80 raise ValueError(f"Server response missing fileurl: {result}")
81 else:
82 error_msg = response.text if response.text else 'Unknown error'
83 raise requests.RequestException(f"HTTP {response.status_code}: {error_msg}")
84
85 except Exception as e:
86 logger.error(f"copyparty upload error: {e}", exc_info=True)
87 raise
75 88
76 # Upload to Copyparty 89 async def _handle_file_output(self, ctx, output_path):
77 with open(output_path, 'rb') as f: 90 """Handle file output based on size"""
78 files = { 91 if os.path.getsize(output_path) > 1024*1024*10:
79 'f': (output_path, f, 'application/octet-stream') 92 try:
80 } 93 dl_url = await self._upload_to_copyparty(output_path)
81 headers = { 94 await ctx.followup.send(f"wysyłon\n{dl_url}\n")
82 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' 95 except Exception as upload_error:
83 } 96 await ctx.followup.send(f"copyparty mnie nienawidzi :cccc\n {str(upload_error)}")
84 params = { 97 else:
85 'j': '1', # Return JSON response 98 await ctx.respond(file=discord.File(output_path))
86 'ck': 'b2' # Use blake2b checksum
87 }
88
89 # Ensure the URL ends with a single slash
90 base_url = "https://catbox.2137697.xyz/sperma/"
91
92 response = requests.post(
93 base_url,
94 files=files,
95 headers=headers,
96 params=params
97 )
98 99
99 if response.status_code == 200 or response.status_code == 201: 100 def _cleanup_files(self, *file_paths):
100 try: 101 """Clean up temporary files"""
101 result = response.json() 102 for path in file_paths:
102 if 'fileurl' in result: 103 if os.path.exists(path):
103 dl_url = result['fileurl'] 104 os.remove(path)
104 await ctx.followup.send(
105 f"wysyłon\n"
106 f"{dl_url}\n"
107 )
108 else:
109 await ctx.followup.send(
110 f"Odpowiedź serwera nie zawiera wymaganych danych: {result}")
111 except ValueError:
112 await ctx.followup.send(f"Nieprawidłowa odpowiedź z serwera: {response.text}")
113 else:
114 error_msg = response.text if response.text else 'Unknown error'
115 await ctx.followup.send(
116 f"nie udało się wysłać pliku (HTTP {response.status_code}): {error_msg}")
117 105
118 except Exception as upload_error: 106 @commands.slash_command(name="konwersja", description="konwertuj plik na inny format")
119 await ctx.followup.send(f"copyparty mnie nienawidzi :cccc\n {str(upload_error)}") 107 @discord.option(
120 logger.error(f"copyparty upload error: {upload_error}", exc_info=True) 108 "plik",
121 else: 109 description="plik do konwersji",
122 await ctx.respond(file=discord.File(output_path)) 110 required=True,
111 type=discord.Attachment
112 )
113 @discord.option(
114 "format",
115 description="format konwersji",
116 required=True,
117 type=str
118 )
119 async def convert(self, ctx, plik, output_format, args):
120 await ctx.defer()
121
122 try:
123 input_path, output_path = await self._setup_file_paths(plik, output_format)
124 file_type = filetype(input_path)
125 logger.info(f"Conversion started for: {file_type}")
126
127 command = self._build_conversion_command(file_type, input_path, output_path, args)
128 if not command:
129 await ctx.respond("Plik jest nieobsługiwany", ephemeral=True)
130 logger.info("Unsupported file type")
131 return
132
133 proc = await asyncio.create_subprocess_exec(*command.split())
134 await proc.wait()
135
136 await self._handle_file_output(ctx, output_path)
123 logger.info(f"Conversion completed for: {file_type}") 137 logger.info(f"Conversion completed for: {file_type}")
124 # Delete compressed file 138
125 os.remove(input_path)
126 os.remove(output_path)
127 except subprocess.CalledProcessError as e: 139 except subprocess.CalledProcessError as e:
128 await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True) 140 await ctx.respond(f"Conversion failed: {str(e)}", ephemeral=True)
129 if os.path.exists(input_path): 141 except Exception as e:
130 os.remove(input_path) 142 await ctx.respond(f"An error occurred: {str(e)}", ephemeral=True)
131 if os.path.exists(output_path): 143 finally:
132 os.remove(output_path) 144 if 'input_path' in locals():
145 self._cleanup_files(input_path, output_path)
133 146
134def setup(bot): 147def setup(bot):
135 bot.add_cog(Conversion(bot)) \ No newline at end of file 148 bot.add_cog(Conversion(bot)) \ No newline at end of file
diff --git a/cogs/downloader.py b/cogs/downloader.py
index 0331ae0..d2835d8 100644
--- a/cogs/downloader.py
+++ b/cogs/downloader.py
@@ -12,6 +12,7 @@ import re
12import yt_dlp 12import yt_dlp
13from discord.ext import commands 13from discord.ext import commands
14from dotenv import load_dotenv 14from dotenv import load_dotenv
15import aiofiles
15 16
16logger = logging.getLogger(__name__) 17logger = logging.getLogger(__name__)
17 18
@@ -68,152 +69,144 @@ class Downloader(commands.Cog):
68 if 'details' in response: 69 if 'details' in response:
69 logger.error(f"Details: {response['details']}") 70 logger.error(f"Details: {response['details']}")
70 71
71
72 @staticmethod 72 @staticmethod
73 async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str = None): 73 async def download_standalone(url: str, catbox: bool = False, fformat: str = "mp4", filename_user: str = None):
74 try: 74 try:
75 logger.info(f"Downloading {url} as {fformat}") 75 logger.info(f"Downloading {url} as {fformat}")
76 76
77 with tempfile.TemporaryDirectory(delete=False) as temp_dir: 77 with tempfile.TemporaryDirectory(delete=False) as temp_dir:
78 # Configure yt-dlp to use the original filename 78 ydl_opts = Downloader._get_ydl_options(fformat, temp_dir)
79 ydl_opts = {
80 'outtmpl': os.path.join(temp_dir, '%(title)s.%(ext)s'),
81 'format': 'bestaudio/best' if fformat in ['mp3', 'wav',
82 'm4a'] else 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
83 'merge_output_format': 'mp4',
84 'postprocessors': [],
85 'remote-components': 'ejs:github',
86 'js-runtimes': {'deno': {'path': '/usr/bin/deno'}},
87 'o': '"%(title)s.%(ext)s"'
88 }
89
90 # Add post-processing for audio formats
91 if fformat in ['mp3', 'wav', 'm4a']:
92 ydl_opts['postprocessors'].append({
93 'key': 'FFmpegExtractAudio',
94 'preferredcodec': fformat,
95 'preferredquality': '192',
96 })
97 elif fformat != 'mp4': # For other video formats
98 ydl_opts['postprocessors'].append({
99 'key': 'FFmpegVideoConvertor',
100 'preferedformat': fformat,
101 })
102 79
103 with yt_dlp.YoutubeDL(ydl_opts) as ydl: 80 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
104 ydl.download([url]) 81 ydl.download([url])
105 82
106 # Find the downloaded file (should be the only file in the temp directory) 83 actual_file, filename = Downloader._find_and_prepare_file(temp_dir, filename_user)
107 files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))]
108 if not files:
109 raise Exception(f"No file was downloaded")
110
111 actual_file = os.path.join(temp_dir, files[0])
112 extension = actual_file.split('.')[-1]
113 84
114 if not filename_user: 85 if Downloader._should_upload_to_server(actual_file, catbox):
115 filename = os.path.basename(actual_file) 86 return await Downloader._upload_to_server(actual_file, filename, temp_dir)
116 else: 87 else:
117 filename = filename_user + '.' + extension 88 return Downloader._return_file_response(actual_file, filename, temp_dir)
118 actual_file = os.path.join(temp_dir, filename) 89
119 90 except Exception as e:
120 if not os.path.exists(actual_file): 91 return Downloader._create_error_response("Download failed", str(e), temp_dir if 'temp_dir' in locals() else None)
121 raise Exception(f"Downloaded file not found: {actual_file}") 92
122 93 @staticmethod
123 if os.path.getsize(actual_file) > 1024*1024*10 or catbox: 94 def _get_ydl_options(fformat: str, temp_dir: str) -> dict:
124 try: 95 ydl_opts = {
125 96 'outtmpl': os.path.join(temp_dir, '%(title)s.%(ext)s'),
126 # Upload to Copyparty 97 'format': 'bestaudio/best' if fformat in ['mp3', 'wav', 'm4a'] else 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
127 with open(actual_file, 'rb') as f: 98 'merge_output_format': 'mp4',
128 files = { 99 'postprocessors': [],
129 'f': (filename, f, 'application/octet-stream') 100 'remote-components': 'ejs:github',
130 } 101 'js-runtimes': {'deno': {'path': '/usr/bin/deno'}},
131 headers = { 102 'o': '"%(title)s.%(ext)s"'
132 'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}' 103 }
133 } 104
134 params = { 105 if fformat in ['mp3', 'wav', 'm4a']:
135 'j': '1', # Return JSON response 106 ydl_opts['postprocessors'].append({
136 'ck': 'b2' # Use blake2b checksum 107 'key': 'FFmpegExtractAudio',
137 } 108 'preferredcodec': fformat,
138 109 'preferredquality': '192',
139 # Ensure the URL ends with a single slash 110 })
140 base_url = "https://catbox.2137697.xyz/sperma/" 111 elif fformat != 'mp4':
141 112 ydl_opts['postprocessors'].append({
142 response = requests.post( 113 'key': 'FFmpegVideoConvertor',
143 base_url, 114 'preferedformat': fformat,
144 files=files, 115 })
145 headers=headers, 116
146 params=params 117 return ydl_opts
147 ) 118
148 119 @staticmethod
149 if response.status_code == 200 or response.status_code == 201: 120 def _find_and_prepare_file(temp_dir: str, filename_user: str) -> tuple:
150 try: 121 files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))]
151 result = response.json() 122 if not files:
152 if 'fileurl' in result: 123 raise FileNotFoundError("No file was downloaded")
153 dl_url = result['fileurl'] 124
154 return { 125 actual_file = os.path.join(temp_dir, files[0])
155 "status": "success", 126 extension = actual_file.split('.')[-1]
156 "type": "url", 127
157 "content": dl_url, 128 if not filename_user:
158 "filename": filename, 129 filename = os.path.basename(actual_file)
159 "temp_dir": temp_dir 130 else:
160 } 131 filename = filename_user + '.' + extension
161 else: 132 actual_file = os.path.join(temp_dir, filename)
162 return { 133
163 "status": "error", 134 if not os.path.exists(actual_file):
164 "type": "error", 135 raise FileNotFoundError(f"Downloaded file not found: {actual_file}")
165 "content": "Invalid response from file server", 136
166 "details": str(result), 137 return actual_file, filename
167 "temp_dir": temp_dir
168 }
169 except ValueError:
170 return {
171 "status": "error",
172 "type": "error",
173 "content": "Failed to parse server response",
174 "details": response.text[:200], # First 200 chars of response
175 "temp_dir": temp_dir
176 }
177 else:
178 error_msg = response.text if response.text else 'Unknown error'
179 return {
180 "status": "error",
181 "type": "error",
182 "content": f"Server returned {response.status_code}",
183 "details": error_msg,
184 "temp_dir": temp_dir
185 }
186
187 except Exception as upload_error:
188 return {
189 "status": "error",
190 "type": "error",
191 "content": "Upload failed",
192 "details": str(upload_error),
193 "temp_dir": temp_dir
194 }
195 138
139 @staticmethod
140 def _should_upload_to_server(actual_file: str, catbox: bool) -> bool:
141 return os.path.getsize(actual_file) > 1024*1024*10 or catbox
142
143 @staticmethod
144 async def _upload_to_server(actual_file: str, filename: str, temp_dir: str) -> dict:
145 try:
146 async with aiofiles.open(actual_file, 'rb') as f:
147 content = await f.read()
148 files = {'f': (filename, content, 'application/octet-stream')}
149 headers = {'Cookie': f'cppwd={os.getenv("COPYPARTY_PASSWORD")}'}
150 params = {'j': '1', 'ck': 'b2'}
151 base_url = "https://catbox.2137697.xyz/sperma/"
152
153 response = requests.post(base_url, files=files, headers=headers, params=params)
154
155 if response.status_code in [200, 201]:
156 return Downloader._handle_upload_success(response, filename, temp_dir)
196 else: 157 else:
197 # If we have a file to return directly 158 return Downloader._create_error_response(
198 return { 159 f"Server returned {response.status_code}",
199 "status": "success", 160 response.text or 'Unknown error',
200 "type": "file", 161 temp_dir
201 "content": actual_file, 162 )
202 "filename": filename, 163 except Exception as upload_error:
203 "temp_dir": temp_dir 164 return Downloader._create_error_response("Upload failed", str(upload_error), temp_dir)
204 }
205 165
206 except Exception as e: 166 @staticmethod
207 error_msg = f"Error in download command: {e}" 167 def _handle_upload_success(response: requests.Response, filename: str, temp_dir: str) -> dict:
208 logger.error(error_msg, exc_info=True) 168 try:
209 return { 169 result = response.json()
210 "status": "error", 170 if 'fileurl' in result:
211 "type": "error", 171 return {
212 "content": "Download failed", 172 "status": "success",
213 "details": str(e), 173 "type": "url",
214 "temp_dir": temp_dir 174 "content": result['fileurl'],
215 } 175 "filename": filename,
176 "temp_dir": temp_dir
177 }
178 else:
179 return Downloader._create_error_response(
180 "Invalid response from file server",
181 str(result),
182 temp_dir
183 )
184 except ValueError:
185 return Downloader._create_error_response(
186 "Failed to parse server response",
187 response.text[:200],
188 temp_dir
189 )
216 190
191 @staticmethod
192 def _return_file_response(actual_file: str, filename: str, temp_dir: str) -> dict:
193 return {
194 "status": "success",
195 "type": "file",
196 "content": actual_file,
197 "filename": filename,
198 "temp_dir": temp_dir
199 }
200
201 @staticmethod
202 def _create_error_response(content: str, details: str, temp_dir: str) -> dict:
203 return {
204 "status": "error",
205 "type": "error",
206 "content": content,
207 "details": details,
208 "temp_dir": temp_dir
209 }
217 210
218 211
219def setup(bot): 212def setup(bot):
diff --git a/cogs/jarvis.py b/cogs/jarvis.py
index 0096b38..b199c8a 100644
--- a/cogs/jarvis.py
+++ b/cogs/jarvis.py
@@ -29,126 +29,164 @@ class Jarvis(commands.Cog):
29 29
30 if message.mentions: 30 if message.mentions:
31 if message.mentions[0].id == self.bot.user.id: 31 if message.mentions[0].id == self.bot.user.id:
32 content = await self.get_original(message) 32 await self.jarvis(message)
33 await self.jarvis(message, content)
34 33
35 elif "jarvis" in message.content.lower(): 34 elif "jarvis" in message.content.lower():
36 content = await self.get_original(message) 35 await self.jarvis(message)
37 await self.jarvis(message, content)
38 36
39 @staticmethod 37 @staticmethod
40 async def jarvis(message, content): 38 async def jarvis(message):
41 url = "https://openrouter.ai/api/v1/chat/completions"
42 headers = { 39 headers = {
43 40 "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
44 "Authorization": f"Bearer {os.getenv("OPENROUTER_API_KEY")}",
45 "Content-Type": "application/json" 41 "Content-Type": "application/json"
46 } 42 }
47 43
48 # Build messages array 44 messages = Jarvis._build_messages(message)
49 messages = [ 45 payload = Jarvis._build_payload(messages)
50 { 46
51 "role": "system", 47 response_data = await Jarvis._make_api_request(payload, headers)
52 "content": "your task is to tell the program what to do and how to respond after receiving a message.\n" 48 response_message, action = Jarvis._parse_response(response_data)
53 "message field has to be filled out in polish.\n" 49
54 "act like jarvis, be a personal assistant to user, do not use emojis\n" 50 channel = message.channel
55 "do not ask the user if they want to do an action. the user will tell you that" 51 await channel.send(response_message)
56 } 52
57 ] 53 if action == "download":
54 await Jarvis._handle_download_action(message, channel)
55
56 @staticmethod
57 def _build_messages(message):
58 messages = [{
59 "role": "system",
60 "content": "your task is to tell the program what to do and how to respond after receiving a message.\n"
61 "message field has to be filled out in polish.\n"
62 "act like jarvis, be a personal assistant to user, do not use emojis\n"
63 "do not ask the user if they want to do an action. the user will tell you that"
64 }]
58 65
59 # Add original message with display name
60 messages.append({ 66 messages.append({
61 "role": "user", 67 "role": "user",
62 "content": f"[from {message.author.display_name}]: {message.content}" 68 "content": f"[from {message.author.display_name}]: {message.content}"
63 }) 69 })
64 70
65 # If it's a reply, add the original message and reply as separate messages 71 if Jarvis._is_reply(message):
66 if message.reference and (message.type == discord.MessageType.reply or message.reference.resolved): 72 messages.extend(Jarvis._build_reply_messages(message))
67 if message.reference.resolved: 73
68 original_msg = message.reference.resolved 74 return messages
69 messages.append({ 75
70 "role": "user", 76 @staticmethod
71 "content": f"[original message from {original_msg.author.display_name}]: {original_msg.content}" 77 def _is_reply(message):
72 }) 78 return message.reference and (message.type == discord.MessageType.reply or message.reference.resolved)
73 messages.append({ 79
74 "role": "user", 80 @staticmethod
75 "content": f"[reply from {message.author.display_name}]: {message.content}" 81 def _build_reply_messages(message):
76 }) 82 original_msg = message.reference.resolved
77 83 return [
78 payload = { 84 {
85 "role": "user",
86 "content": f"[original message from {original_msg.author.display_name}]: {original_msg.content}"
87 },
88 {
89 "role": "user",
90 "content": f"[reply from {message.author.display_name}]: {message.content}"
91 }
92 ]
93
94 @staticmethod
95 def _build_payload(messages):
96 return {
79 "model": "arcee-ai/trinity-large-preview:free", 97 "model": "arcee-ai/trinity-large-preview:free",
80 "messages": messages, 98 "messages": messages,
81 "response_format": { 99 "response_format": Jarvis._get_response_schema(),
82 "type": "json_schema", 100 "plugins": [{"id": "response-healing"}]
83 "json_schema": { 101 }
84 "name": "response", 102
85 "strict": True, 103 @staticmethod
86 "schema": { 104 def _get_response_schema():
87 "type": "object", 105 return {
88 "properties": { 106 "type": "json_schema",
89 "message": { 107 "json_schema": {
90 "type": "string", 108 "name": "response",
91 "description": "the message that user will receive", 109 "strict": True,
92 }, 110 "schema": {
93 "action": { 111 "type": "object",
94 "type": "string", 112 "properties": {
95 "description": "the action to do, either download, compress or none" 113 "message": {
96 } 114 "type": "string",
115 "description": "the message that user will receive",
97 }, 116 },
98 "required": ["message", "action"], 117 "action": {
99 "additionalProperties": False, 118 "type": "string",
119 "description": "the action to do, either download, compress or none"
100 } 120 }
101 } 121 },
102 }, 122 "required": ["message", "action"],
103 "plugins": [ 123 "additionalProperties": False,
104 {"id": "response-healing"} 124 }
105 ] 125 }
106 } 126 }
107 127
128 @staticmethod
129 async def _make_api_request(payload, headers):
130 url = "https://openrouter.ai/api/v1/chat/completions"
108 response = requests.post(url=url, headers=headers, json=payload) 131 response = requests.post(url=url, headers=headers, json=payload)
109 # Parse the JSON response
110 response_data = response.json() 132 response_data = response.json()
111 logger.info(response_data) 133 logger.info(response_data)
112 # Extract the content string 134 return response_data
113 content = response_data['choices'][0]['message']['content']
114 135
115 # Remove the code block markers and parse the inner JSON 136 @staticmethod
137 def _parse_response(response_data):
138 content = response_data['choices'][0]['message']['content']
116 inner_json_str = content.strip('```json\n').strip('```').strip() 139 inner_json_str = content.strip('```json\n').strip('```').strip()
117 inner_data = json.loads(inner_json_str) 140 inner_data = json.loads(inner_json_str)
118 141
119 # Now you can access the message and action 142 response_message = inner_data['message']
120 response_message = inner_data['message'] # "Proszę podać plik do kompresji lub link do pobrania." 143 action = inner_data['action']
121 action = inner_data['action'] # "none OR download OR compress" 144
122 channel = message.channel
123 logger.info(response_message) 145 logger.info(response_message)
124 logger.info(action) 146 logger.info(action)
147
148 return response_message, action
125 149
126 await channel.send(response_message) 150 @staticmethod
151 async def _handle_download_action(message, channel):
152 url_match = Jarvis._extract_url(message.content)
153 if not url_match:
154 await channel.send("Nie znaleziono poprawnego linku w odpowiedzi.")
155 return
127 156
128 if action == "download": 157 response = await Downloader.download_standalone(url=url_match)
129 # Extract the URL from the message using regex 158 await Jarvis._process_download_response(response, channel)
130 url_match = re.search(r'https://(?:[A-Za-z0-9-]+\.)*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,63}/\S*', message.content).group(0) 159
131 if not url_match: 160 @staticmethod
132 await channel.send("Nie znaleziono poprawnego linku w odpowiedzi.") 161 def _extract_url(content):
133 return 162 match = re.search(r'https://(?:[A-Za-z0-9-]+\.)*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,63}/\S*', content)
134 163 return match.group(0) if match else None
135 response = await Downloader.download_standalone(url=url_match) 164
136 if isinstance(response, dict): 165 @staticmethod
137 if response["status"] == "success": 166 async def _process_download_response(response, channel):
138 if response["type"] == "url": 167 if not isinstance(response, dict):
139 await channel.send(f"pobioron i wysyłon: {response['content']}") 168 return
140 elif response["type"] == "file": 169
141 file_data = response["content"] 170 if response["status"] == "success":
142 await channel.send("wysyłon:", file=discord.File(file_data)) 171 await Jarvis._handle_successful_download(response, channel)
143 shutil.rmtree(response["temp_dir"]) 172 else:
144 else: 173 await Jarvis._handle_failed_download(response)
145 logger.error(f"Error: {response['content']}") 174
146 if 'details' in response: 175 @staticmethod
147 logger.error(f"Details: {response['details']}") 176 async def _handle_successful_download(response, channel):
148 shutil.rmtree(response["temp_dir"]) 177 if response["type"] == "url":
149 178 await channel.send(f"pobioron i wysyłon: {response['content']}")
150 elif action == "compress": 179 elif response["type"] == "file":
151 pass 180 file_data = response["content"]
181 await channel.send("wysyłon:", file=discord.File(file_data))
182 shutil.rmtree(response["temp_dir"])
183
184 @staticmethod
185 async def _handle_failed_download(response):
186 logger.error(f"Error: {response['content']}")
187 if 'details' in response:
188 logger.error(f"Details: {response['details']}")
189 shutil.rmtree(response["temp_dir"])
152 190
153 @staticmethod 191 @staticmethod
154 async def get_original(message): 192 async def get_original(message):
diff --git a/cogs/math.py b/cogs/math.py
index 4bd62a0..bf164b3 100644
--- a/cogs/math.py
+++ b/cogs/math.py
@@ -4,7 +4,7 @@
4# https://codeberg.org/salmonik/senator-sperma 4# https://codeberg.org/salmonik/senator-sperma
5 5
6import logging 6import logging
7import subprocess 7import asyncio
8 8
9from discord import ApplicationContext 9from discord import ApplicationContext
10from discord.ext import commands 10from discord.ext import commands
@@ -22,8 +22,9 @@ class Math(commands.Cog):
22 ) 22 )
23 async def math(self, ctx: ApplicationContext, equation: str): 23 async def math(self, ctx: ApplicationContext, equation: str):
24 try: 24 try:
25 math = subprocess.check_output(f"qalc -e '{equation}'", shell=True) 25 math = await asyncio.create_subprocess_exec(f"qalc -e '{equation}'")
26 await ctx.respond(f"{equation} = {math.decode('utf-8').strip()}") 26 result = await math.wait()
27 await ctx.respond(f"{equation} = {result.decode('utf-8').strip()}")
27 logger.info(f"Math command executed: {equation}") 28 logger.info(f"Math command executed: {equation}")
28 except Exception as e: 29 except Exception as e:
29 await ctx.respond(e, ephemeral=True) 30 await ctx.respond(e, ephemeral=True)
diff --git a/cogs/tts.py b/cogs/tts.py
index 3c2f9ce..158006c 100644
--- a/cogs/tts.py
+++ b/cogs/tts.py
@@ -6,7 +6,7 @@
6import os 6import os
7import json 7import json
8import discord 8import discord
9import tempfile 9import aiofiles
10import logging 10import logging
11from discord.ext import commands 11from discord.ext import commands
12from gtts import gTTS 12from gtts import gTTS
@@ -18,80 +18,102 @@ class TTS(commands.Cog):
18 def __init__(self, bot): 18 def __init__(self, bot):
19 self.bot = bot 19 self.bot = bot
20 20
21 async def _load_settings(self):
22 """Load volume settings from settings.json"""
23 volume = 1.0
24 if os.path.exists('settings.json'):
25 try:
26 async with aiofiles.open('settings.json') as f:
27 data = json.load(f)
28 volume = data.get('volume', 1.0)
29 except (json.JSONDecodeError, KeyError) as e:
30 logger.error(f"Error loading settings: {e}")
31 return volume
32
33 async def _handle_file_output(self, ctx, temp_path, text):
34 """Handle TTS file output"""
35 try:
36 await ctx.respond(file=discord.File(temp_path, filename='tts.mp3'))
37 logger.info(f"Sent TTS for text: {text}")
38 except Exception as e:
39 logger.error(f"Error sending file: {e}")
40 await ctx.respond("eror :c", ephemeral=True)
41
42 async def _get_or_connect_voice_client(self, channel):
43 """Get existing voice client or connect to new channel"""
44 voice_client = discord.utils.get(self.bot.voice_clients, guild=channel.guild)
45
46 if not voice_client or not voice_client.is_connected():
47 voice_client = await channel.connect()
48 elif voice_client.channel != channel:
49 await voice_client.move_to(channel)
50
51 return voice_client
52
53 async def _handle_voice_output(self, ctx, temp_path, text, volume):
54 """Handle TTS voice channel output"""
55 await ctx.respond(content="\u200b", ephemeral=True)
56 await ctx.interaction.delete_original_response()
57
58 # Check if the user is in a voice channel
59 if not (ctx.author.voice and ctx.author.voice.channel):
60 logger.info("User is not in a voice channel")
61 return
62
63 channel = ctx.author.voice.channel
64 try:
65 voice_client = await self._get_or_connect_voice_client(channel)
66
67 # Play the TTS audio with volume adjustment
68 audio_source = discord.FFmpegPCMAudio(
69 temp_path,
70 options=f'-filter:a "volume={volume}"'
71 )
72 voice_client.play(audio_source)
73 logger.info(f"Playing TTS: {text}")
74
75 # Wait for the audio to finish playing
76 while voice_client.is_playing():
77 await asyncio.sleep(0.1)
78
79 except Exception as e:
80 logger.error(f"Error in voice channel handling: {e}")
81 await ctx.respond("eror :c", ephemeral=True)
82
83 async def _cleanup_temp_file(self, temp_path):
84 """Clean up temporary file"""
85 try:
86 if temp_path and os.path.exists(temp_path):
87 os.unlink(temp_path)
88 except Exception as e:
89 logger.error(f"Error cleaning up temp file {temp_path}: {e}")
90
21 @commands.slash_command(name="tts", description="Generuje TTS dla danego tekstu") 91 @commands.slash_command(name="tts", description="Generuje TTS dla danego tekstu")
22 async def tts(self, ctx, 92 async def tts(self, ctx,
23 text: str = discord.Option(str, "Tekst do przeczytania", required=True), 93 text: str = discord.Option(str, "Tekst do przeczytania", required=True),
24 output: str = discord.Option(str, "Wybierz sposób odtwarzania", choices=["file", "vc"], default="vc") 94 output: str = discord.Option(str, "Wybierz sposób odtwarzania", choices=["file", "vc"], default="vc")
25 ): 95 ):
26 await ctx.defer() 96 await ctx.defer()
27 # Create a temporary file for TTS output
28 with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
29 temp_path = temp_file.name
30 97
98 async with NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file:
99 temp_path = temp_file.name
100
31 try: 101 try:
32 # Generate and save TTS to temp file 102 # Generate and save TTS to temp file
33 tts = gTTS(text=text, lang='pl') 103 tts = gTTS(text=text, lang='pl')
34 tts.save(temp_path) 104 tts.save(temp_path)
35 105
36 # Load settings if available 106 # Load settings
37 volume = 1.0 # Default volume 107 volume = await self._load_settings()
38 if os.path.exists('settings.json'):
39 try:
40 with open('settings.json') as f:
41 data = json.load(f)
42 volume = data.get('volume', 1.0)
43 except (json.JSONDecodeError, KeyError) as e:
44 logger.error(f"Error loading settings: {e}")
45 108
109 # Handle output based on user choice
46 if output == "file": 110 if output == "file":
47 try: 111 await self._handle_file_output(ctx, temp_path, text)
48 await ctx.respond(file=discord.File(temp_path, filename='tts.mp3'))
49 logger.info(f"Sent TTS for text: {text}")
50 except Exception as e:
51 logger.error(f"Error sending file: {e}")
52 await ctx.respond("eror :c", ephemeral=True)
53
54 elif output == "vc": 112 elif output == "vc":
55 await ctx.respond(content="\u200b", ephemeral=True) 113 await self._handle_voice_output(ctx, temp_path, text, volume)
56 await ctx.interaction.delete_original_response()
57
58 # Check if the user is in a voice channel
59 if not (ctx.author.voice and ctx.author.voice.channel):
60 logger.info("User is not in a voice channel")
61 return
62
63 channel = ctx.author.voice.channel
64 try:
65 # Connect to the voice channel if not already connected
66 voice_client = discord.utils.get(self.bot.voice_clients, guild=channel.guild)
67 if not voice_client or not voice_client.is_connected():
68 voice_client = await channel.connect()
69 elif voice_client.channel != channel:
70 await voice_client.move_to(channel)
71
72 # Play the TTS audio with volume adjustment
73 audio_source = discord.FFmpegPCMAudio(
74 temp_path,
75 options=f'-filter:a "volume={volume}"'
76 )
77 voice_client.play(audio_source)
78 logger.info(f"Playing TTS: {text}")
79
80 # Wait for the audio to finish playing
81 while voice_client.is_playing():
82 await asyncio.sleep(0.1)
83
84 except Exception as e:
85 logger.error(f"Error in voice channel handling: {e}")
86 await ctx.respond("eror :c", ephemeral=True)
87 114
88 finally: 115 finally:
89 # Clean up the temporary file 116 await self._cleanup_temp_file(temp_path)
90 try:
91 if temp_path and os.path.exists(temp_path):
92 os.unlink(temp_path)
93 except Exception as e:
94 logger.error(f"Error cleaning up temp file {temp_path}: {e}")
95 117
96def setup(bot): 118def setup(bot):
97 bot.add_cog(TTS(bot)) \ No newline at end of file 119 bot.add_cog(TTS(bot)) \ No newline at end of file
diff --git a/cogs/vclogs.py b/cogs/vclogs.py
index 40a618c..bc317a4 100644
--- a/cogs/vclogs.py
+++ b/cogs/vclogs.py
@@ -10,6 +10,7 @@ import os
10import discord 10import discord
11from discord.ext import commands 11from discord.ext import commands
12from gtts import gTTS 12from gtts import gTTS
13import aiofiles
13 14
14logger = logging.getLogger(__name__) 15logger = logging.getLogger(__name__)
15 16
@@ -20,15 +21,14 @@ class VcLogs(commands.Cog):
20 self.bot = bot 21 self.bot = bot
21 self.settings_check() 22 self.settings_check()
22 23
24 settings_file = 'settings.json'
25
23 @staticmethod 26 @staticmethod
24 def settings_check(): 27 def settings_check():
25 if not os.path.exists('settings.json'): 28 if not os.path.exists(settings_file):
26 with open('settings.json', 'w') as f: 29 with open(settings_file, 'w') as f:
27 json.dump({'volume': '5'}, f, indent=4) 30 json.dump({'volume': '5'}, f, indent=4)
28 logger.info("settings.json created") 31 logger.info("settings.json created")
29 with open('settings.json', 'r') as f:
30 settings = json.load(f)
31 volume = settings.get('volume', '5')
32 32
33 @staticmethod 33 @staticmethod
34 async def handle_state_change(member, channel, state_type, is_enabled, current_time, log_channel): 34 async def handle_state_change(member, channel, state_type, is_enabled, current_time, log_channel):
@@ -62,12 +62,13 @@ class VcLogs(commands.Cog):
62 return tts_text, channel 62 return tts_text, channel
63 63
64 async def play_tts(self, channel, text): 64 async def play_tts(self, channel, text):
65 tts_file = "temp_tts.mp3"
65 # Generate TTS audio 66 # Generate TTS audio
66 tts = gTTS(text=text, lang='pl') 67 tts = gTTS(text=text, lang='pl')
67 tts.save('temp_tts.mp3') 68 tts.save(tts_file)
68 69
69 if os.path.exists('settings.json'): 70 if os.path.exists(settings_file):
70 with open('settings.json') as f: 71 async with aiofiles.open(settings_file) as f:
71 data = json.load(f) 72 data = json.load(f)
72 volume = data['volume'] 73 volume = data['volume']
73 else: 74 else:
@@ -82,23 +83,23 @@ class VcLogs(commands.Cog):
82 83
83 # Play the TTS audio 84 # Play the TTS audio
84 if not voice_client.is_playing(): 85 if not voice_client.is_playing():
85 audio_source = discord.FFmpegPCMAudio('temp_tts.mp3', options=f'-filter:a "volume={volume}"') 86 audio_source = discord.FFmpegPCMAudio(tts_file, options=f'-filter:a "volume={volume}"')
86 voice_client.play(audio_source) 87 voice_client.play(audio_source)
87 while voice_client.is_playing(): 88 while voice_client.is_playing():
88 await discord.utils.sleep_until(datetime.datetime.now() + datetime.timedelta(seconds=1)) 89 await discord.utils.sleep_until(datetime.datetime.now() + datetime.timedelta(seconds=1))
89 if os.path.exists('temp_tts.mp3'): 90 if os.path.exists(tts_file):
90 os.remove('temp_tts.mp3') 91 os.remove(tts_file)
91 92
92 @commands.slash_command( 93 @commands.slash_command(
93 name="log_volume" 94 name="log_volume"
94 ) 95 )
95 async def volume(self, ctx: discord.ApplicationContext, volume: int): 96 async def volume(self, ctx: discord.ApplicationContext, volume: int):
96 try: 97 try:
97 if not os.path.exists('settings.json'): 98 if not os.path.exists(settings_file):
98 with open('settings.json', 'w') as f: 99 async with aiofiles.open(settings_file, 'w') as f:
99 json.dump({'volume': '5'}, f, indent=4) 100 json.dump({'volume': '5'}, f, indent=4)
100 logger.info("settings.json created") 101 logger.info("settings.json created")
101 with open('settings.json', 'w') as f: 102 async with aiofiles.open(settings_file, 'w') as f:
102 json.dump({"volume": volume}, f, indent=4) 103 json.dump({"volume": volume}, f, indent=4)
103 await ctx.followup.send(f"Volume has been set to: {volume}") 104 await ctx.followup.send(f"Volume has been set to: {volume}")
104 logger.info(f"Volume changed to: {volume}") 105 logger.info(f"Volume changed to: {volume}")
diff --git a/cogs/voice.py b/cogs/voice.py
index 261af49..dd5b74a 100644
--- a/cogs/voice.py
+++ b/cogs/voice.py
@@ -14,43 +14,69 @@ class Voice(commands.Cog):
14 def __init__(self, bot): 14 def __init__(self, bot):
15 self.bot = bot 15 self.bot = bot
16 16
17 async def ensure_voice_client(self, ctx): 17 async def _validate_user_voice_state(self, ctx):
18 """Ensure we have a valid voice client connection.""" 18 """Validate that user is in a voice channel."""
19 if not ctx.author.voice or not ctx.author.voice.channel: 19 if not ctx.author.voice or not ctx.author.voice.channel:
20 await ctx.send("You need to be in a voice channel first!") 20 await ctx.send("You need to be in a voice channel first!")
21 return False
22 return True
23
24 async def _create_voice_client(self, ctx):
25 """Create a new voice client connection."""
26 try:
27 voice_client = await ctx.author.voice.channel.connect(timeout=30.0, reconnect=True)
28 logger.info(f"Connected to voice channel: {ctx.author.voice.channel}")
29 return voice_client
30 except asyncio.TimeoutError:
31 await ctx.send("Connection to voice channel timed out.")
32 return None
33 except discord.ClientException as e:
34 logger.error(f"Failed to connect to voice channel: {e}")
35 await ctx.send("Failed to connect to voice channel. Please try again.", ephemeral=True)
36 return None
37
38 async def _move_voice_client(self, voice_client, ctx):
39 """Move existing voice client to user's channel."""
40 try:
41 await voice_client.move_to(ctx.author.voice.channel)
42 logger.info(f"Moved to voice channel: {ctx.author.voice.channel}")
43 return voice_client
44 except asyncio.TimeoutError:
45 await ctx.send("Connection to voice channel timed out.")
46 return None
47 except discord.ClientException as e:
48 logger.error(f"Failed to move to voice channel: {e}")
49 await ctx.send("Failed to move to your voice channel. Please try again.", ephemeral=True)
50 return None
51
52 async def _verify_connection(self, voice_client):
53 """Verify and repair voice client connection if needed."""
54 if not voice_client.is_connected():
55 try:
56 await voice_client.connect(reconnect=True, timeout=10.0)
57 return voice_client
58 except Exception as e:
59 logger.error(f"Failed to reconnect to voice: {e}")
60 return None
61 return voice_client
62
63 async def ensure_voice_client(self, ctx):
64 """Ensure we have a valid voice client connection."""
65 if not await self._validate_user_voice_state(ctx):
21 return None 66 return None
22 67
23 voice_client = discord.utils.get(self.bot.voice_clients, guild=ctx.guild) 68 voice_client = discord.utils.get(self.bot.voice_clients, guild=ctx.guild)
24 69
25 # If no voice client exists, create one 70 # If no voice client exists, create one
26 if not voice_client or not voice_client.is_connected(): 71 if not voice_client or not voice_client.is_connected():
27 try: 72 return await self._create_voice_client(ctx)
28 voice_client = await ctx.author.voice.channel.connect(timeout=30.0, reconnect=True) 73
29 logger.info(f"Connected to voice channel: {ctx.author.voice.channel}")
30 except asyncio.TimeoutError:
31 await ctx.send("Connection to voice channel timed out.")
32 except discord.ClientException as e:
33 logger.error(f"Failed to connect to voice channel: {e}")
34 await ctx.send("Failed to connect to voice channel. Please try again.", ephemeral=True)
35 # If we're in a different channel, move to the user's channel 74 # If we're in a different channel, move to the user's channel
36 elif voice_client.channel != ctx.author.voice.channel: 75 if voice_client.channel != ctx.author.voice.channel:
37 try: 76 return await self._move_voice_client(voice_client, ctx)
38 await voice_client.move_to(ctx.author.voice.channel)
39 logger.info(f"Moved to voice channel: {ctx.author.voice.channel}")
40 except asyncio.TimeoutError:
41 await ctx.send("Connection to voice channel timed out.")
42 except discord.ClientException as e:
43 logger.error(f"Failed to move to voice channel: {e}")
44 await ctx.send("Failed to move to your voice channel. Please try again.", ephemeral=True)
45 77
46 # Verify the connection is healthy 78 # Verify the connection is healthy
47 if not voice_client.is_connected(): 79 return await self._verify_connection(voice_client)
48 try:
49 await voice_client.connect(reconnect=True, timeout=10.0)
50 except Exception as e:
51 logger.error(f"Failed to reconnect to voice: {e}")
52 await ctx.send("Failed to establish voice connection. Please try again.")
53 return None
54 80
55 async def connect(self, ctx): 81 async def connect(self, ctx):
56 """Connect to the user's voice channel with improved connection handling.""" 82 """Connect to the user's voice channel with improved connection handling."""
diff --git a/cogs/web.py b/cogs/web.py
index a9f8fbb..fcec8f9 100644
--- a/cogs/web.py
+++ b/cogs/web.py
@@ -18,6 +18,49 @@ class Web(commands.Cog):
18 def __init__(self, bot): 18 def __init__(self, bot):
19 self.bot = bot 19 self.bot = bot
20 20
21 def _extract_youtube_title(self, url):
22 """Extract YouTube video title if URL is a YouTube link."""
23 if not url.startswith('https://www.youtube.com/'):
24 return None
25
26 logger.info("youtube link")
27 try:
28 parsed_url = urllib.parse.urlparse(url)
29 video_id = urllib.parse.parse_qs(parsed_url.query).get('v', [None])[0]
30 if video_id:
31 yt = pytube.YouTube(f'https://www.youtube.com/watch?v={video_id}')
32 return yt.title
33 except Exception:
34 pass
35 return None
36
37 def _create_button(self, index, link):
38 """Create a button for sending search result."""
39 button = discord.ui.Button(label=f"Send Result {index}")
40
41 async def button_callback(interaction: discord.Interaction):
42 await interaction.response.send_message(link.get('href'))
43
44 button.callback = button_callback
45 return button
46
47 def _process_search_results(self, links, query):
48 """Process search results and create embed with buttons."""
49 embed = Embed(title="Search Results", description=f"Top results for: {query}", color=0x1E90FF)
50 view = discord.ui.View(disable_on_timeout=True)
51
52 for i, link in enumerate(links[:5], start=1):
53 title = link.text
54 youtube_title = self._extract_youtube_title(link.get('href'))
55 if youtube_title:
56 title = youtube_title
57
58 embed.add_field(name=f"Result {i}", value=f"[{title}]({link.get('href')})", inline=False)
59 button = self._create_button(i, link)
60 view.add_item(button)
61
62 return embed, view
63
21 @commands.slash_command( 64 @commands.slash_command(
22 name="web", 65 name="web",
23 description="Search the web using DuckDuckGo" 66 description="Search the web using DuckDuckGo"
@@ -35,40 +78,22 @@ class Web(commands.Cog):
35 78
36 logger.info(f"Query: {query}") 79 logger.info(f"Query: {query}")
37 80
38 if response.status_code == 200: 81 if response.status_code != 200:
39 soup = BeautifulSoup(response.text, 'html.parser')
40 links = soup.find_all('a', class_='result__a')
41 if links:
42 embed = Embed(title="Search Results", description=f"Top results for: {query}", color=0x1E90FF)
43 view = discord.ui.View(disable_on_timeout=True)
44 for i, link in enumerate(links[:5], start=1):
45 title = link.text
46 if link.get('href').startswith('https://www.youtube.com/'):
47 logger.info("youtube link")
48 try:
49 parsed_url = urllib.parse.urlparse(link.get('href'))
50 video_id = urllib.parse.parse_qs(parsed_url.query).get('v', [None])[0]
51 if video_id:
52 yt = pytube.YouTube(f'https://www.youtube.com/watch?v={video_id}')
53 title = yt.title
54 except Exception:
55 pass
56
57 embed.add_field(name=f"Result {i}", value=f"[{title}]({link.get('href')})", inline=False)
58 button = discord.ui.Button(label=f"Send Result {i}")
59
60 async def button_callback(interaction: discord.Interaction, link=link):
61 await interaction.response.send_message(link.get('href'))
62 button.callback = button_callback
63 view.add_item(button)
64 await ctx.respond(embed=embed, view=view, ephemeral=True)
65 logger.info("Search results sent")
66 else:
67 await ctx.respond(content="No results found.", ephemeral=True)
68 logger.warning("No results found")
69 else:
70 await ctx.respond(content=f"Error: {response.status_code}", ephemeral=True) 82 await ctx.respond(content=f"Error: {response.status_code}", ephemeral=True)
71 logger.error(f"Error: {response.status_code}") 83 logger.error(f"Error: {response.status_code}")
84 return
85
86 soup = BeautifulSoup(response.text, 'html.parser')
87 links = soup.find_all('a', class_='result__a')
88
89 if not links:
90 await ctx.respond(content="No results found.", ephemeral=True)
91 logger.warning("No results found")
92 return
93
94 embed, view = self._process_search_results(links, query)
95 await ctx.respond(embed=embed, view=view, ephemeral=True)
96 logger.info("Search results sent")
72 97
73def setup(bot): 98def setup(bot):
74 bot.add_cog(Web(bot)) 99 bot.add_cog(Web(bot))
diff --git a/main.py b/main.py
index efeb3a2..3ee86b2 100644
--- a/main.py
+++ b/main.py
@@ -8,6 +8,7 @@ import os
8import sys 8import sys
9import json 9import json
10import asyncio 10import asyncio
11import aiofiles
11import discord 12import discord
12from discord.ext import commands 13from discord.ext import commands
13from dotenv import load_dotenv 14from dotenv import load_dotenv
@@ -28,27 +29,29 @@ asyncio.set_event_loop(loop)
28 29
29bot = commands.Bot(intents=discord.Intents.all(), loop=loop) 30bot = commands.Bot(intents=discord.Intents.all(), loop=loop)
30 31
32settings = "settings.json"
33
31@bot.event 34@bot.event
32async def on_ready(): 35async def on_ready():
33 logger.info("ŁUKASZ SPERMA AKTYWACJA") 36 logger.info("ŁUKASZ SPERMA AKTYWACJA")
34 # send the message to channel ID 37 # send the message to channel ID
35 channel = bot.get_channel(1081667823799111804) 38 channel = bot.get_channel(1081667823799111804)
36 await channel.send("ŁUKASZ SPERMA AKTYWACJA") 39 await channel.send("ŁUKASZ SPERMA AKTYWACJA")
37 if not os.path.exists('settings.json'): 40 if not os.path.exists(settings):
38 with open('settings.json', 'w') as f: 41 async with aiofiles.open(settings, 'w') as f:
39 json.dump({ 42 json.dump({
40 'volume': 1.0, 43 'volume': 1.0,
41 'audio_paths': ['audio', 'music', 'sounds'] 44 'audio_paths': ['audio', 'music', 'sounds']
42 }, f) 45 }, f)
43 if os.path.exists('settings.json'): 46 if os.path.exists(settings):
44 try: 47 try:
45 with open('settings.json') as f: 48 async with aiofiles.open(settings) as f:
46 data = json.load(f) 49 data = json.load(f)
47 assert 'volume' in data and isinstance(data['volume'], float) 50 assert 'volume' in data and isinstance(data['volume'], float)
48 assert 'audio_paths' in data and isinstance(data['audio_paths'], list) 51 assert 'audio_paths' in data and isinstance(data['audio_paths'], list)
49 except (json.JSONDecodeError, AssertionError) as error: 52 except (json.JSONDecodeError, AssertionError) as error:
50 logger.error(f"Error loading settings.json: {error}") 53 logger.error(f"Error loading settings.json: {error}")
51 with open('settings.json', 'w') as f: 54 async with aiofiles.open(settings, 'w') as f:
52 json.dump({ 55 json.dump({
53 'volume': 1.0, 56 'volume': 1.0,
54 'audio_paths': ['audio', 'music', 'sounds'] 57 'audio_paths': ['audio', 'music', 'sounds']
@@ -69,7 +72,7 @@ if __name__ == "__main__":
69 'cogs.downloader', 72 'cogs.downloader',
70 'cogs.say', 73 'cogs.say',
71 'cogs.jarvis', 74 'cogs.jarvis',
72 'cogs.music.play', 75 'cogs.music.play_commands',
73 'cogs.music.stop', 76 'cogs.music.stop',
74 'cogs.music.join', 77 'cogs.music.join',
75 ] 78 ]
diff --git a/requirements.txt b/requirements.txt
index 74013da..8000108 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,7 +21,7 @@ idna==3.11
21multidict==6.7.1 21multidict==6.7.1
22mutagen==1.47.0 22mutagen==1.47.0
23propcache==0.4.1 23propcache==0.4.1
24py-cord==2.7.0 24py-cord==2.6.1
25pycparser==3.0 25pycparser==3.0
26pycryptodomex==3.23.0 26pycryptodomex==3.23.0
27PyNaCl==1.6.2 27PyNaCl==1.6.2