diff --git a/MusicBot/cogs/general.py b/MusicBot/cogs/general.py index f85acee..a96f67c 100644 --- a/MusicBot/cogs/general.py +++ b/MusicBot/cogs/general.py @@ -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 diff --git a/MusicBot/cogs/settings.py b/MusicBot/cogs/settings.py index 39ad4ad..f5fef41 100644 --- a/MusicBot/cogs/settings.py +++ b/MusicBot/cogs/settings.py @@ -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 = "❌ Неизвестный тип голосования." diff --git a/MusicBot/cogs/utils/base_bot.py b/MusicBot/cogs/utils/base_bot.py index be8c344..251e3b9 100644 --- a/MusicBot/cogs/utils/base_bot.py +++ b/MusicBot/cogs/utils/base_bot.py @@ -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: diff --git a/MusicBot/cogs/utils/voice_extension.py b/MusicBot/cogs/utils/voice_extension.py index ce9c9e6..65b2a4f 100644 --- a/MusicBot/cogs/utils/voice_extension.py +++ b/MusicBot/cogs/utils/voice_extension.py @@ -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: diff --git a/MusicBot/cogs/voice.py b/MusicBot/cogs/voice.py index 0aca49f..28ccebd 100644 --- a/MusicBot/cogs/voice.py +++ b/MusicBot/cogs/voice.py @@ -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}") diff --git a/MusicBot/database/base.py b/MusicBot/database/base.py index cd49b5d..2581476 100644 --- a/MusicBot/database/base.py +++ b/MusicBot/database/base.py @@ -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': []}} - ) diff --git a/MusicBot/database/guild.py b/MusicBot/database/guild.py index bc26157..0fa8e5e 100644 --- a/MusicBot/database/guild.py +++ b/MusicBot/database/guild.py @@ -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 diff --git a/MusicBot/database/user.py b/MusicBot/database/user.py index 9ba049c..5f0486d 100644 --- a/MusicBot/database/user.py +++ b/MusicBot/database/user.py @@ -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