mirror of
https://github.com/deadcxap/YandexMusicDiscordBot.git
synced 2026-01-09 07:41:53 +03:00
feat: Add the ability to use the bot with single YM token.
This commit is contained in:
@@ -211,6 +211,11 @@ class General(Cog, BaseBot):
|
||||
async def likes(self, ctx: discord.ApplicationContext) -> None:
|
||||
logging.info(f"[GENERAL] Likes command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||
|
||||
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']:
|
||||
await ctx.respond('❌ Только владелец токена может делиться личными плейлистами.', delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
if not (client := await self.init_ym_client(ctx)):
|
||||
return
|
||||
|
||||
@@ -253,6 +258,11 @@ class General(Cog, BaseBot):
|
||||
# NOTE: Recommendations can be accessed by using /find, but it's more convenient to have it in separate command.
|
||||
logging.debug(f"[GENERAL] Recommendations command invoked by user {ctx.user.id} in guild {ctx.guild_id} for type '{content_type}'")
|
||||
|
||||
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']:
|
||||
await ctx.respond('❌ Только владелец токена может делиться личными плейлистами.', delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
if not (client := await self.init_ym_client(ctx)):
|
||||
return
|
||||
|
||||
@@ -284,6 +294,11 @@ class General(Cog, BaseBot):
|
||||
async def playlist(self, ctx: discord.ApplicationContext, name: str) -> None:
|
||||
logging.info(f"[GENERAL] Playlist command invoked by user {ctx.user.id} in guild {ctx.guild_id}")
|
||||
|
||||
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']:
|
||||
await ctx.respond('❌ Только владелец токена может делиться личными плейлистами.', delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
if not (client := await self.init_ym_client(ctx)):
|
||||
return
|
||||
|
||||
|
||||
@@ -21,60 +21,80 @@ class Settings(Cog):
|
||||
@settings.command(name="show", description="Показать текущие настройки бота.")
|
||||
async def show(self, ctx: discord.ApplicationContext) -> None:
|
||||
if not ctx.guild_id:
|
||||
logging.warning("[SETTINGS] Show command invoked without guild_id")
|
||||
logging.info("[SETTINGS] Show command invoked without guild_id")
|
||||
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||
return
|
||||
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1})
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={
|
||||
'allow_change_connect': 1, 'vote_switch_track': 1, 'vote_add': 1, 'use_single_token': 1
|
||||
})
|
||||
|
||||
vote = "✅ - Переключение" if guild['vote_switch_track'] else "❌ - Переключение"
|
||||
vote += "\n✅ - Добавление в очередь" if guild['vote_add'] else "\n❌ - Добавление в очередь"
|
||||
|
||||
connect = "\n✅ - Разрешено всем" if guild['allow_change_connect'] else "\n❌ - Только для участникам с правами управления каналом"
|
||||
|
||||
token = "🔐 - Используется токен пользователя, запустившего бота" if guild['use_single_token'] else "🔒 - Используется личный токен пользователя"
|
||||
|
||||
embed = discord.Embed(title="Настройки бота", color=0xfed42b)
|
||||
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)
|
||||
|
||||
await ctx.respond(embed=embed, ephemeral=True)
|
||||
|
||||
@settings.command(name="toggle", description="Переключить параметр настроек.")
|
||||
@settings.command(name="toggle", description="Переключить параметры основных настроек.")
|
||||
@discord.option(
|
||||
"параметр",
|
||||
parameter_name="vote_type",
|
||||
description="Тип голосования.",
|
||||
type=discord.SlashCommandOptionType.string,
|
||||
choices=['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота']
|
||||
choices=[
|
||||
'Переключение треков без голосования для всех',
|
||||
'Добавление в очередь без голосования для всех',
|
||||
'Добавление/Отключение бота из канала для всех',
|
||||
'Использовать единый токен для прослушивания'
|
||||
]
|
||||
)
|
||||
async def toggle(
|
||||
self,
|
||||
ctx: discord.ApplicationContext,
|
||||
vote_type: Literal['Переключение', 'Добавление в очередь', 'Добавление/Отключение бота']
|
||||
vote_type: Literal[
|
||||
'Переключение треков без голосования для всех',
|
||||
'Добавление в очередь без голосования для всех',
|
||||
'Добавление/Отключение бота из канала для всех',
|
||||
'Использовать единый токен для прослушивания'
|
||||
]
|
||||
) -> None:
|
||||
member = cast(discord.Member, ctx.author)
|
||||
if not ctx.guild_id:
|
||||
logging.info("[SETTINGS] Toggle command invoked without guild_id")
|
||||
await ctx.respond("❌ Эта команда может быть использована только на сервере.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
member = cast(discord.Member, ctx.user)
|
||||
if not member.guild_permissions.manage_channels:
|
||||
await ctx.respond("❌ У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
if not ctx.guild_id:
|
||||
logging.warning("[SETTINGS] Toggle command invoked without guild_id")
|
||||
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||
return
|
||||
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={
|
||||
'vote_switch_track': 1, 'vote_add': 1, 'allow_change_connect': 1})
|
||||
'vote_switch_track': 1, 'vote_add': 1, 'allow_change_connect': 1, 'use_single_token': 1
|
||||
})
|
||||
|
||||
if vote_type == 'Переключение':
|
||||
if vote_type == 'Переключение треков без голосования для всех':
|
||||
await self.db.update(ctx.guild_id, {'vote_switch_track': not guild['vote_switch_track']})
|
||||
response_message = "Голосование за переключение трека " + ("❌ выключено." if guild['vote_switch_track'] else "✅ включено.")
|
||||
|
||||
elif vote_type == 'Добавление в очередь':
|
||||
elif vote_type == 'Добавление в очередь без голосования для всех':
|
||||
await self.db.update(ctx.guild_id, {'vote_add': not guild['vote_add']})
|
||||
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']})
|
||||
response_message = f"Добавление/Отключение бота от канала теперь {'✅ разрешено' if not guild['allow_change_connect'] else '❌ запрещено'} участникам без прав управления каналом."
|
||||
|
||||
elif vote_type == 'Использовать единый токен для прослушивания':
|
||||
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 '❌ выключено'}."
|
||||
|
||||
else:
|
||||
response_message = "❌ Неизвестный тип голосования."
|
||||
|
||||
@@ -37,11 +37,7 @@ class BaseBot:
|
||||
"""
|
||||
logging.debug("[VC_EXT] Initializing Yandex Music client")
|
||||
|
||||
if not token:
|
||||
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||
token = await self.users_db.get_ym_token(uid) if uid else None
|
||||
|
||||
if not token:
|
||||
if not (token := await self.get_ym_token(ctx)):
|
||||
logging.debug("[VC_EXT] No token found")
|
||||
await self.send_response_message(ctx, "❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
|
||||
return None
|
||||
@@ -56,12 +52,28 @@ class BaseBot:
|
||||
client = await YMClient(token).init()
|
||||
except yandex_music.exceptions.UnauthorizedError:
|
||||
del self._ym_clients[token]
|
||||
await self.send_response_message(ctx, "❌ Недействительный токен. Обновите его с помощью /account login.", ephemeral=True, delete_after=15)
|
||||
await self.send_response_message(ctx, "❌ Недействительный токен Yandex Music.", ephemeral=True, delete_after=15)
|
||||
return None
|
||||
|
||||
self._ym_clients[token] = client
|
||||
return client
|
||||
|
||||
async def get_ym_token(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> str | None:
|
||||
"""Get Yandex Music token from context. It's either individual or single."""
|
||||
|
||||
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||
|
||||
if not ctx.guild_id or not uid:
|
||||
logging.info("[VC_EXT] No guild id or user id found")
|
||||
return None
|
||||
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'single_token_uid': 1})
|
||||
|
||||
if guild['single_token_uid']:
|
||||
return await self.users_db.get_ym_token(guild['single_token_uid'])
|
||||
else:
|
||||
return await self.users_db.get_ym_token(uid)
|
||||
|
||||
async def send_response_message(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
@@ -151,8 +163,22 @@ class BaseBot:
|
||||
self.menu_views[ctx.guild_id].stop()
|
||||
|
||||
self.menu_views[ctx.guild_id] = await MenuView(ctx).init(disable=disable)
|
||||
|
||||
async def get_discord_user_by_id(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, user_id: int) -> discord.User | None:
|
||||
|
||||
if isinstance(ctx, ApplicationContext) and ctx.user:
|
||||
logging.debug(f"[BASE_BOT] Getting user {user_id} from ApplicationContext")
|
||||
return await ctx.bot.fetch_user(user_id)
|
||||
elif isinstance(ctx, Interaction):
|
||||
logging.debug(f"[BASE_BOT] Getting user {user_id} from Interaction")
|
||||
return await ctx.client.fetch_user(user_id)
|
||||
elif not self.bot:
|
||||
raise ValueError("Bot instance is not available")
|
||||
else:
|
||||
logging.debug(f"[BASE_BOT] Getting user {user_id} from bot instance")
|
||||
return await self.bot.fetch_user(user_id)
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -38,7 +38,9 @@ class VoiceExtension(BaseBot):
|
||||
logging.warning("[VC_EXT] Guild id not found in context")
|
||||
return False
|
||||
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'current_track': 1, 'current_menu': 1, 'vibing': 1})
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={
|
||||
'current_track': 1, 'current_menu': 1, 'vibing': 1, 'single_token_uid': 1
|
||||
})
|
||||
|
||||
if not guild['current_track']:
|
||||
embed = None
|
||||
@@ -49,10 +51,13 @@ class VoiceExtension(BaseBot):
|
||||
guild['current_track'],
|
||||
client=YMClient() # type: ignore
|
||||
))
|
||||
|
||||
embed = await generate_item_embed(track, guild['vibing'])
|
||||
|
||||
if vc.is_paused():
|
||||
embed.set_footer(text='Приостановлено')
|
||||
elif guild['single_token_uid'] and (user := await self.get_discord_user_by_id(ctx, guild['single_token_uid'])):
|
||||
embed.set_footer(text=f"Используется токен {user.display_name}", icon_url=user.display_avatar.url)
|
||||
else:
|
||||
embed.remove_footer()
|
||||
|
||||
@@ -135,7 +140,10 @@ class VoiceExtension(BaseBot):
|
||||
logging.warning("[VC_EXT] Guild ID or User ID not found in context inside 'update_menu_embed'")
|
||||
return False
|
||||
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'vibing': 1, 'current_menu': 1, 'current_track': 1})
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={
|
||||
'vibing': 1, 'current_menu': 1, 'current_track': 1, 'single_token_uid': 1
|
||||
})
|
||||
|
||||
if not guild['current_menu']:
|
||||
logging.debug("[VC_EXT] No current menu found")
|
||||
return False
|
||||
@@ -152,6 +160,7 @@ class VoiceExtension(BaseBot):
|
||||
guild['current_track'],
|
||||
client=YMClient() # type: ignore
|
||||
))
|
||||
|
||||
embed = await generate_item_embed(track, guild['vibing'])
|
||||
|
||||
if not (vc := await self.get_voice_client(ctx)):
|
||||
@@ -160,6 +169,8 @@ class VoiceExtension(BaseBot):
|
||||
|
||||
if vc.is_paused():
|
||||
embed.set_footer(text='Приостановлено')
|
||||
elif guild['single_token_uid'] and (user := await self.get_discord_user_by_id(ctx, guild['single_token_uid'])):
|
||||
embed.set_footer(text=f"Используется токен {user.display_name}", icon_url=user.display_avatar.url)
|
||||
else:
|
||||
embed.remove_footer()
|
||||
|
||||
@@ -259,10 +270,10 @@ class VoiceExtension(BaseBot):
|
||||
logging.warning("[VC_EXT] Guild ID or User ID not found in context")
|
||||
return False
|
||||
|
||||
user = await self.users_db.get_user(uid, projection={'ym_token': 1, 'vibe_settings': 1})
|
||||
user = await self.users_db.get_user(uid, projection={'vibe_settings': 1})
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'vibing': 1, 'current_track': 1})
|
||||
|
||||
if not (client := await self.init_ym_client(ctx, user['ym_token'])):
|
||||
if not (client := await self.init_ym_client(ctx)):
|
||||
return False
|
||||
|
||||
if update_settings:
|
||||
@@ -335,7 +346,7 @@ class VoiceExtension(BaseBot):
|
||||
await ctx.respond("❌ Эта команда может быть использована только на сервере.", delete_after=15, ephemeral=True)
|
||||
return False
|
||||
|
||||
if not await self.users_db.get_ym_token(ctx.user.id):
|
||||
if not await self.get_ym_token(ctx):
|
||||
logging.debug(f"[VC_EXT] No token found for user {ctx.user.id}")
|
||||
await ctx.respond("❌ Укажите токен через /account login.", delete_after=15, ephemeral=True)
|
||||
return False
|
||||
@@ -399,7 +410,6 @@ class VoiceExtension(BaseBot):
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
track: Track | dict[str, Any],
|
||||
*,
|
||||
client: YMClient | None = None,
|
||||
vc: discord.VoiceClient | None = None,
|
||||
menu_message: discord.Message | None = None,
|
||||
button_callback: bool = False,
|
||||
@@ -427,7 +437,7 @@ class VoiceExtension(BaseBot):
|
||||
if isinstance(track, dict):
|
||||
track = cast(Track, Track.de_json(
|
||||
track,
|
||||
client=await self.init_ym_client(ctx) if not client else client # type: ignore # Async client can be used here.
|
||||
client=await self.init_ym_client(ctx) # type: ignore # Async client can be used here.
|
||||
))
|
||||
|
||||
return await self._play_track(
|
||||
@@ -475,7 +485,9 @@ class VoiceExtension(BaseBot):
|
||||
await self.send_vibe_feedback(ctx, 'trackFinished', guild['current_track'])
|
||||
|
||||
await self.db.update(ctx.guild_id, {
|
||||
'current_menu': None, 'repeat': False, 'shuffle': False, 'previous_tracks': [], 'next_tracks': [], 'votes': {}, 'vibing': False
|
||||
'current_menu': None, 'repeat': False, 'shuffle': False,
|
||||
'previous_tracks': [], 'next_tracks': [], 'votes': {},
|
||||
'vibing': False, 'current_viber_id': None
|
||||
})
|
||||
|
||||
if guild['current_menu']:
|
||||
@@ -489,7 +501,6 @@ class VoiceExtension(BaseBot):
|
||||
vc: discord.VoiceClient | None = None,
|
||||
*,
|
||||
after: bool = False,
|
||||
client: YMClient | None = None,
|
||||
menu_message: discord.Message | None = None,
|
||||
button_callback: bool = False
|
||||
) -> str | None:
|
||||
@@ -500,7 +511,6 @@ class VoiceExtension(BaseBot):
|
||||
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context
|
||||
vc (discord.VoiceClient, optional): Voice client.
|
||||
after (bool, optional): Whether the function is being called by the after callback. Defaults to False.
|
||||
client (YMClient | None, optional): Yandex Music client. Defaults to None.
|
||||
menu_message (discord.Message | None): Menu message. If None, fetches menu from channel using message id from database. Defaults to None.
|
||||
button_callback (bool, optional): Should be True if the function is being called from button callback. Defaults to False.
|
||||
|
||||
@@ -552,19 +562,7 @@ class VoiceExtension(BaseBot):
|
||||
next_track = await self.db.get_track(ctx.guild_id, 'next')
|
||||
|
||||
if next_track:
|
||||
title = await self.play_track(ctx, next_track, client=client, vc=vc, button_callback=button_callback)
|
||||
|
||||
if after and not guild['current_menu']:
|
||||
if isinstance(ctx, discord.RawReactionActionEvent):
|
||||
if not self.bot:
|
||||
raise ValueError("Bot instance not found")
|
||||
|
||||
channel = cast(discord.VoiceChannel, self.bot.get_channel(ctx.channel_id))
|
||||
await channel.send(f"Сейчас играет: **{title}**!", delete_after=15)
|
||||
else:
|
||||
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
|
||||
|
||||
return title
|
||||
return await self.play_track(ctx, next_track, vc=vc, button_callback=button_callback)
|
||||
|
||||
logging.info("[VC_EXT] No next track found")
|
||||
if after:
|
||||
@@ -609,15 +607,20 @@ class VoiceExtension(BaseBot):
|
||||
|
||||
return None
|
||||
|
||||
async def get_liked_tracks(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> list[TrackShort]:
|
||||
"""Get liked tracks from Yandex Music. Return list of tracks on success.
|
||||
async def get_reacted_tracks(
|
||||
self,
|
||||
ctx: ApplicationContext | Interaction | RawReactionActionEvent,
|
||||
tracks_type: Literal['like', 'dislike']
|
||||
) -> list[TrackShort]:
|
||||
"""Get liked or disliked tracks from Yandex Music. Return list of tracks on success.
|
||||
Return empty list if no likes found or error occurred.
|
||||
|
||||
|
||||
Args:
|
||||
ctx (ApplicationContext | Interaction | RawReactionActionEvent): Context.
|
||||
|
||||
tracks_type (Literal['like', 'dislike']): Type of tracks to get.
|
||||
|
||||
Returns:
|
||||
list[Track]: List of tracks.
|
||||
list[TrackShort]: List of tracks.
|
||||
"""
|
||||
logging.info("[VC_EXT] Getting liked tracks")
|
||||
|
||||
@@ -632,11 +635,11 @@ class VoiceExtension(BaseBot):
|
||||
if not (client := await self.init_ym_client(ctx)):
|
||||
return []
|
||||
|
||||
if not (likes := await client.users_likes_tracks()):
|
||||
logging.info("[VC_EXT] No likes found")
|
||||
if not (collection := await client.users_likes_tracks() if tracks_type == 'like' else await client.users_dislikes_tracks()):
|
||||
logging.info(f"[VC_EXT] No {tracks_type}s found")
|
||||
return []
|
||||
|
||||
return likes.tracks
|
||||
return collection.tracks
|
||||
|
||||
async def react_track(
|
||||
self,
|
||||
@@ -656,14 +659,11 @@ class VoiceExtension(BaseBot):
|
||||
logging.warning("[VC_EXT] Guild or User not found")
|
||||
return (False, None)
|
||||
|
||||
current_track = await self.db.get_track(gid, 'current')
|
||||
client = await self.init_ym_client(ctx, await self.users_db.get_ym_token(ctx.user.id))
|
||||
|
||||
if not current_track:
|
||||
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:
|
||||
if not (client := await self.init_ym_client(ctx)):
|
||||
return (False, None)
|
||||
|
||||
if action == 'like':
|
||||
@@ -701,6 +701,9 @@ class VoiceExtension(BaseBot):
|
||||
bool: Success status.
|
||||
"""
|
||||
logging.info(f"[VOICE] Performing '{vote_data['action']}' action for message {ctx.message_id}")
|
||||
|
||||
if guild['current_viber_id']:
|
||||
ctx.user_id = guild['current_viber_id']
|
||||
|
||||
if not ctx.guild_id:
|
||||
logging.warning("[VOICE] Guild not found")
|
||||
@@ -817,18 +820,20 @@ class VoiceExtension(BaseBot):
|
||||
|
||||
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
|
||||
|
||||
if not uid:
|
||||
logging.warning("[VC_EXT] User id not found")
|
||||
if not uid or not ctx.guild_id:
|
||||
logging.warning("[VC_EXT] User id or guild id not found")
|
||||
return False
|
||||
|
||||
user = await self.users_db.get_user(uid, projection={'ym_token': 1, 'vibe_batch_id': 1, 'vibe_type': 1, 'vibe_id': 1})
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'current_viber_id': 1})
|
||||
|
||||
if not user['ym_token']:
|
||||
logging.warning(f"[VC_EXT] No YM token for user {user['_id']}.")
|
||||
return False
|
||||
if guild['current_viber_id']:
|
||||
viber_id = guild['current_viber_id']
|
||||
else:
|
||||
viber_id = uid
|
||||
|
||||
client = await self.init_ym_client(ctx, user['ym_token'])
|
||||
if not client:
|
||||
user = await self.users_db.get_user(viber_id, projection={'vibe_batch_id': 1, 'vibe_type': 1, 'vibe_id': 1})
|
||||
|
||||
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
|
||||
@@ -854,7 +859,7 @@ class VoiceExtension(BaseBot):
|
||||
return feedback
|
||||
|
||||
async def _download_track(self, gid: int, track: Track) -> None:
|
||||
"""Download track to local storage. Return True on success.
|
||||
"""Download track to local storage.
|
||||
|
||||
Args:
|
||||
gid (int): Guild ID.
|
||||
@@ -927,14 +932,11 @@ class VoiceExtension(BaseBot):
|
||||
if not guild['current_track'] or track.id != guild['current_track']['id']:
|
||||
await self._download_track(gid, track)
|
||||
except yandex_music.exceptions.TimedOutError:
|
||||
if not isinstance(ctx, RawReactionActionEvent) and ctx.channel:
|
||||
channel = cast(discord.VoiceChannel, ctx.channel)
|
||||
elif not retry:
|
||||
if not retry:
|
||||
return await self._play_track(ctx, track, vc=vc, menu_message=menu_message, button_callback=button_callback, retry=True)
|
||||
elif self.bot and isinstance(ctx, RawReactionActionEvent):
|
||||
channel = cast(discord.VoiceChannel, self.bot.get_channel(ctx.channel_id))
|
||||
else:
|
||||
await self.send_response_message(ctx, f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
|
||||
logging.error(f"[VC_EXT] Failed to download track '{track.title}'")
|
||||
await channel.send(f"😔 Не удалось загрузить трек. Попробуйте сбросить меню.", delete_after=15)
|
||||
return None
|
||||
|
||||
async with aiofiles.open(f'music/{gid}.mp3', "rb") as f:
|
||||
@@ -951,7 +953,7 @@ class VoiceExtension(BaseBot):
|
||||
# Giving FFMPEG enough time to process the audio file
|
||||
await asyncio.sleep(1)
|
||||
|
||||
loop = self._get_current_event_loop(ctx)
|
||||
loop = self.get_current_event_loop(ctx)
|
||||
try:
|
||||
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.play_next_track(ctx, after=True), loop))
|
||||
except discord.errors.ClientException as e:
|
||||
|
||||
@@ -48,7 +48,7 @@ class Voice(Cog, VoiceExtension):
|
||||
guild = await self.db.get_guild(member.guild.id, projection={'current_menu': 1})
|
||||
|
||||
if not after.channel or not before.channel:
|
||||
logging.warning(f"[VOICE] No channel found for member {member.id}")
|
||||
logging.debug(f"[VOICE] No channel found for member {member.id}")
|
||||
return
|
||||
|
||||
vc = cast(
|
||||
@@ -125,12 +125,13 @@ class Voice(Cog, VoiceExtension):
|
||||
logging.info(f"[VOICE] Message {payload.message_id} is not a bot message")
|
||||
return
|
||||
|
||||
if not await self.users_db.get_ym_token(payload.user_id):
|
||||
guild = await self.db.get_guild(payload.guild_id)
|
||||
|
||||
if not guild['use_single_token'] and not (guild['single_token_uid'] or await self.users_db.get_ym_token(payload.user_id)):
|
||||
await message.remove_reaction(payload.emoji, payload.member)
|
||||
await channel.send("❌ Для участия в голосовании необходимо авторизоваться через /account login.", delete_after=15)
|
||||
return
|
||||
|
||||
guild = await self.db.get_guild(payload.guild_id)
|
||||
votes = guild['votes']
|
||||
|
||||
if str(payload.message_id) not in votes:
|
||||
@@ -220,9 +221,14 @@ class Voice(Cog, VoiceExtension):
|
||||
logging.warning("[VOICE] Join command invoked without guild_id")
|
||||
await ctx.respond("❌ Эта команда может быть использована только на сервере.", ephemeral=True)
|
||||
return
|
||||
|
||||
if ctx.author.id not in ctx.channel.voice_states:
|
||||
logging.debug("[VC_EXT] User is not connected to the voice channel")
|
||||
await ctx.respond("❌ Вы должны находиться в голосовом канале.", delete_after=15, ephemeral=True)
|
||||
return
|
||||
|
||||
member = cast(discord.Member, ctx.author)
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1})
|
||||
guild = await self.db.get_guild(ctx.guild_id, projection={'allow_change_connect': 1, 'use_single_token': 1})
|
||||
|
||||
await ctx.defer(ephemeral=True)
|
||||
if not member.guild_permissions.manage_channels and not guild['allow_change_connect']:
|
||||
@@ -234,8 +240,14 @@ class Voice(Cog, VoiceExtension):
|
||||
response_message = "❌ Не удалось подключиться к голосовому каналу."
|
||||
except discord.ClientException:
|
||||
response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave."
|
||||
except discord.DiscordException as e:
|
||||
logging.error(f"[VOICE] DiscordException: {e}")
|
||||
response_message = "❌ Произошла неизвестная ошибка при подключении к голосовому каналу."
|
||||
else:
|
||||
response_message = "✅ Подключение успешно!"
|
||||
|
||||
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})
|
||||
else:
|
||||
response_message = "❌ Вы должны отправить команду в чате голосового канала."
|
||||
|
||||
@@ -272,9 +284,11 @@ class Voice(Cog, VoiceExtension):
|
||||
return
|
||||
|
||||
await vc.disconnect(force=True)
|
||||
await ctx.respond("✅ Отключение успешно!", delete_after=15, ephemeral=True)
|
||||
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 ctx.respond("✅ Отключение успешно!", delete_after=15, ephemeral=True)
|
||||
|
||||
@queue.command(description="Очистить очередь треков и историю прослушивания.")
|
||||
async def clear(self, ctx: discord.ApplicationContext) -> None:
|
||||
logging.info(f"[VOICE] Clear queue command invoked by user {ctx.author.id} in guild {ctx.guild_id}")
|
||||
|
||||
@@ -8,6 +8,9 @@ from .user import User, ExplicitUser
|
||||
from .guild import Guild, ExplicitGuild, MessageVotes
|
||||
|
||||
mongo_server = os.getenv('MONGO_URI')
|
||||
if not mongo_server:
|
||||
raise ValueError('MONGO_URI environment variable is not set')
|
||||
|
||||
client: AsyncMongoClient = AsyncMongoClient(mongo_server)
|
||||
|
||||
db = client.YandexMusicBot
|
||||
@@ -67,12 +70,6 @@ class BaseUsersDatabase:
|
||||
)
|
||||
return cast(str | None, user.get('ym_token') if user else None)
|
||||
|
||||
async def add_playlist(self, uid: int, playlist_data: dict) -> UpdateResult:
|
||||
return await users.update_one(
|
||||
{'_id': uid},
|
||||
{'$push': {'playlists': playlist_data}}
|
||||
)
|
||||
|
||||
|
||||
class BaseGuildsDatabase:
|
||||
DEFAULT_GUILD = Guild(
|
||||
@@ -81,7 +78,6 @@ class BaseGuildsDatabase:
|
||||
current_track=None,
|
||||
current_menu=None,
|
||||
is_stopped=True,
|
||||
always_allow_menu=False,
|
||||
allow_change_connect=True,
|
||||
vote_switch_track=True,
|
||||
vote_add=True,
|
||||
@@ -89,7 +85,9 @@ class BaseGuildsDatabase:
|
||||
repeat=False,
|
||||
votes={},
|
||||
vibing=False,
|
||||
current_viber_id=None
|
||||
current_viber_id=None,
|
||||
use_single_token=False,
|
||||
single_token_uid=None
|
||||
)
|
||||
|
||||
async def update(self, gid: int, data: Guild | dict[str, Any]) -> UpdateResult:
|
||||
@@ -127,9 +125,3 @@ class BaseGuildsDatabase:
|
||||
{'_id': gid},
|
||||
{'$set': {f'votes.{mid}': data}}
|
||||
)
|
||||
|
||||
async def clear_queue(self, gid: int) -> UpdateResult:
|
||||
return await guilds.update_one(
|
||||
{'_id': gid},
|
||||
{'$set': {'next_tracks': []}}
|
||||
)
|
||||
|
||||
@@ -10,13 +10,12 @@ class MessageVotes(TypedDict):
|
||||
]
|
||||
vote_content: Any | None
|
||||
|
||||
class Guild(TypedDict, total=False):
|
||||
class Guild(TypedDict, total=False): # Don't forget to change base.py if you add a new field
|
||||
next_tracks: list[dict[str, Any]]
|
||||
previous_tracks: list[dict[str, Any]]
|
||||
current_track: dict[str, Any] | None
|
||||
current_menu: int | None
|
||||
is_stopped: bool
|
||||
always_allow_menu: bool
|
||||
is_stopped: bool # Prevents the `after` callback of play_track
|
||||
allow_change_connect: bool
|
||||
vote_switch_track: bool
|
||||
vote_add: bool
|
||||
@@ -25,6 +24,8 @@ class Guild(TypedDict, total=False):
|
||||
votes: dict[str, MessageVotes]
|
||||
vibing: bool
|
||||
current_viber_id: int | None
|
||||
use_single_token: bool
|
||||
single_token_uid: int | None
|
||||
|
||||
class ExplicitGuild(TypedDict):
|
||||
_id: int
|
||||
@@ -32,8 +33,7 @@ class ExplicitGuild(TypedDict):
|
||||
previous_tracks: list[dict[str, Any]]
|
||||
current_track: dict[str, Any] | None
|
||||
current_menu: int | None
|
||||
is_stopped: bool # Prevents the `after` callback of play_track
|
||||
always_allow_menu: bool
|
||||
is_stopped: bool
|
||||
allow_change_connect: bool
|
||||
vote_switch_track: bool
|
||||
vote_add: bool
|
||||
@@ -42,3 +42,5 @@ class ExplicitGuild(TypedDict):
|
||||
votes: dict[str, MessageVotes]
|
||||
vibing: bool
|
||||
current_viber_id: int | None
|
||||
use_single_token: bool
|
||||
single_token_uid: int | None
|
||||
|
||||
@@ -6,7 +6,7 @@ VibeSettingsOptions: TypeAlias = Literal[
|
||||
'russian', 'not-russian', 'without-words', 'any',
|
||||
]
|
||||
|
||||
class User(TypedDict, total=False):
|
||||
class User(TypedDict, total=False): # Don't forget to change base.py if you add a new field
|
||||
ym_token: str | None
|
||||
playlists: list[tuple[str, int]]
|
||||
playlists_page: int
|
||||
|
||||
Reference in New Issue
Block a user