feat: Add the ability to use the bot with single YM token.

This commit is contained in:
Lemon4ksan
2025-03-15 17:30:42 +03:00
parent a9c938b736
commit 7fe9d699b1
8 changed files with 171 additions and 100 deletions

View File

@@ -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

View File

@@ -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 = "❌ Неизвестный тип голосования."

View File

@@ -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:

View File

@@ -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:

View File

@@ -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}")

View File

@@ -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': []}}
)

View File

@@ -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

View File

@@ -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