impr: Replace all of the responses to embeds.

This commit is contained in:
Lemon4ksan
2025-03-16 18:13:17 +03:00
parent 50ed083712
commit 1228516800
7 changed files with 253 additions and 203 deletions

View File

@@ -162,7 +162,7 @@ class General(Cog, BaseBot):
"Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```" "Запустить станцию. Без уточнения станции, запускает Мою Волну.\n```/voice vibe <название станции>```"
) )
else: else:
await ctx.respond('Неизвестная команда.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Неизвестная команда.", delete_after=15, ephemeral=True)
return return
await ctx.respond(embed=embed, ephemeral=True) await ctx.respond(embed=embed, ephemeral=True)
@@ -176,16 +176,16 @@ class General(Cog, BaseBot):
client = await YMClient(token).init() client = await YMClient(token).init()
except UnauthorizedError: except UnauthorizedError:
logging.info(f"[GENERAL] Invalid token provided by user {ctx.author.id}") logging.info(f"[GENERAL] Invalid token provided by user {ctx.author.id}")
await ctx.respond('Недействительный токен.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Недействительный токен.", delete_after=15, ephemeral=True)
return return
if not client.me or not client.me.account: if not client.me or not client.me.account:
logging.warning(f"[GENERAL] Failed to get user info for user {ctx.author.id}") logging.warning(f"[GENERAL] Failed to get user info for user {ctx.author.id}")
await ctx.respond('Не удалось получить информацию о пользователе.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Не удалось получить информацию о пользователе.", delete_after=15, ephemeral=True)
return return
await self.users_db.update(ctx.author.id, {'ym_token': token}) await self.users_db.update(ctx.author.id, {'ym_token': token})
await ctx.respond(f'Привет, {client.me.account.first_name}!', delete_after=15, ephemeral=True) await self.respond(ctx, "success", f"Привет, {client.me.account.first_name}!", delete_after=15, ephemeral=True)
self._ym_clients[token] = client self._ym_clients[token] = client
logging.info(f"[GENERAL] User {ctx.author.id} logged in successfully") logging.info(f"[GENERAL] User {ctx.author.id} logged in successfully")
@@ -196,7 +196,7 @@ class General(Cog, BaseBot):
if not (token := await self.users_db.get_ym_token(ctx.user.id)): if not (token := await self.users_db.get_ym_token(ctx.user.id)):
logging.info(f"[GENERAL] No token found for user {ctx.author.id}") logging.info(f"[GENERAL] No token found for user {ctx.author.id}")
await ctx.respond('Токен не указан.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Токен не указан.", delete_after=15, ephemeral=True)
return return
if token in self._ym_clients: if token in self._ym_clients:
@@ -205,7 +205,7 @@ class General(Cog, BaseBot):
await self.users_db.update(ctx.user.id, {'ym_token': None}) await self.users_db.update(ctx.user.id, {'ym_token': None})
logging.info(f"[GENERAL] Token removed for user {ctx.author.id}") logging.info(f"[GENERAL] Token removed for user {ctx.author.id}")
await ctx.respond(f'Токен был удалён.', delete_after=15, ephemeral=True) await self.respond(ctx, "success", "Токен был удалён.", delete_after=15, ephemeral=True)
@account.command(description="Получить плейлист «Мне нравится»") @account.command(description="Получить плейлист «Мне нравится»")
async def likes(self, ctx: discord.ApplicationContext) -> None: async def likes(self, ctx: discord.ApplicationContext) -> None:
@@ -213,7 +213,7 @@ class General(Cog, BaseBot):
guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid'}) guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid'})
if guild['single_token_uid'] and ctx.author.id != guild['single_token_uid']: if guild['single_token_uid'] and ctx.author.id != guild['single_token_uid']:
await ctx.respond('Только владелец токена может делиться личными плейлистами.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Только владелец токена может делиться личными плейлистами.", delete_after=15, ephemeral=True)
return return
if not (client := await self.init_ym_client(ctx)): if not (client := await self.init_ym_client(ctx)):
@@ -223,16 +223,20 @@ class General(Cog, BaseBot):
likes = await client.users_likes_tracks() likes = await client.users_likes_tracks()
except UnauthorizedError: except UnauthorizedError:
logging.warning(f"[GENERAL] Unknown token error for user {ctx.user.id}") logging.warning(f"[GENERAL] Unknown token error for user {ctx.user.id}")
await ctx.respond("❌ Произошла неизвестная ошибка при попытке получения лайков. Пожалуйста, сообщите об этом разработчику.", delete_after=15, ephemeral=True) await self.respond(
ctx, "error",
"Произошла неизвестная ошибка при попытке получения лайков. Пожалуйста, сообщите об этом разработчику.",
delete_after=15, ephemeral=True
)
return return
if likes is None: if likes is None:
logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}") logging.info(f"[GENERAL] Failed to fetch likes for user {ctx.user.id}")
await ctx.respond('Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
return return
elif not likes: elif not likes:
logging.info(f"[GENERAL] Empty likes for user {ctx.user.id}") logging.info(f"[GENERAL] Empty likes for user {ctx.user.id}")
await ctx.respond('У вас нет треков в плейлисте «Мне нравится».', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "У вас нет треков в плейлисте «Мне нравится».", delete_after=15, ephemeral=True)
return return
await ctx.defer() # Sometimes it takes a while to fetch all tracks, so we defer the response await ctx.defer() # Sometimes it takes a while to fetch all tracks, so we defer the response
@@ -260,7 +264,7 @@ class General(Cog, BaseBot):
guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid'}) guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid'})
if guild['single_token_uid'] and ctx.author.id != guild['single_token_uid']: if guild['single_token_uid'] and ctx.author.id != guild['single_token_uid']:
await ctx.respond('Только владелец токена может делиться личными плейлистами.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Только владелец токена может делиться личными плейлистами.", delete_after=15, ephemeral=True)
return return
if not (client := await self.init_ym_client(ctx)): if not (client := await self.init_ym_client(ctx)):
@@ -269,16 +273,16 @@ class General(Cog, BaseBot):
search = await client.search(content_type, type_='playlist') search = await client.search(content_type, type_='playlist')
if not search or not search.playlists: if not search or not search.playlists:
logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}") logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}")
await ctx.respond('Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
return return
if (playlist := search.playlists.results[0]) is None: if (playlist := search.playlists.results[0]) is None:
logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}") logging.info(f"[GENERAL] Failed to fetch recommendations for user {ctx.user.id}")
await ctx.respond('Что-то пошло не так. Повторите попытку позже.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
if not await playlist.fetch_tracks_async(): if not await playlist.fetch_tracks_async():
logging.info(f"[GENERAL] User {ctx.user.id} search for '{content_type}' returned no tracks") logging.info(f"[GENERAL] User {ctx.user.id} search for '{content_type}' returned no tracks")
await ctx.respond("Пустой плейлист.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Пустой плейлист.", delete_after=15, ephemeral=True)
return return
await ctx.respond(embed=await generate_item_embed(playlist), view=ListenView(playlist)) await ctx.respond(embed=await generate_item_embed(playlist), view=ListenView(playlist))
@@ -296,7 +300,7 @@ class General(Cog, BaseBot):
guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid'}) guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid'})
if guild['single_token_uid'] and ctx.author.id != guild['single_token_uid']: if guild['single_token_uid'] and ctx.author.id != guild['single_token_uid']:
await ctx.respond('Только владелец токена может делиться личными плейлистами.', delete_after=15, ephemeral=True) await self.respond(ctx, "error", "олько владелец токена может делиться личными плейлистами.", delete_after=15, ephemeral=True)
return return
if not (client := await self.init_ym_client(ctx)): if not (client := await self.init_ym_client(ctx)):
@@ -306,17 +310,17 @@ class General(Cog, BaseBot):
playlists = await client.users_playlists_list() playlists = await client.users_playlists_list()
except UnauthorizedError: except UnauthorizedError:
logging.warning(f"[GENERAL] Unknown token error for user {ctx.user.id}") logging.warning(f"[GENERAL] Unknown token error for user {ctx.user.id}")
await ctx.respond("Произошла неизвестная ошибка при попытке получения плейлистов. Пожалуйста, сообщите об этом разработчику.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Произошла неизвестная ошибка при попытке получения плейлистов. Пожалуйста, сообщите об этом разработчику.", delete_after=15, ephemeral=True)
return return
if not (playlist := next((playlist for playlist in playlists if playlist.title == name), None)): if not (playlist := next((playlist for playlist in playlists if playlist.title == name), None)):
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found") logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' not found")
await ctx.respond("Плейлист не найден.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Плейлист не найден.", delete_after=15, ephemeral=True)
return return
if not await playlist.fetch_tracks_async(): if not await playlist.fetch_tracks_async():
logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty") logging.info(f"[GENERAL] User {ctx.user.id} playlist '{name}' is empty")
await ctx.respond("Плейлист пуст.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Плейлист пуст.", delete_after=15, ephemeral=True)
return return
await ctx.respond(embed=await generate_item_embed(playlist), view=ListenView(playlist)) await ctx.respond(embed=await generate_item_embed(playlist), view=ListenView(playlist))
@@ -349,7 +353,7 @@ class General(Cog, BaseBot):
if not (search_result := await client.search(name, nocorrect=True)): if not (search_result := await client.search(name, nocorrect=True)):
logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}") logging.warning(f"Failed to search for '{name}' for user {ctx.user.id}")
await ctx.respond("Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Что-то пошло не так. Повторите попытку позже.", delete_after=15, ephemeral=True)
return return
if content_type == 'Трек': if content_type == 'Трек':
@@ -363,7 +367,7 @@ class General(Cog, BaseBot):
if not content: if not content:
logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results") logging.info(f"[GENERAL] User {ctx.user.id} search for '{name}' returned no results")
await ctx.respond("По запросу ничего не найдено.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "По запросу ничего не найдено.", delete_after=15, ephemeral=True)
return return
result = content.results[0] result = content.results[0]

View File

@@ -5,11 +5,12 @@ import discord
from discord.ext.commands import Cog from discord.ext.commands import Cog
from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase from MusicBot.database import BaseUsersDatabase, BaseGuildsDatabase
from MusicBot.cogs.utils import BaseBot
def setup(bot): def setup(bot):
bot.add_cog(Settings(bot)) bot.add_cog(Settings(bot))
class Settings(Cog): class Settings(Cog, BaseBot):
settings = discord.SlashCommandGroup("settings", "Команды для изменения настроек бота.") settings = discord.SlashCommandGroup("settings", "Команды для изменения настроек бота.")
@@ -22,7 +23,7 @@ class Settings(Cog):
async def show(self, ctx: discord.ApplicationContext) -> None: async def show(self, ctx: discord.ApplicationContext) -> None:
if not ctx.guild_id: if not ctx.guild_id:
logging.info("[SETTINGS] Show command invoked without guild_id") logging.info("[SETTINGS] Show command invoked without guild_id")
await ctx.respond("Эта команда может быть использована только на сервере.", ephemeral=True) await self.respond(ctx, "error", "Эта команда может быть использована только на сервере.", ephemeral=True)
return return
guild = await self.db.get_guild(ctx.guild_id, projection={ guild = await self.db.get_guild(ctx.guild_id, projection={
@@ -37,6 +38,8 @@ class Settings(Cog):
token = "🔐 - Используется токен пользователя, запустившего бота" if guild['use_single_token'] else "🔒 - Используется личный токен пользователя" token = "🔐 - Используется токен пользователя, запустившего бота" if guild['use_single_token'] else "🔒 - Используется личный токен пользователя"
embed = discord.Embed(title="Настройки бота", color=0xfed42b) embed = discord.Embed(title="Настройки бота", color=0xfed42b)
embed.set_author(name='YandexMusic', icon_url="https://github.com/Lemon4ksan/YandexMusicDiscordBot/blob/main/assets/Logo.png?raw=true")
embed.add_field(name="__Голосование__", value=vote, inline=False) embed.add_field(name="__Голосование__", value=vote, inline=False)
embed.add_field(name="__Подключение/Отключение__", value=connect, inline=False) embed.add_field(name="__Подключение/Отключение__", value=connect, inline=False)
embed.add_field(name="__Токен__", value=token, inline=False) embed.add_field(name="__Токен__", value=token, inline=False)
@@ -52,8 +55,8 @@ class Settings(Cog):
choices=[ choices=[
'Переключение треков без голосования для всех', 'Переключение треков без голосования для всех',
'Добавление в очередь без голосования для всех', 'Добавление в очередь без голосования для всех',
'Добавление/Отключение бота из канала для всех', 'Добавление/Отключение бота от канала для всех',
'Использовать единый токен для прослушивания' 'Использовать токен запустившего пользователя для всех'
] ]
) )
async def toggle( async def toggle(
@@ -62,18 +65,18 @@ class Settings(Cog):
vote_type: Literal[ vote_type: Literal[
'Переключение треков без голосования для всех', 'Переключение треков без голосования для всех',
'Добавление в очередь без голосования для всех', 'Добавление в очередь без голосования для всех',
'Добавление/Отключение бота из канала для всех', 'Добавление/Отключение бота от канала для всех',
'Использовать единый токен для прослушивания' 'Использовать токен запустившего пользователя для всех'
] ]
) -> None: ) -> None:
if not ctx.guild_id: if not ctx.guild_id:
logging.info("[SETTINGS] Toggle command invoked without guild_id") logging.info("[SETTINGS] Toggle command invoked without guild_id")
await ctx.respond("Эта команда может быть использована только на сервере.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Эта команда может быть использована только на сервере.", delete_after=15, ephemeral=True)
return return
member = cast(discord.Member, ctx.user) member = cast(discord.Member, ctx.user)
if not member.guild_permissions.manage_channels: if not member.guild_permissions.manage_channels:
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return return
guild = await self.db.get_guild(ctx.guild_id, projection={ guild = await self.db.get_guild(ctx.guild_id, projection={
@@ -88,15 +91,15 @@ class Settings(Cog):
await self.db.update(ctx.guild_id, {'vote_add': not guild['vote_add']}) await self.db.update(ctx.guild_id, {'vote_add': not guild['vote_add']})
response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.") response_message = "Голосование за добавление в очередь " + ("❌ выключено." if guild['vote_add'] else "✅ включено.")
elif vote_type == 'Добавление/Отключение бота из канала для всех': elif vote_type == 'Добавление/Отключение бота от канала для всех':
await self.db.update(ctx.guild_id, {'allow_change_connect': not guild['allow_change_connect']}) await self.db.update(ctx.guild_id, {'allow_change_connect': not guild['allow_change_connect']})
response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом." response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом."
elif vote_type == 'Использовать единый токен для прослушивания': elif vote_type == 'Использовать токен запустившего пользователя для всех':
await self.db.update(ctx.guild_id, {'use_single_token': not guild['use_single_token']}) await self.db.update(ctx.guild_id, {'use_single_token': not guild['use_single_token']})
response_message = f"Использование единого токена для прослушивания теперь {'✅ включено' if not guild['use_single_token'] else '❌ выключено'}." response_message = f"Использование единого токена для прослушивания теперь {'✅ включено' if not guild['use_single_token'] else '❌ выключено'}."
else: else:
response_message = "Неизвестный тип голосования." response_message = "Неизвестный тип настроек."
await ctx.respond(response_message, delete_after=15, ephemeral=True) await self.respond(ctx, 'info', response_message, delete_after=30, ephemeral=True)

View File

@@ -7,7 +7,7 @@ from yandex_music import ClientAsync as YMClient
import discord import discord
from discord.ui import View from discord.ui import View
from discord import Interaction, ApplicationContext, RawReactionActionEvent from discord import Interaction, ApplicationContext, RawReactionActionEvent, MISSING
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
@@ -39,7 +39,7 @@ class BaseBot:
if not (token := await self.get_ym_token(ctx)): if not (token := await self.get_ym_token(ctx)):
logging.debug("[BASE_BOT] No token found") logging.debug("[BASE_BOT] No token found")
await self.send_response_message(ctx, "Укажите токен через /account login.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Укажите токен через /account login.", delete_after=15, ephemeral=True)
return None return None
try: try:
@@ -52,7 +52,7 @@ class BaseBot:
client = await YMClient(token).init() client = await YMClient(token).init()
except yandex_music.exceptions.UnauthorizedError: except yandex_music.exceptions.UnauthorizedError:
del self._ym_clients[token] del self._ym_clients[token]
await self.send_response_message(ctx, "Недействительный токен Yandex Music.", ephemeral=True, delete_after=15) await self.respond(ctx, "error", "Недействительный токен Yandex Music.", ephemeral=True, delete_after=15)
return None return None
self._ym_clients[token] = client self._ym_clients[token] = client
@@ -74,30 +74,44 @@ class BaseBot:
else: else:
return await self.users_db.get_ym_token(uid) return await self.users_db.get_ym_token(uid)
async def send_response_message( async def respond(
self, self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
response_type: Literal['info', 'success', 'error'] | None = None,
content: str | None = None, content: str | None = None,
*, *,
delete_after: float | None = None, delete_after: float | None = None,
ephemeral: bool = False, ephemeral: bool = False,
embed: discord.Embed | None = None,
view: discord.ui.View | None = None, view: discord.ui.View | None = None,
embed: discord.Embed | None = None **kwargs: Any
) -> discord.Interaction | discord.WebhookMessage | discord.Message | None: ) -> discord.Interaction | discord.WebhookMessage | discord.Message | None:
"""Send response message based on context type. self.bot must be set in order to use RawReactionActionEvent context type. """Send response message based on context type. `self.bot` must be set in order to use RawReactionActionEvent context type.
RawReactionActionEvent can't be ephemeral. RawReactionActionEvent can't be ephemeral.
Args: Args:
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context. ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
content (str): Message content to send. content (str): Message content to send. If embed is not set, used as description.
delete_after (float | None, optional): Time after which the message will be deleted. Defaults to None. response_type (Literal['info', 'success', 'error'] | None, optional): Response type. Applies if embed is not specified.
delete_after (float, optional): Time after which the message will be deleted. Defaults to None.
ephemeral (bool, optional): Whether the message is ephemeral. Defaults to False. ephemeral (bool, optional): Whether the message is ephemeral. Defaults to False.
view (discord.ui.View | None, optional): Discord view. Defaults to None. embed (discord.Embed, optional): Discord embed. Defaults to None.
embed (discord.Embed | None, optional): Discord embed. Defaults to None. view (discord.ui.View, optional): Discord view. Defaults to None.
kwargs: Additional arguments for embed generation. Applies if embed is not specified.
Returns: Returns:
(discord.InteractionMessage | discord.WebhookMessage | discord.Message | None): Message or None. Type depends on the context type. (discord.InteractionMessage | discord.WebhookMessage | discord.Message | None): Message or None. Type depends on the context type.
""" """
if not embed and response_type:
if content:
kwargs['description'] = content
embed = self.generate_response_embed(response_type, **kwargs)
content = None
if not isinstance(ctx, RawReactionActionEvent) and ctx.response.is_done():
view = MISSING
if not isinstance(ctx, RawReactionActionEvent): if not isinstance(ctx, RawReactionActionEvent):
return await ctx.respond(content, delete_after=delete_after, ephemeral=ephemeral, view=view, embed=embed) return await ctx.respond(content, delete_after=delete_after, ephemeral=ephemeral, view=view, embed=embed)
elif self.bot: elif self.bot:
@@ -106,7 +120,7 @@ class BaseBot:
return await channel.send(content, delete_after=delete_after, view=view, embed=embed) # type: ignore return await channel.send(content, delete_after=delete_after, view=view, embed=embed) # type: ignore
return None return None
async def get_message_by_id( async def get_message_by_id(
self, self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent, ctx: ApplicationContext | Interaction | RawReactionActionEvent,
@@ -188,7 +202,27 @@ class BaseBot:
self.menu_views[ctx.guild_id].stop() self.menu_views[ctx.guild_id].stop()
self.menu_views[ctx.guild_id] = await MenuView(ctx).init(disable=disable) self.menu_views[ctx.guild_id] = await MenuView(ctx).init(disable=disable)
def generate_response_embed(
self,
embed_type: Literal['info', 'success', 'error'] = 'info',
**kwargs: Any
) -> discord.Embed:
embed = discord.Embed(**kwargs)
embed.set_author(name='YandexMusic', icon_url="https://github.com/Lemon4ksan/YandexMusicDiscordBot/blob/main/assets/Logo.png?raw=true")
if embed_type == 'info':
embed.color = 0xfed42b
elif embed_type == 'success':
embed.set_author(name = "✅ Успех")
embed.color = discord.Color.green()
else:
embed.set_author(name = "❌ Ошибка")
embed.color = discord.Color.red()
return embed
def get_current_event_loop(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> asyncio.AbstractEventLoop: def get_current_event_loop(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> asyncio.AbstractEventLoop:
"""Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the self.bot instance. """Get the current event loop. If the context is a RawReactionActionEvent, get the loop from the self.bot instance.

View File

@@ -68,7 +68,7 @@ class VoiceExtension(BaseBot):
await self.update_menu_views_dict(ctx, disable=disable) await self.update_menu_views_dict(ctx, disable=disable)
interaction = await self.send_response_message(ctx, embed=embed, view=self.menu_views[ctx.guild_id]) interaction = await self.respond(ctx, embed=embed, view=self.menu_views[ctx.guild_id])
response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction response = await interaction.original_response() if isinstance(interaction, discord.Interaction) else interaction
if response: if response:
@@ -338,40 +338,40 @@ class VoiceExtension(BaseBot):
""" """
if not ctx.user: if not ctx.user:
logging.info("[VC_EXT] User not found in context inside 'voice_check'") logging.info("[VC_EXT] User not found in context inside 'voice_check'")
await ctx.respond("Пользователь не найден.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Пользователь не найден.", delete_after=15, ephemeral=True)
return False return False
if not ctx.guild_id: if not ctx.guild_id:
logging.info("[VC_EXT] Guild id not found in context inside 'voice_check'") logging.info("[VC_EXT] Guild id not found in context inside 'voice_check'")
await ctx.respond("Эта команда может быть использована только на сервере.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Эта команда может быть использована только на сервере.", delete_after=15, ephemeral=True)
return False return False
if not await self.get_ym_token(ctx): if not await self.get_ym_token(ctx):
logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}") logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}")
await ctx.respond("Укажите токен через /account login.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Укажите токен через /account login.", delete_after=15, ephemeral=True)
return False return False
if not isinstance(ctx.channel, discord.VoiceChannel): if not isinstance(ctx.channel, discord.VoiceChannel):
logging.debug("[VC_EXT] User is not in a voice channel") logging.debug("[VC_EXT] User is not in a voice channel")
await ctx.respond("Вы должны отправить команду в чате голосового канала.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Вы должны отправить команду в чате голосового канала.", delete_after=15, ephemeral=True)
return False return False
if ctx.user.id not in ctx.channel.voice_states: if ctx.user.id not in ctx.channel.voice_states:
logging.debug("[VC_EXT] User is not connected to the voice channel") logging.debug("[VC_EXT] User is not connected to the voice channel")
await ctx.respond("Вы должны находиться в голосовом канале.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Вы должны находиться в голосовом канале.", delete_after=15, ephemeral=True)
return False return False
voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients voice_clients = ctx.client.voice_clients if isinstance(ctx, Interaction) else ctx.bot.voice_clients
if not discord.utils.get(voice_clients, guild=ctx.guild): if not discord.utils.get(voice_clients, guild=ctx.guild):
logging.debug("[VC_EXT] Voice client not found") logging.debug("[VC_EXT] Voice client not found")
await ctx.respond("Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Добавьте бота в голосовой канал при помощи команды /voice join.", delete_after=15, ephemeral=True)
return False return False
if check_vibe_privilage: if check_vibe_privilage:
guild = await self.db.get_guild(ctx.guild_id, projection={'current_viber_id': 1, 'vibing': 1}) guild = await self.db.get_guild(ctx.guild_id, projection={'current_viber_id': 1, 'vibing': 1})
if guild['vibing'] and ctx.user.id != guild['current_viber_id']: if guild['vibing'] and ctx.user.id != guild['current_viber_id']:
logging.debug("[VIBE] Context user is not the current viber") logging.debug("[VIBE] Context user is not the current viber")
await ctx.respond("Вы не можете изменять чужую волну!", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Вы не можете изменять чужую волну!", delete_after=15, ephemeral=True)
return False return False
logging.debug("[VC_EXT] Voice requirements met") logging.debug("[VC_EXT] Voice requirements met")
@@ -535,7 +535,7 @@ class VoiceExtension(BaseBot):
await self.db.modify_track(ctx.guild_id, guild['current_track'], 'previous', 'insert') await self.db.modify_track(ctx.guild_id, guild['current_track'], 'previous', 'insert')
if after and not await self.update_menu_view(ctx, menu_message=menu_message, disable=True): if after and not await self.update_menu_view(ctx, menu_message=menu_message, disable=True):
await self.send_response_message(ctx, "Не удалось обновить меню.", ephemeral=True, delete_after=15) await self.respond(ctx, "error", "Не удалось обновить меню.", ephemeral=True, delete_after=15)
if guild['vibing'] and guild['current_track']: if guild['vibing'] and guild['current_track']:
await self.send_vibe_feedback(ctx, 'trackFinished' if after else 'skip', guild['current_track']) await self.send_vibe_feedback(ctx, 'trackFinished' if after else 'skip', guild['current_track'])
@@ -573,7 +573,11 @@ class VoiceExtension(BaseBot):
return None return None
async def play_previous_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, button_callback: bool = False) -> str | None: async def play_previous_track(
self,
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
button_callback: bool = False
) -> str | None:
"""Switch to the previous track in the queue. Repeat current track if no previous one found. """Switch to the previous track in the queue. Repeat current track if no previous one found.
Return track title on success. Should be called only if there's already track playing. Return track title on success. Should be called only if there's already track playing.
@@ -643,55 +647,12 @@ class VoiceExtension(BaseBot):
return [] return []
return collection.tracks return collection.tracks
async def react_track(
self,
ctx: ApplicationContext | Interaction,
action: Literal['like', 'dislike']
) -> tuple[bool, Literal['added', 'removed'] | None]:
"""Like or dislike current track. Return track title on success.
Args:
ctx (ApplicationContext | Interaction): Context.
action (Literal['like', 'dislike']): Action to perform.
Returns:
(tuple[bool, Literal['added', 'removed'] | None]): Tuple with success status and action.
"""
if not (gid := ctx.guild_id) or not ctx.user:
logging.warning("[VC_EXT] Guild or User not found")
return (False, None)
if not (current_track := await self.db.get_track(gid, 'current')):
logging.debug("[VC_EXT] Current track not found")
return (False, None)
if not (client := await self.init_ym_client(ctx)):
return (False, None)
if action == 'like':
tracks = await client.users_likes_tracks()
add_func = client.users_likes_tracks_add
remove_func = client.users_likes_tracks_remove
else:
tracks = await client.users_dislikes_tracks()
add_func = client.users_dislikes_tracks_add
remove_func = client.users_dislikes_tracks_remove
if tracks is None:
logging.debug(f"[VC_EXT] No {action}s found")
return (False, None)
if str(current_track['id']) not in [str(track.id) for track in tracks]:
logging.debug(f"[VC_EXT] Track not found in {action}s. Adding...")
await add_func(current_track['id'])
return (True, 'added')
else:
logging.debug(f"[VC_EXT] Track found in {action}s. Removing...")
await remove_func(current_track['id'])
return (True, 'removed')
async def proccess_vote(self, ctx: RawReactionActionEvent, guild: ExplicitGuild, channel: VoiceChannel, vote_data: MessageVotes) -> bool: async def proccess_vote(
self,
ctx: RawReactionActionEvent,
guild: ExplicitGuild,
vote_data: MessageVotes) -> bool:
"""Proccess vote and perform action from `vote_data` and respond. Return True on success. """Proccess vote and perform action from `vote_data` and respond. Return True on success.
Args: Args:
@@ -710,16 +671,16 @@ class VoiceExtension(BaseBot):
return False return False
if not guild['current_menu'] and not await self.send_menu_message(ctx): if not guild['current_menu'] and not await self.send_menu_message(ctx):
await channel.send(content=f"Не удалось отправить меню! Попробуйте ещё раз.", delete_after=15) await self.respond(ctx, "error", "Не удалось отправить меню! Попробуйте ещё раз.", delete_after=15)
return False return False
if vote_data['action'] in ('next', 'previous'): if vote_data['action'] in ('next', 'previous'):
if not guild.get(f'{vote_data['action']}_tracks'): if not guild.get(f'{vote_data['action']}_tracks'):
logging.info(f"[VOICE] No {vote_data['action']} tracks found for message {ctx.message_id}") logging.info(f"[VOICE] No {vote_data['action']} tracks found for message {ctx.message_id}")
await channel.send(content=f"Очередь пуста!", delete_after=15) await self.respond(ctx, "error", "Очередь пуста!", delete_after=15)
elif not (await self.play_next_track(ctx) if vote_data['action'] == 'next' else await self.play_previous_track(ctx)): elif not (await self.play_next_track(ctx) if vote_data['action'] == 'next' else await self.play_previous_track(ctx)):
await channel.send(content=f"Ошибка при смене трека! Попробуйте ещё раз.", delete_after=15) await self.respond(ctx, "error", "Ошибка при смене трека! Попробуйте ещё раз.", delete_after=15)
return False return False
elif vote_data['action'] == 'add_track': elif vote_data['action'] == 'add_track':
@@ -730,9 +691,9 @@ class VoiceExtension(BaseBot):
await self.db.modify_track(guild['_id'], vote_data['vote_content'], 'next', 'append') await self.db.modify_track(guild['_id'], vote_data['vote_content'], 'next', 'append')
if guild['current_track']: if guild['current_track']:
await channel.send(content=f"Трек был добавлен в очередь!", delete_after=15) await self.respond(ctx, "success", "Трек был добавлен в очередь!", delete_after=15)
elif not await self.play_next_track(ctx): elif not await self.play_next_track(ctx):
await channel.send(content=f"Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15) await self.respond(ctx, "error", "Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15)
return False return False
elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'): elif vote_data['action'] in ('add_album', 'add_artist', 'add_playlist'):
@@ -744,14 +705,14 @@ class VoiceExtension(BaseBot):
await self.db.modify_track(guild['_id'], vote_data['vote_content'], 'next', 'extend') await self.db.modify_track(guild['_id'], vote_data['vote_content'], 'next', 'extend')
if guild['current_track']: if guild['current_track']:
await channel.send(content=f"Контент был добавлен в очередь!", delete_after=15) await self.respond(ctx, "success", "Контент был добавлен в очередь!", delete_after=15)
elif not await self.play_next_track(ctx): elif not await self.play_next_track(ctx):
await channel.send(content=f"Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15) await self.respond(ctx, "error", "Ошибка при воспроизведении! Попробуйте ещё раз.", delete_after=15)
return False return False
elif vote_data['action'] == 'play/pause': elif vote_data['action'] == 'play/pause':
if not (vc := await self.get_voice_client(ctx)): if not (vc := await self.get_voice_client(ctx)):
await channel.send(content=f"Ошибка при изменении воспроизведения! Попробуйте ещё раз.", delete_after=15) await self.respond(ctx, "error", "Ошибка при изменении воспроизведения! Попробуйте ещё раз.", delete_after=15)
return False return False
if vc.is_playing(): if vc.is_playing():
@@ -767,31 +728,31 @@ class VoiceExtension(BaseBot):
elif vote_data['action'] == 'clear_queue': elif vote_data['action'] == 'clear_queue':
await self.db.update(ctx.guild_id, {'previous_tracks': [], 'next_tracks': []}) await self.db.update(ctx.guild_id, {'previous_tracks': [], 'next_tracks': []})
await channel.send("Очередь и история сброшены.", delete_after=15) await self.respond(ctx, "success", "Очередь и история сброшены.", delete_after=15)
elif vote_data['action'] == 'stop': elif vote_data['action'] == 'stop':
if await self.stop_playing(ctx, full=True): if await self.stop_playing(ctx, full=True):
await channel.send("Воспроизведение остановлено.", delete_after=15) await self.respond(ctx, "success", "Воспроизведение остановлено.", delete_after=15)
else: else:
await channel.send("Произошла ошибка при остановке воспроизведения.", delete_after=15) await self.respond(ctx, "error", "Произошла ошибка при остановке воспроизведения.", delete_after=15)
return False return False
elif vote_data['action'] == 'vibe_station': elif vote_data['action'] == 'vibe_station':
vibe_type, vibe_id, viber_id = vote_data['vote_content'] if isinstance(vote_data['vote_content'], list) else (None, None, None) vibe_type, vibe_id, viber_id = vote_data['vote_content'] if isinstance(vote_data['vote_content'], list) else (None, None, None)
if not vibe_type or not vibe_id or not viber_id: if not vibe_type or not vibe_id or not viber_id:
logging.warning(f"[VOICE] Recieved empty vote context for message {ctx.message_id}") logging.warning(f"[VOICE] Recieved empty vote context for message {ctx.message_id}")
await channel.send("Произошла ошибка при обновлении станции.", delete_after=15) await self.respond(ctx, "error", "Произошла ошибка при обновлении станции.", delete_after=15)
return False return False
if not await self.update_vibe(ctx, vibe_type, vibe_id, viber_id=viber_id): if not await self.update_vibe(ctx, vibe_type, vibe_id, viber_id=viber_id):
await channel.send("Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15) await self.respond(ctx, "error", "Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15)
return False return False
if (next_track := await self.db.get_track(ctx.guild_id, 'next')): if (next_track := await self.db.get_track(ctx.guild_id, 'next')):
await self.play_track(ctx, next_track) await self.play_track(ctx, next_track)
else: else:
await channel.send("Не удалось воспроизвести трек.", delete_after=15) await self.respond(ctx, "error", "Не удалось воспроизвести трек.", delete_after=15)
return False return False
else: else:
@@ -825,8 +786,6 @@ class VoiceExtension(BaseBot):
user = await self.users_db.get_user(uid, projection={'vibe_batch_id': 1, 'vibe_type': 1, 'vibe_id': 1}) user = await self.users_db.get_user(uid, projection={'vibe_batch_id': 1, 'vibe_type': 1, 'vibe_id': 1})
if not (client := await self.init_ym_client(ctx)): if not (client := await self.init_ym_client(ctx)):
logging.info(f"[VC_EXT] Failed to init YM client for user {user['_id']}")
await self.send_response_message(ctx, "❌ Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
return False return False
if feedback_type not in ('radioStarted', 'trackStarted') and track['duration_ms']: if feedback_type not in ('radioStarted', 'trackStarted') and track['duration_ms']:
@@ -930,10 +889,15 @@ class VoiceExtension(BaseBot):
if not retry: if not retry:
return await self._play_track(ctx, track, vc=vc, menu_message=menu_message, button_callback=button_callback, retry=True) return await self._play_track(ctx, track, vc=vc, menu_message=menu_message, button_callback=button_callback, retry=True)
await self.send_response_message(ctx, f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15) await self.respond(ctx, "error", "Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
logging.error(f"[VC_EXT] Failed to download track '{track.title}'") logging.error(f"[VC_EXT] Failed to download track '{track.title}'")
return None return None
except yandex_music.exceptions.InvalidBitrateError:
logging.error(f"[VC_EXT] Invalid bitrate while playing track '{track.title}'")
await self.respond(ctx, "error", "У трека отсутствует необходимый битрейт. Его проигрывание невозможно.", delete_after=15, ephemeral=True)
return None
async with aiofiles.open(f'music/{ctx.guild_id}.mp3', "rb") as f: async with aiofiles.open(f'music/{ctx.guild_id}.mp3', "rb") as f:
track_bytes = io.BytesIO(await f.read()) track_bytes = io.BytesIO(await f.read())
song = discord.FFmpegPCMAudio(track_bytes, pipe=True, options='-vn -b:a 64k -filter:a "volume=0.15"') song = discord.FFmpegPCMAudio(track_bytes, pipe=True, options='-vn -b:a 64k -filter:a "volume=0.15"')
@@ -953,11 +917,7 @@ class VoiceExtension(BaseBot):
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.play_next_track(ctx, after=True), loop)) vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.play_next_track(ctx, after=True), loop))
except discord.errors.ClientException as e: except discord.errors.ClientException as e:
logging.error(f"[VC_EXT] Error while playing track '{track.title}': {e}") logging.error(f"[VC_EXT] Error while playing track '{track.title}': {e}")
await self.send_response_message(ctx, f"Не удалось проиграть трек. Попробуйте сбросить меню.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Не удалось проиграть трек. Попробуйте сбросить меню.", delete_after=15, ephemeral=True)
return None
except yandex_music.exceptions.InvalidBitrateError:
logging.error(f"[VC_EXT] Invalid bitrate while playing track '{track.title}'")
await self.send_response_message(ctx, f"У трека отсутствует необходимый битрейт. Его проигрывание невозможно.", delete_after=15, ephemeral=True)
return None return None
logging.info(f"[VC_EXT] Playing track '{track.title}'") logging.info(f"[VC_EXT] Playing track '{track.title}'")

View File

@@ -152,7 +152,7 @@ class Voice(Cog, VoiceExtension):
if len(vote_data['positive_votes']) >= required_votes: if len(vote_data['positive_votes']) >= required_votes:
logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}") logging.info(f"[VOICE] Enough positive votes for message {payload.message_id}")
await message.delete() await message.delete()
await self.proccess_vote(payload, guild, channel, vote_data) await self.proccess_vote(payload, guild, vote_data)
del votes[str(payload.message_id)] del votes[str(payload.message_id)]
elif len(vote_data['negative_votes']) >= required_votes: elif len(vote_data['negative_votes']) >= required_votes:
@@ -211,7 +211,7 @@ class Voice(Cog, VoiceExtension):
async def menu(self, ctx: discord.ApplicationContext) -> None: async def menu(self, ctx: discord.ApplicationContext) -> None:
logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild_id}") logging.info(f"[VOICE] Menu command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
if await self.voice_check(ctx) and not await self.send_menu_message(ctx): if await self.voice_check(ctx) and not await self.send_menu_message(ctx):
await ctx.respond("Не удалось создать меню.", ephemeral=True) await self.respond(ctx, "error", "Не удалось создать меню.", ephemeral=True)
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.") @voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
async def join(self, ctx: discord.ApplicationContext) -> None: async def join(self, ctx: discord.ApplicationContext) -> None:
@@ -219,40 +219,41 @@ class Voice(Cog, VoiceExtension):
if not ctx.guild_id: if not ctx.guild_id:
logging.warning("[VOICE] Join command invoked without guild_id") logging.warning("[VOICE] Join command invoked without guild_id")
await ctx.respond("Эта команда может быть использована только на сервере.", ephemeral=True) await self.respond(ctx, "error", "Эта команда может быть использована только на сервере.", ephemeral=True)
return return
if ctx.author.id not in ctx.channel.voice_states: if ctx.author.id not in ctx.channel.voice_states:
logging.debug("[VC_EXT] User is not connected to the voice channel") logging.debug("[VC_EXT] User is not connected to the voice channel")
await ctx.respond("Вы должны находиться в голосовом канале.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Вы должны находиться в голосовом канале.", delete_after=15, ephemeral=True)
return return
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1, 'use_single_token': 1}) guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1, 'use_single_token': 1})
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']: if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
response_message = "У вас нет прав для выполнения этой команды." response_message = ("error", "У вас нет прав для выполнения этой команды.")
elif isinstance(ctx.channel, discord.VoiceChannel): elif isinstance(ctx.channel, discord.VoiceChannel):
try: try:
await ctx.channel.connect() await ctx.channel.connect()
except TimeoutError: except TimeoutError:
response_message = "Не удалось подключиться к голосовому каналу." response_message = ("error", "Не удалось подключиться к голосовому каналу.")
except discord.ClientException: except discord.ClientException:
response_message = "Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave." response_message = ("error", "Бот уже находится в голосовом канале.\nВыключите его с помощью команды /voice leave.")
except discord.DiscordException as e: except discord.DiscordException as e:
logging.error(f"[VOICE] DiscordException: {e}") logging.error(f"[VOICE] DiscordException: {e}")
response_message = "Произошла неизвестная ошибка при подключении к голосовому каналу." response_message = ("error", "Произошла неизвестная ошибка при подключении к голосовому каналу.")
else: else:
response_message = "Подключение успешно!" response_message = ("success", "Подключение успешно!")
if guild['use_single_token'] and await self.users_db.get_ym_token(ctx.author.id): if guild['use_single_token'] and await self.users_db.get_ym_token(ctx.author.id):
await self.db.update(ctx.guild_id, {'single_token_uid': ctx.author.id}) await self.db.update(ctx.guild_id, {'single_token_uid': ctx.author.id})
else: else:
response_message = "Вы должны отправить команду в чате голосового канала." response_message = ("error", "Вы должны отправить команду в чате голосового канала.")
logging.info(f"[VOICE] Join command response: {response_message}") logging.info(f"[VOICE] Join command response: {response_message}")
await ctx.respond(response_message, delete_after=15, ephemeral=True) await self.respond(ctx, *response_message, delete_after=15, ephemeral=True)
@voice.command(description="Заставить бота покинуть голосовой канал.") @voice.command(description="Заставить бота покинуть голосовой канал.")
async def leave(self, ctx: discord.ApplicationContext) -> None: async def leave(self, ctx: discord.ApplicationContext) -> None:
@@ -260,7 +261,7 @@ class Voice(Cog, VoiceExtension):
if not ctx.guild_id: if not ctx.guild_id:
logging.info("[VOICE] Leave command invoked without guild_id") logging.info("[VOICE] Leave command invoked without guild_id")
await ctx.respond("Эта команда может быть использована только на сервере.", ephemeral=True) await self.respond(ctx, "error", "Эта команда может быть использована только на сервере.", ephemeral=True)
return return
member = cast(discord.Member, ctx.author) member = cast(discord.Member, ctx.author)
@@ -268,7 +269,7 @@ class Voice(Cog, VoiceExtension):
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']: if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild_id}") logging.info(f"[VOICE] User {ctx.author.id} does not have permissions to execute leave command in guild {ctx.guild_id}")
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return return
if not await self.voice_check(ctx): if not await self.voice_check(ctx):
@@ -276,18 +277,18 @@ class Voice(Cog, VoiceExtension):
if not (vc := await self.get_voice_client(ctx)) or not vc.is_connected: if not (vc := await self.get_voice_client(ctx)) or not vc.is_connected:
logging.info(f"[VOICE] Voice client is not connected in guild {ctx.guild_id}") logging.info(f"[VOICE] Voice client is not connected in guild {ctx.guild_id}")
await ctx.respond("Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Бот не подключен к голосовому каналу.", delete_after=15, ephemeral=True)
return return
if not await self.stop_playing(ctx, vc=vc, full=True): if not await self.stop_playing(ctx, vc=vc, full=True):
await ctx.respond("Не удалось отключиться.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Не удалось отключиться.", delete_after=15, ephemeral=True)
return return
await vc.disconnect(force=True) await vc.disconnect(force=True)
logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild_id}") logging.info(f"[VOICE] Successfully disconnected from voice channel in guild {ctx.guild_id}")
await self.db.update(ctx.guild_id, {'single_token_uid': None}) await self.db.update(ctx.guild_id, {'single_token_uid': None})
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True) await self.respond(ctx, "success", "Отключение успешно!", delete_after=15, ephemeral=True)
@queue.command(description="Очистить очередь треков и историю прослушивания.") @queue.command(description="Очистить очередь треков и историю прослушивания.")
async def clear(self, ctx: discord.ApplicationContext) -> None: async def clear(self, ctx: discord.ApplicationContext) -> None:
@@ -303,7 +304,7 @@ class Voice(Cog, VoiceExtension):
logging.info(f"Starting vote for clearing queue in guild {ctx.guild_id}") logging.info(f"Starting vote for clearing queue in guild {ctx.guild_id}")
response_message = f"{member.mention} хочет очистить историю прослушивания и очередь треков.\n\n Выполнить действие?." response_message = f"{member.mention} хочет очистить историю прослушивания и очередь треков.\n\n Выполнить действие?."
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60)) message = cast(discord.Interaction, await self.respond(ctx, "info", response_message, delete_after=60))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -323,7 +324,7 @@ class Voice(Cog, VoiceExtension):
return return
await self.db.update(ctx.guild_id, {'previous_tracks': [], 'next_tracks': []}) await self.db.update(ctx.guild_id, {'previous_tracks': [], 'next_tracks': []})
await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True) await self.respond(ctx, "success", "Очередь и история сброшены.", delete_after=15, ephemeral=True)
logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild_id}") logging.info(f"[VOICE] Queue and history cleared in guild {ctx.guild_id}")
@queue.command(description="Получить очередь треков.") @queue.command(description="Получить очередь треков.")
@@ -336,7 +337,7 @@ class Voice(Cog, VoiceExtension):
tracks = await self.db.get_tracks_list(ctx.guild_id, 'next') tracks = await self.db.get_tracks_list(ctx.guild_id, 'next')
if len(tracks) == 0: if len(tracks) == 0:
await ctx.respond("❌ Очередь пуста.", ephemeral=True) await self.respond(ctx, "error", "Очередь прослушивания пуста.", delete_after=15, ephemeral=True)
return return
embed = generate_queue_embed(0, tracks) embed = generate_queue_embed(0, tracks)
@@ -358,7 +359,7 @@ class Voice(Cog, VoiceExtension):
logging.info(f"Starting vote for stopping playback in guild {ctx.guild_id}") logging.info(f"Starting vote for stopping playback in guild {ctx.guild_id}")
response_message = f"{member.mention} хочет полностью остановить проигрывание.\n\n Выполнить действие?." response_message = f"{member.mention} хочет полностью остановить проигрывание.\n\n Выполнить действие?."
message = cast(discord.Interaction, await ctx.respond(response_message, delete_after=60)) message = cast(discord.Interaction, await self.respond(ctx, "info", response_message, delete_after=60))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -380,9 +381,9 @@ class Voice(Cog, VoiceExtension):
await ctx.defer(ephemeral=True) await ctx.defer(ephemeral=True)
res = await self.stop_playing(ctx, full=True) res = await self.stop_playing(ctx, full=True)
if res: if res:
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True) await self.respond(ctx, "success", "Воспроизведение остановлено.", delete_after=15, ephemeral=True)
else: else:
await ctx.respond("Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Произошла ошибка при остановке воспроизведения.", delete_after=15, ephemeral=True)
@voice.command(description="Запустить Мою Волну.") @voice.command(description="Запустить Мою Волну.")
@discord.option( @discord.option(
@@ -403,7 +404,7 @@ class Voice(Cog, VoiceExtension):
if guild['vibing']: if guild['vibing']:
logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild_id}") logging.info(f"[VOICE] Action declined: vibing is already enabled in guild {ctx.guild_id}")
await ctx.respond("Моя Волна уже включена. Используйте /voice stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Моя Волна уже включена. Используйте /voice stop, чтобы остановить воспроизведение.", delete_after=15, ephemeral=True)
return return
await ctx.defer(invisible=False) await ctx.defer(invisible=False)
@@ -420,14 +421,14 @@ class Voice(Cog, VoiceExtension):
if not content: if not content:
logging.debug(f"[VOICE] Station {name} not found") logging.debug(f"[VOICE] Station {name} not found")
await ctx.respond("Станция не найдена.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Станция не найдена.", delete_after=15, ephemeral=True)
return return
vibe_type, vibe_id = content.ad_params.other_params.split(':') if content.ad_params else (None, None) vibe_type, vibe_id = content.ad_params.other_params.split(':') if content.ad_params else (None, None)
if not vibe_type or not vibe_id: if not vibe_type or not vibe_id:
logging.debug(f"[VOICE] Station {name} has no ad params") logging.debug(f"[VOICE] Station {name} has no ad params")
await ctx.respond("Станция не найдена.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Станция не найдена.", delete_after=15, ephemeral=True)
return return
else: else:
vibe_type, vibe_id = 'user', 'onyourwave' vibe_type, vibe_id = 'user', 'onyourwave'
@@ -445,11 +446,11 @@ class Voice(Cog, VoiceExtension):
station = content.station.name station = content.station.name
else: else:
logging.warning(f"[VOICE] Station {name} not found") logging.warning(f"[VOICE] Station {name} not found")
await ctx.respond("Станция не найдена.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Станция не найдена.", delete_after=15, ephemeral=True)
return return
response_message = f"{member.mention} хочет запустить станцию **{station}**.\n\n Выполнить действие?" response_message = f"{member.mention} хочет запустить станцию **{station}**.\n\n Выполнить действие?"
message = cast(discord.WebhookMessage, await ctx.respond(response_message)) message = cast(discord.WebhookMessage, await self.respond(ctx, "info", response_message, delete_after=60))
await message.add_reaction('') await message.add_reaction('')
await message.add_reaction('') await message.add_reaction('')
@@ -468,13 +469,13 @@ class Voice(Cog, VoiceExtension):
return return
if not await self.update_vibe(ctx, vibe_type, vibe_id): if not await self.update_vibe(ctx, vibe_type, vibe_id):
await ctx.respond("Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Операция не удалась. Возможно, у вес нет подписки на Яндекс Музыку.", delete_after=15, ephemeral=True)
return return
if guild['current_menu']: if guild['current_menu']:
await ctx.respond("Моя Волна включена.", delete_after=15, ephemeral=True) await self.respond(ctx, "success", "Моя Волна включена.", delete_after=15, ephemeral=True)
elif not await self.send_menu_message(ctx, disable=True): elif not await self.send_menu_message(ctx, disable=True):
await ctx.respond("Не удалось отправить меню. Попробуйте позже.", delete_after=15, ephemeral=True) await self.respond(ctx, "error", "Не удалось отправить меню. Попробуйте позже.", delete_after=15, ephemeral=True)
if (next_track := await self.db.get_track(ctx.guild_id, 'next')): if (next_track := await self.db.get_track(ctx.guild_id, 'next')):
await self.play_track(ctx, next_track) await self.play_track(ctx, next_track)

View File

@@ -20,7 +20,7 @@ class PlayButton(Button, VoiceExtension):
if not interaction.guild_id: if not interaction.guild_id:
logging.info("[FIND] No guild found in PlayButton callback") logging.info("[FIND] No guild found in PlayButton callback")
await interaction.respond("Эта команда доступна только на серверах.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Эта команда доступна только на серверах.", ephemeral=True, delete_after=15)
return return
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
@@ -28,7 +28,7 @@ class PlayButton(Button, VoiceExtension):
guild = await self.db.get_guild(interaction.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vote_add': 1, 'vibing': 1}) guild = await self.db.get_guild(interaction.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vote_add': 1, 'vibing': 1})
if guild['vibing']: if guild['vibing']:
await interaction.respond("Нельзя добавлять треки в очередь, пока запущена волна.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Нельзя добавлять треки в очередь, пока запущена волна.", ephemeral=True, delete_after=15)
return return
channel = cast(discord.VoiceChannel, interaction.channel) channel = cast(discord.VoiceChannel, interaction.channel)
@@ -38,54 +38,54 @@ class PlayButton(Button, VoiceExtension):
tracks = [self.item] tracks = [self.item]
action = 'add_track' action = 'add_track'
vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить трек **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Трек **{self.item.title}** был добавлен в очередь." response_message = f"Трек **{self.item.title}** был добавлен в очередь."
elif isinstance(self.item, Album): elif isinstance(self.item, Album):
album = await self.item.with_tracks_async() album = await self.item.with_tracks_async()
if not album or not album.volumes: if not album or not album.volumes:
logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback") logging.debug("[FIND] Failed to fetch album tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки альбома.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось получить треки альбома.", ephemeral=True, delete_after=15)
return return
tracks = [track for volume in album.volumes for track in volume] tracks = [track for volume in album.volumes for track in volume]
action = 'add_album' action = 'add_album'
vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить альбом **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Альбом **{self.item.title}** был добавлен в очередь." response_message = f"Альбом **{self.item.title}** был добавлен в очередь."
elif isinstance(self.item, Artist): elif isinstance(self.item, Artist):
artist_tracks = await self.item.get_tracks_async() artist_tracks = await self.item.get_tracks_async()
if not artist_tracks: if not artist_tracks:
logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback") logging.debug("[FIND] Failed to fetch artist tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки артиста.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось получить треки артиста.", ephemeral=True, delete_after=15)
return return
tracks = artist_tracks.tracks.copy() tracks = artist_tracks.tracks.copy()
action = 'add_artist' action = 'add_artist'
vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить треки от **{self.item.name}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь." response_message = f"Песни артиста **{self.item.name}** были добавлены в очередь."
elif isinstance(self.item, Playlist): elif isinstance(self.item, Playlist):
short_tracks = await self.item.fetch_tracks_async() short_tracks = await self.item.fetch_tracks_async()
if not short_tracks: if not short_tracks:
logging.debug("[FIND] Failed to fetch playlist tracks in PlayButton callback") logging.debug("[FIND] Failed to fetch playlist tracks in PlayButton callback")
await interaction.respond("Не удалось получить треки из плейлиста.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось получить треки из плейлиста.", ephemeral=True, delete_after=15)
return return
tracks = [cast(Track, short_track.track) for short_track in short_tracks] tracks = [cast(Track, short_track.track) for short_track in short_tracks]
action = 'add_playlist' action = 'add_playlist'
vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить плейлист **{self.item.title}** в очередь.\n\n Голосуйте за добавление."
response_message = f"Плейлист **{self.item.title}** был добавлен в очередь." response_message = f"Плейлист **{self.item.title}** был добавлен в очередь."
elif isinstance(self.item, list): elif isinstance(self.item, list):
tracks = self.item.copy() tracks = self.item.copy()
if not tracks: if not tracks:
logging.debug("[FIND] Empty tracks list in PlayButton callback") logging.debug("[FIND] Empty tracks list in PlayButton callback")
await interaction.respond("Не удалось получить треки.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось получить треки.", ephemeral=True, delete_after=15)
return return
action = 'add_playlist' action = 'add_playlist'
vote_message = f"{member.mention} хочет добавить плейлист **Мне Нравится** в очередь.\n\n Голосуйте за добавление." vote_message = f"{member.mention} хочет добавить плейлист **Мне Нравится** в очередь.\n\n Голосуйте за добавление."
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь." response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
else: else:
raise ValueError(f"Unknown item type: '{type(self.item).__name__}'") raise ValueError(f"Unknown item type: '{type(self.item).__name__}'")
@@ -93,7 +93,7 @@ class PlayButton(Button, VoiceExtension):
if guild['vote_add'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels: if guild['vote_add'] and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
logging.info(f"Starting vote for '{action}' (from PlayButton callback)") logging.info(f"Starting vote for '{action}' (from PlayButton callback)")
message = cast(discord.Interaction, await interaction.respond(vote_message, delete_after=60)) message = cast(discord.Interaction, await self.respond(interaction, "info", vote_message, delete_after=60))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -113,9 +113,9 @@ class PlayButton(Button, VoiceExtension):
return return
if guild['current_menu']: if guild['current_menu']:
await interaction.respond(response_message, delete_after=15) await self.respond(interaction, "success", response_message, delete_after=15)
elif not await self.send_menu_message(interaction, disable=True): elif not await self.send_menu_message(interaction, disable=True):
await interaction.respond('Не удалось отправить сообщение.', ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось отправить сообщение.", ephemeral=True, delete_after=15)
if guild['current_track']: if guild['current_track']:
logging.debug(f"[FIND] Adding tracks to queue") logging.debug(f"[FIND] Adding tracks to queue")
@@ -125,7 +125,7 @@ class PlayButton(Button, VoiceExtension):
track = tracks.pop(0) track = tracks.pop(0)
await self.db.modify_track(interaction.guild_id, tracks, 'next', 'extend') await self.db.modify_track(interaction.guild_id, tracks, 'next', 'extend')
if not await self.play_track(interaction, track): if not await self.play_track(interaction, track):
await interaction.respond('Не удалось воспроизвести трек.', ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось воспроизвести трек.", ephemeral=True, delete_after=15)
if interaction.message: if interaction.message:
await interaction.message.delete() await interaction.message.delete()
@@ -150,7 +150,7 @@ class MyVibeButton(Button, VoiceExtension):
guild = await self.db.get_guild(interaction.guild_id, projection={'current_menu': 1, 'vibing': 1}) guild = await self.db.get_guild(interaction.guild_id, projection={'current_menu': 1, 'vibing': 1})
if guild['vibing']: if guild['vibing']:
await interaction.respond('Волна уже запущена. Остановите её с помощью команды /voice stop.', ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Волна уже запущена. Остановите её с помощью команды /voice stop.", ephemeral=True, delete_after=15)
return return
track_type_map = { track_type_map = {
@@ -160,7 +160,7 @@ class MyVibeButton(Button, VoiceExtension):
if isinstance(self.item, Playlist): if isinstance(self.item, Playlist):
if not self.item.owner: if not self.item.owner:
logging.warning(f"[VIBE] Playlist owner is None") logging.warning(f"[VIBE] Playlist owner is None")
await interaction.respond("Не удалось получить информацию о плейлисте. Отсутствует владелец.", ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось получить информацию о плейлисте. Отсутствует владелец.", ephemeral=True, delete_after=15)
return return
_id = self.item.owner.login + '_' + str(self.item.kind) _id = self.item.owner.login + '_' + str(self.item.kind)
@@ -187,7 +187,7 @@ class MyVibeButton(Button, VoiceExtension):
case list(): case list():
response_message = f"{member.mention} хочет запустить станцию **Моя Волна**.\n\n Выполнить действие?" response_message = f"{member.mention} хочет запустить станцию **Моя Волна**.\n\n Выполнить действие?"
message = cast(discord.Interaction, await interaction.respond(response_message)) message = cast(discord.Interaction, await self.respond(interaction, "info", response_message))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -207,7 +207,7 @@ class MyVibeButton(Button, VoiceExtension):
return return
if not guild['current_menu'] and not await self.send_menu_message(interaction, disable=True): if not guild['current_menu'] and not await self.send_menu_message(interaction, disable=True):
await interaction.respond('Не удалось отправить сообщение.', ephemeral=True, delete_after=15) await self.respond(interaction, "error", "Не удалось отправить сообщение.", ephemeral=True, delete_after=15)
await self.update_vibe(interaction, track_type_map[type(self.item)], _id) await self.update_vibe(interaction, track_type_map[type(self.item)], _id)

View File

@@ -1,5 +1,5 @@
import logging import logging
from typing import Self, cast from typing import Self, Literal, cast
from discord.ui import View, Button, Item, Select from discord.ui import View, Button, Item, Select
from discord import ( from discord import (
@@ -26,7 +26,7 @@ class ToggleButton(Button, VoiceExtension):
if not (gid := interaction.guild_id) or not interaction.user: if not (gid := interaction.guild_id) or not interaction.user:
logging.warning('[MENU] Failed to get guild ID.') logging.warning('[MENU] Failed to get guild ID.')
await interaction.respond("Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
return return
if not await self.voice_check(interaction): if not await self.voice_check(interaction):
@@ -41,7 +41,7 @@ class ToggleButton(Button, VoiceExtension):
action = "выключить" if guild[callback_type] else "включить" action = "выключить" if guild[callback_type] else "включить"
task = "перемешивание треков" if callback_type == 'shuffle' else "повтор трека" task = "перемешивание треков" if callback_type == 'shuffle' else "повтор трека"
message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {action} {task}.\n\nВыполнить действие?", delete_after=60)) message = cast(Interaction, await self.respond(interaction, "info", f"{member.mention} хочет {action} {task}.\n\nВыполнить действие?", delete_after=60))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -63,7 +63,7 @@ class ToggleButton(Button, VoiceExtension):
await self.db.update(gid, {callback_type: not guild[callback_type]}) await self.db.update(gid, {callback_type: not guild[callback_type]})
if not await self.update_menu_view(interaction, button_callback=True): if not await self.update_menu_view(interaction, button_callback=True):
await interaction.respond("Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте снова.", delete_after=15, ephemeral=True)
class PlayPauseButton(Button, VoiceExtension): class PlayPauseButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -90,7 +90,7 @@ class PlayPauseButton(Button, VoiceExtension):
logging.info(f"[MENU] User {interaction.user.id} started vote to pause/resume track in guild {gid}") logging.info(f"[MENU] User {interaction.user.id} started vote to pause/resume track in guild {gid}")
task = "приостановить" if vc.is_playing() else "возобновить" task = "приостановить" if vc.is_playing() else "возобновить"
message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {task} проигрывание.\n\nВыполнить действие?", delete_after=60)) message = cast(Interaction, await self.respond(interaction, "info", f"{member.mention} хочет {task} проигрывание.\n\nВыполнить действие?", delete_after=60))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -112,7 +112,7 @@ class PlayPauseButton(Button, VoiceExtension):
try: try:
embed = interaction.message.embeds[0] embed = interaction.message.embeds[0]
except IndexError: except IndexError:
await interaction.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return return
guild = await self.db.get_guild(interaction.guild_id, projection={'single_token_uid': 1}) guild = await self.db.get_guild(interaction.guild_id, projection={'single_token_uid': 1})
@@ -153,7 +153,7 @@ class SwitchTrackButton(Button, VoiceExtension):
if not guild[tracks_type] and not guild['vibing']: if not guild[tracks_type] and not guild['vibing']:
logging.info(f"[MENU] No tracks in '{tracks_type}' list in guild {gid}") logging.info(f"[MENU] No tracks in '{tracks_type}' list in guild {gid}")
await interaction.respond(f"Нет треков в {'очереди' if callback_type == 'next' else 'истории'}.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", f"Нет треков в {'очереди' if callback_type == 'next' else 'истории'}.", delete_after=15, ephemeral=True)
return return
member = cast(Member, interaction.user) member = cast(Member, interaction.user)
@@ -163,7 +163,7 @@ class SwitchTrackButton(Button, VoiceExtension):
logging.info(f"[MENU] User {interaction.user.id} started vote to skip track in guild {gid}") logging.info(f"[MENU] User {interaction.user.id} started vote to skip track in guild {gid}")
task = "пропустить текущий трек" if callback_type == 'next' else "вернуться к предыдущему треку" task = "пропустить текущий трек" if callback_type == 'next' else "вернуться к предыдущему треку"
message = cast(Interaction, await interaction.respond(f"{member.mention} хочет {task}.\n\nВыполнить переход?", delete_after=60)) message = cast(Interaction, await self.respond(interaction, "info", f"{member.mention} хочет {task}.\n\nВыполнить переход?", delete_after=60))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -188,7 +188,7 @@ class SwitchTrackButton(Button, VoiceExtension):
title = await self.play_previous_track(interaction, button_callback=True) title = await self.play_previous_track(interaction, button_callback=True)
if not title: if not title:
await interaction.respond(f"Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
class ReactionButton(Button, VoiceExtension): class ReactionButton(Button, VoiceExtension):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -206,7 +206,7 @@ class ReactionButton(Button, VoiceExtension):
return return
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing: if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
await interaction.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
channel = cast(VoiceChannel, interaction.channel) channel = cast(VoiceChannel, interaction.channel)
res = await self.react_track(interaction, callback_type) res = await self.react_track(interaction, callback_type)
@@ -214,26 +214,75 @@ class ReactionButton(Button, VoiceExtension):
if callback_type == 'like' and res[0]: if callback_type == 'like' and res[0]:
await self.update_menu_views_dict(interaction) await self.update_menu_views_dict(interaction)
await interaction.edit(view=self.menu_views[gid]) await interaction.edit(view=self.menu_views[gid])
await interaction.respond( await self.respond(
f"✅ Трек был {'добавлен в понравившиеся.' if res[1] == 'added' else 'удалён из понравившихся.'}", interaction, "success",
f"Трек был {'добавлен в понравившиеся.' if res[1] == 'added' else 'удалён из понравившихся.'}",
delete_after=15, ephemeral=True delete_after=15, ephemeral=True
) )
elif callback_type == 'dislike' and res[0]: elif callback_type == 'dislike' and res[0]:
if len(channel.members) == 2 and not await self.play_next_track(interaction, vc=vc, button_callback=True): if len(channel.members) == 2 and not await self.play_next_track(interaction, vc=vc, button_callback=True):
await interaction.respond("Воспроизведение приостановлено. Нет треков в очереди.", delete_after=15) await self.respond(interaction, "info", "Воспроизведение приостановлено. Нет треков в очереди.", delete_after=15)
await self.update_menu_views_dict(interaction) await self.update_menu_views_dict(interaction)
await interaction.edit(view=self.menu_views[gid]) await interaction.edit(view=self.menu_views[gid])
await interaction.respond( await self.respond(
f"✅ Трек был {'добавлен в дизлайки.' if res[1] == 'added' else 'удалён из дизлайков.'}", interaction, "success",
f"Трек был {'добавлен в дизлайки.' if res[1] == 'added' else 'удалён из дизлайков.'}",
delete_after=15, ephemeral=True delete_after=15, ephemeral=True
) )
else: else:
logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks") logging.debug(f"[VC_EXT] Failed to get {callback_type} tracks")
await interaction.respond("Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Операция не удалась. Попробуйте позже.", delete_after=15, ephemeral=True)
async def react_track(
self,
ctx: ApplicationContext | Interaction,
action: Literal['like', 'dislike']
) -> tuple[bool, Literal['added', 'removed'] | None]:
"""Like or dislike current track. Return track title on success.
Args:
ctx (ApplicationContext | Interaction): Context.
action (Literal['like', 'dislike']): Action to perform.
Returns:
(tuple[bool, Literal['added', 'removed'] | None]): Tuple with success status and action.
"""
if not (gid := ctx.guild_id) or not ctx.user:
logging.warning("[VC_EXT] Guild or User not found")
return (False, None)
if not (current_track := await self.db.get_track(gid, 'current')):
logging.debug("[VC_EXT] Current track not found")
return (False, None)
if not (client := await self.init_ym_client(ctx)):
return (False, None)
if action == 'like':
tracks = await client.users_likes_tracks()
add_func = client.users_likes_tracks_add
remove_func = client.users_likes_tracks_remove
else:
tracks = await client.users_dislikes_tracks()
add_func = client.users_dislikes_tracks_add
remove_func = client.users_dislikes_tracks_remove
if tracks is None:
logging.debug(f"[VC_EXT] No {action}s found")
return (False, None)
if str(current_track['id']) not in [str(track.id) for track in tracks]:
logging.debug(f"[VC_EXT] Track not found in {action}s. Adding...")
await add_func(current_track['id'])
return (True, 'added')
else:
logging.debug(f"[VC_EXT] Track found in {action}s. Removing...")
await remove_func(current_track['id'])
return (True, 'removed')
class LyricsButton(Button, VoiceExtension): class LyricsButton(Button, VoiceExtension):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@@ -249,8 +298,7 @@ class LyricsButton(Button, VoiceExtension):
if not (client := await self.init_ym_client(interaction)): if not (client := await self.init_ym_client(interaction)):
return return
current_track = await self.db.get_track(interaction.guild_id, 'current') if not (current_track := await self.db.get_track(interaction.guild_id, 'current')):
if not current_track:
logging.debug('[MENU] No current track found') logging.debug('[MENU] No current track found')
return return
@@ -258,7 +306,7 @@ class LyricsButton(Button, VoiceExtension):
lyrics = cast(TrackLyrics, await client.tracks_lyrics(current_track['id'])) lyrics = cast(TrackLyrics, await client.tracks_lyrics(current_track['id']))
except yandex_music.exceptions.NotFoundError: except yandex_music.exceptions.NotFoundError:
logging.debug('[MENU] Lyrics not found') logging.debug('[MENU] Lyrics not found')
await interaction.respond("Текст песни не найден. Яндекс нам соврал (опять)!", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Текст песни не найден. Яндекс нам соврал (опять)!", delete_after=15, ephemeral=True)
return return
embed = Embed( embed = Embed(
@@ -304,7 +352,7 @@ class MyVibeButton(Button, VoiceExtension):
vibe_type = 'user' vibe_type = 'user'
vibe_id = 'onyourwave' vibe_id = 'onyourwave'
message = cast(Interaction, await interaction.respond(response_message)) message = cast(Interaction, await self.respond(interaction, "info", response_message))
response = await message.original_response() response = await message.original_response()
await response.add_reaction('') await response.add_reaction('')
@@ -340,7 +388,7 @@ class MyVibeButton(Button, VoiceExtension):
if not res: if not res:
logging.info('[MENU] Failed to start the vibe') logging.info('[MENU] Failed to start the vibe')
await interaction.respond('Не удалось запустить "Мою Волну". Возможно, у вас нет подписки на Яндекс Музыку.', ephemeral=True) await self.respond(interaction, "error", "Не удалось запустить **Мою Волну**. Возможно, у вас нет подписки на Яндекс Музыку.", ephemeral=True)
if (next_track := await self.db.get_track(interaction.guild_id, 'next')): if (next_track := await self.db.get_track(interaction.guild_id, 'next')):
await self.play_track(interaction, next_track, button_callback=True) await self.play_track(interaction, next_track, button_callback=True)
@@ -359,7 +407,7 @@ class MyVibeSelect(Select, VoiceExtension):
if not interaction.user: if not interaction.user:
logging.warning('[MENU] No user in select callback') logging.warning('[MENU] No user in select callback')
return return
custom_id = interaction.custom_id custom_id = interaction.custom_id
if custom_id not in ('diversity', 'mood', 'lang'): if custom_id not in ('diversity', 'mood', 'lang'):
logging.error(f'[MENU] Unknown custom_id: {custom_id}') logging.error(f'[MENU] Unknown custom_id: {custom_id}')
@@ -470,7 +518,7 @@ class MyVibeSettingsButton(Button, VoiceExtension):
if not await self.voice_check(interaction, check_vibe_privilage=True): if not await self.voice_check(interaction, check_vibe_privilage=True):
return return
await interaction.respond('Настройки **Волны**', view=await MyVibeSettingsView(interaction).init(), ephemeral=True) await self.respond(interaction, "info", "Настройки **Волны**", view=await MyVibeSettingsView(interaction).init(), ephemeral=True)
class AddToPlaylistSelect(Select, VoiceExtension): class AddToPlaylistSelect(Select, VoiceExtension):
def __init__(self, ym_client: YMClient, *args, **kwargs): def __init__(self, ym_client: YMClient, *args, **kwargs):
@@ -522,11 +570,11 @@ class AddToPlaylistSelect(Select, VoiceExtension):
) )
if not res: if not res:
await interaction.respond('Что-то пошло не так. Попробуйте позже.', delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
elif track_in_playlist: elif track_in_playlist:
await interaction.respond('🗑 Трек был удалён из плейлиста.', delete_after=15, ephemeral=True) await self.respond(interaction, "success", "🗑 Трек был удалён из плейлиста.", delete_after=15, ephemeral=True)
else: else:
await interaction.respond('📩 Трек был добавлен в плейлист.', delete_after=15, ephemeral=True) await self.respond(interaction, "success", "📩 Трек был добавлен в плейлист.", delete_after=15, ephemeral=True)
class AddToPlaylistButton(Button, VoiceExtension): class AddToPlaylistButton(Button, VoiceExtension):
@@ -540,20 +588,20 @@ class AddToPlaylistButton(Button, VoiceExtension):
current_track = await self.db.get_track(interaction.guild_id, 'current') current_track = await self.db.get_track(interaction.guild_id, 'current')
if not current_track: if not current_track:
await interaction.respond('Нет воспроизводимого трека.', delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return return
if not (client := await self.init_ym_client(interaction)): if not (client := await self.init_ym_client(interaction)):
await interaction.respond('Что-то пошло не так. Попробуйте позже.', delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Что-то пошло не так. Попробуйте позже.", delete_after=15, ephemeral=True)
return return
if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing: if not (vc := await self.get_voice_client(interaction)) or not vc.is_playing:
await interaction.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True) await self.respond(interaction, "error", "Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
return return
playlists = await client.users_playlists_list() playlists = await client.users_playlists_list()
if not playlists: if not playlists:
await interaction.respond('У вас нет плейлистов.', delete_after=15, ephemeral=True) await self.respond(interaction, "error", "У вас нет плейлистов.", delete_after=15, ephemeral=True)
return return
view = View( view = View(