feat: Add moderation features and voting system.

This commit is contained in:
Lemon4ksan
2025-01-19 14:29:13 +03:00
parent b9ea63b4d5
commit 025046a8cb
9 changed files with 328 additions and 115 deletions

View File

@@ -79,17 +79,17 @@ class General(Cog):
embed.description += "Добавить трек в плейлист «Мне нравится».\n```/like```"
elif command == 'queue':
embed.description += ("Получить очередь треков. По 15 элементов на страницу.\n```/queue get```\n"
"Очистить очередь треков и историю прослушивания. Требует согласия части слушателей.\n```/queue clear```\n"
"`Примечание`: Если вы один в голосовом канале или имеете роль администратора бота, голосование не требуется.")
"Очистить очередь треков и историю прослушивания. Доступно только если вы единственный в голосовом канале"
"или имеете разрешение управления каналом.\n```/queue clear```\n")
elif command == 'track':
embed.description += ("`Примечание`: Следующие команды требуют согласия части слушателей. Если вы один в голосовом канале или имеете роль администратора бота, голосование не требуется.\n\n"
"Переключиться на следующий трек в очереди и добавить его в историю.\n```/track next```\n"
embed.description += ("`Примечание`: Если вы один в голосовом канале или имеете разрешение управления каналом, голосование не начинается.\n\n"
"Переключиться на следующий трек в очереди. \n```/track next```\n"
"Приостановить текущий трек.\n ```/track pause```\n"
"Возобновить текущий трек.\n ```/track resume```\n"
"Прервать проигрывание, удалить историю, очередь и текущий плеер.\n ```/track stop```")
elif command == 'voice':
embed.description += ("Присоединить бота в голосовой канал. Требует роли администратора.\n ```/voice join```\n"
"Заставить бота покинуть голосовой канал. Требует роли администратора.\n ```/voice leave```\n"
embed.description += ("Присоединить бота в голосовой канал. Требует разрешения управления каналом.\n ```/voice join```\n"
"Заставить бота покинуть голосовой канал. Требует разрешения управления каналом.\n ```/voice leave```\n"
"Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.\n```/voice menu```")
else:
response_message = '❌ Неизвестная команда.'
@@ -213,7 +213,7 @@ class General(Cog):
embed = await process_album(album)
await ctx.respond(embed=embed, view=ListenAlbum(album))
elif content_type == 'Track' and result.tracks:
track: yandex_music.Track = result.tracks.results[0]
track = result.tracks.results[0]
album_id = cast(int, track.albums[0].id)
embed = await process_track(track)
await ctx.respond(embed=embed, view=ListenTrack(track, album_id))

View File

@@ -15,7 +15,7 @@ class PlayTrackButton(Button, VoiceExtension):
def __init__(self, track: Track, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
self.track = track
async def callback(self, interaction: Interaction) -> None:
@@ -41,7 +41,7 @@ class PlayAlbumButton(Button, VoiceExtension):
def __init__(self, album: Album, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
self.album = album
async def callback(self, interaction: Interaction) -> None:
@@ -74,7 +74,7 @@ class PlayAlbumButton(Button, VoiceExtension):
class PlayArtistButton(Button, VoiceExtension):
def __init__(self, artist: Artist, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
self.artist = artist
async def callback(self, interaction: Interaction) -> None:
@@ -88,7 +88,7 @@ class PlayArtistButton(Button, VoiceExtension):
gid = interaction.guild.id
guild = self.db.get_guild(gid)
tracks: list[Track] = artist_tracks.tracks
tracks: list[Track] = artist_tracks.tracks.copy()
if guild['current_track'] is not None:
self.db.modify_track(gid, tracks, 'next', 'extend')
@@ -107,7 +107,7 @@ class PlayArtistButton(Button, VoiceExtension):
class PlayPlaylistButton(Button, VoiceExtension):
def __init__(self, playlist: Playlist, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
self.playlist = playlist
async def callback(self, interaction: Interaction) -> None:

View File

@@ -64,22 +64,23 @@ def generate_queue_embed(page: int, tracks_list: list[dict[str, Any]]) -> Embed:
class PlayLikesButton(Button, VoiceExtension):
def __init__(self, playlist: list[Track], **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
self.playlist = playlist
async def callback(self, interaction: Interaction):
if not interaction.guild or not await self.voice_check(interaction):
return
playlist = self.playlist.copy()
gid = interaction.guild.id
guild = self.db.get_guild(gid)
if guild['current_track'] is not None:
self.db.modify_track(gid, self.playlist, 'next', 'extend')
self.db.modify_track(gid, playlist, 'next', 'extend')
response_message = f"Плейлист **«Мне нравится»** был добавлен в очередь."
else:
track = self.playlist.pop(0)
self.db.modify_track(gid, self.playlist, 'next', 'extend')
track = playlist.pop(0)
self.db.modify_track(gid, playlist, 'next', 'extend')
await self.play_track(interaction, track)
response_message = f"Сейчас играет плейлист **«Мне нравится»**!"
@@ -96,7 +97,7 @@ class ListenLikesPlaylist(View):
class MPNextButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not interaction.user:
@@ -110,7 +111,7 @@ class MPNextButton(Button, VoiceExtension):
class MPPrevButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not interaction.user:
@@ -124,7 +125,7 @@ class MPPrevButton(Button, VoiceExtension):
class MyPlalists(View, VoiceExtension):
def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True):
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
if not ctx.user:
return
user = self.users_db.get_user(ctx.user.id)
@@ -144,7 +145,7 @@ class MyPlalists(View, VoiceExtension):
class QNextButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not interaction.user or not interaction.guild:
@@ -159,7 +160,7 @@ class QNextButton(Button, VoiceExtension):
class QPrevButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not interaction.user or not interaction.guild:
@@ -174,7 +175,7 @@ class QPrevButton(Button, VoiceExtension):
class QueueView(View, VoiceExtension):
def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True):
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
if not ctx.user or not ctx.guild:
return

View File

@@ -6,7 +6,7 @@ from MusicBot.cogs.utils.voice_extension import VoiceExtension
class ToggleRepeatButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not interaction.guild:
@@ -19,7 +19,7 @@ class ToggleRepeatButton(Button, VoiceExtension):
class ToggleShuffleButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not interaction.guild:
@@ -32,13 +32,13 @@ class ToggleShuffleButton(Button, VoiceExtension):
class PlayPauseButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not await self.voice_check(interaction):
return
vc = self.get_voice_client(interaction)
vc = await self.get_voice_client(interaction)
if not vc or not interaction.message:
return
@@ -56,7 +56,7 @@ class PlayPauseButton(Button, VoiceExtension):
class NextTrackButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not await self.voice_check(interaction):
@@ -68,7 +68,7 @@ class NextTrackButton(Button, VoiceExtension):
class PrevTrackButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if not await self.voice_check(interaction):
@@ -80,11 +80,11 @@ class PrevTrackButton(Button, VoiceExtension):
class LikeButton(Button, VoiceExtension):
def __init__(self, **kwargs):
Button.__init__(self, **kwargs)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
async def callback(self, interaction: Interaction) -> None:
if await self.voice_check(interaction):
vc = self.get_voice_client(interaction)
vc = await self.get_voice_client(interaction)
if not vc or not vc.is_playing:
await interaction.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
result = await self.like_track(interaction)
@@ -99,7 +99,7 @@ class Player(View, VoiceExtension):
def __init__(self, ctx: ApplicationContext | Interaction, *items: Item, timeout: float | None = 3600, disable_on_timeout: bool = True):
View.__init__(self, *items, timeout=timeout, disable_on_timeout=disable_on_timeout)
VoiceExtension.__init__(self)
VoiceExtension.__init__(self, None)
if not ctx.guild:
return
guild = self.db.get_guild(ctx.guild.id)

View File

@@ -9,7 +9,7 @@ from PIL import Image
from yandex_music import Track, ClientAsync
import discord
from discord import Interaction, ApplicationContext
from discord import Interaction, ApplicationContext, RawReactionActionEvent
from MusicBot.database import VoiceGuildsDatabase, BaseUsersDatabase
@@ -129,7 +129,8 @@ async def get_average_color_from_url(url: str) -> int:
class VoiceExtension:
def __init__(self) -> None:
def __init__(self, bot: discord.Bot | None) -> None:
self.bot = bot
self.db = VoiceGuildsDatabase()
self.users_db = BaseUsersDatabase()
@@ -141,10 +142,13 @@ class VoiceExtension:
player_mid (int): Id of the player message. There can only be only one player in the guild.
"""
if isinstance(ctx, Interaction):
player = ctx.client.get_message(player_mid)
else:
player = await ctx.fetch_message(player_mid)
try:
if isinstance(ctx, Interaction):
player = ctx.client.get_message(player_mid)
else:
player = await ctx.fetch_message(player_mid)
except discord.DiscordException:
return
if not player:
return
@@ -197,7 +201,7 @@ class VoiceExtension:
return True
def get_voice_client(self, ctx: ApplicationContext | Interaction) -> discord.VoiceClient | None:
async def get_voice_client(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> discord.VoiceClient | None:
"""Return voice client for the given guild id. Return None if not present.
Args:
@@ -209,12 +213,18 @@ class VoiceExtension:
if isinstance(ctx, Interaction):
voice_chat = discord.utils.get(ctx.client.voice_clients, guild=ctx.guild)
elif isinstance(ctx, RawReactionActionEvent):
if not self.bot:
raise ValueError("Bot is not set.")
if not ctx.guild_id:
return
voice_chat = discord.utils.get(self.bot.voice_clients, guild=await self.bot.fetch_guild(ctx.guild_id))
else:
voice_chat = discord.utils.get(ctx.bot.voice_clients, guild=ctx.guild)
return cast(discord.VoiceClient, voice_chat)
return cast((discord.VoiceClient | None), voice_chat)
async def play_track(self, ctx: ApplicationContext | Interaction, track: Track) -> str | None:
async def play_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, track: Track) -> str | None:
"""Download ``track`` by its id and play it in the voice channel. Return track title on success.
If sound is already playing, add track id to the queue. There's no response to the context.
@@ -225,22 +235,27 @@ class VoiceExtension:
Returns:
str | None: Song title or None.
"""
if not ctx.guild:
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid:
return None
vc = self.get_voice_client(ctx)
vc = await self.get_voice_client(ctx)
if not vc:
return None
if isinstance(ctx, Interaction):
loop = ctx.client.loop
else:
elif isinstance(ctx, ApplicationContext):
loop = ctx.bot.loop
else:
if not self.bot:
raise ValueError("Bot is not set.")
loop = self.bot.loop
gid = ctx.guild.id
guild = self.db.get_guild(gid)
await track.download_async(f'music/{ctx.guild.id}.mp3')
song = discord.FFmpegPCMAudio(f'music/{ctx.guild.id}.mp3', options='-vn -filter:a "volume=0.15"')
await track.download_async(f'music/{gid}.mp3')
song = discord.FFmpegPCMAudio(f'music/{gid}.mp3', options='-vn -filter:a "volume=0.15"')
vc.play(song, after=lambda exc: asyncio.run_coroutine_threadsafe(self.next_track(ctx, after=True), loop))
@@ -248,22 +263,23 @@ class VoiceExtension:
self.db.update(gid, {'is_stopped': False})
player = guild['current_player']
if player is not None:
if player is not None and not isinstance(ctx, discord.RawReactionActionEvent):
await self.update_player_embed(ctx, player)
return track.title
def stop_playing(self, ctx: ApplicationContext | Interaction) -> None:
if not ctx.guild:
async def stop_playing(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> None:
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
if not gid:
return
vc = self.get_voice_client(ctx)
vc = await self.get_voice_client(ctx)
if vc:
self.db.update(ctx.guild.id, {'current_track': None, 'is_stopped': True})
self.db.update(gid, {'current_track': None, 'is_stopped': True})
vc.stop()
return
async def next_track(self, ctx: ApplicationContext | Interaction, *, after: bool = False) -> str | None:
async def next_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent, *, after: bool = False) -> str | None:
"""Switch to the next track in the queue. Return track title on success.
Doesn't change track if stopped. Stop playing if tracks list is empty.
@@ -274,16 +290,18 @@ class VoiceExtension:
Returns:
str | None: Track title or None.
"""
if not ctx.guild or not ctx.user:
return None
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid:
return
gid = ctx.guild.id
guild = self.db.get_guild(gid)
token = self.users_db.get_ym_token(ctx.user.id)
token = self.users_db.get_ym_token(uid)
title = None
if guild['is_stopped']:
return None
if not self.get_voice_client(ctx): # Silently return if bot got kicked
if not await self.get_voice_client(ctx): # Silently return if bot got kicked
return None
current_track = guild['current_track']
@@ -296,15 +314,18 @@ class VoiceExtension:
else:
next_track = self.db.get_track(gid, 'next')
if current_track:
if current_track and guild['current_player']:
self.db.modify_track(gid, current_track, 'previous', 'insert')
if next_track:
ym_track = Track.de_json(next_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore
return None
await self.stop_playing(ctx)
title = await self.play_track(ctx, ym_track) # type: ignore
if after and not guild['current_player'] and not isinstance(ctx, discord.RawReactionActionEvent):
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
return title
async def prev_track(self, ctx: ApplicationContext | Interaction) -> str | None:
"""Switch to the previous track in the queue. Repeat curren the song if no previous tracks.
@@ -328,14 +349,14 @@ class VoiceExtension:
title = None
if prev_track:
ym_track = Track.de_json(prev_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx)
await self.stop_playing(ctx)
title = await self.play_track(ctx, ym_track) # type: ignore
elif current_track:
title = await self.repeat_current_track(ctx)
return title
async def repeat_current_track(self, ctx: ApplicationContext | Interaction) -> str | None:
async def repeat_current_track(self, ctx: ApplicationContext | Interaction | RawReactionActionEvent) -> str | None:
"""Repeat current track. Return track title on success.
Args:
@@ -345,16 +366,17 @@ class VoiceExtension:
str | None: Track title or None.
"""
if not ctx.guild or not ctx.user:
return None
gid = ctx.guild_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.guild.id if ctx.guild else None
uid = ctx.user_id if isinstance(ctx, discord.RawReactionActionEvent) else ctx.user.id if ctx.user else None
if not gid or not uid:
return
gid = ctx.guild.id
token = self.users_db.get_ym_token(ctx.user.id)
token = self.users_db.get_ym_token(gid)
current_track = self.db.get_track(gid, 'current')
if current_track:
ym_track = Track.de_json(current_track, client=ClientAsync(token)) # type: ignore
self.stop_playing(ctx)
await self.stop_playing(ctx)
return await self.play_track(ctx, ym_track) # type: ignore
return None

View File

@@ -1,4 +1,4 @@
from typing import cast
from typing import cast, TypedDict, Literal
import discord
from discord.ext.commands import Cog
@@ -10,13 +10,130 @@ from MusicBot.cogs.utils.player import Player
from MusicBot.cogs.utils.misc import QueueView, generate_queue_embed
def setup(bot: discord.Bot):
bot.add_cog(Voice())
bot.add_cog(Voice(bot))
class Voice(Cog, VoiceExtension):
voice = discord.SlashCommandGroup("voice", "Команды, связанные с голосовым каналом.")
queue = discord.SlashCommandGroup("queue", "Команды, связанные с очередью треков.")
track = discord.SlashCommandGroup("track", "Команды, связанные с текущим треком.")
track = discord.SlashCommandGroup("track", "Команды, связанные с треками в голосовом канале.")
def __init__(self, bot: discord.Bot):
VoiceExtension.__init__(self, bot)
self.bot = bot
MessageVotes = TypedDict('MessageVotes', {'positive_votes': set[int], 'negative_votes': set[int], 'total_members': int, 'action': Literal['next']})
self.vote_messages: dict[int, dict[int, MessageVotes]] = {}
@Cog.listener()
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None:
gid = member.guild.id
guild = self.db.get_guild(gid)
if after.channel:
channel = cast(discord.VoiceChannel, after.channel)
else:
channel = cast(discord.VoiceChannel, before.channel)
if not channel:
return
discord_guild = await self.bot.fetch_guild(gid)
vc = cast((discord.VoiceClient | None), discord.utils.get(self.bot.voice_clients, guild=discord_guild))
if len(channel.members) == 1 and vc:
self.db.clear_history(gid)
self.db.update(gid, {'current_track': None, 'is_stopped': True})
vc.stop()
if len(channel.members) > 2 and not guild['always_allow_menu']:
current_player = self.db.get_current_player(gid)
if current_player is not None:
self.db.update(gid, {'current_player': None, 'repeat': False, 'shuffle': False})
try:
message = await channel.fetch_message(current_player)
await message.delete()
except (discord.NotFound, discord.Forbidden):
pass
await channel.send("Текущий плеер отключён, так как в канале больше одного человека.", delete_after=15)
@Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
bot_id = self.bot.user.id # type: ignore
if payload.user_id == bot_id:
return
channel = cast(discord.VoiceChannel, self.bot.get_channel(payload.channel_id))
if not channel:
return
message = await channel.fetch_message(payload.message_id)
if not message or message.author.id != bot_id:
return
if not self.users_db.get_ym_token(payload.user_id):
await message.remove_reaction(payload.emoji, payload.member) # type: ignore
await channel.send("Для участия в голосовании необходимо авторизоваться через /account login.", delete_after=15)
return
guild_id = payload.guild_id
if guild_id not in self.vote_messages:
return
if payload.message_id not in self.vote_messages[guild_id]:
return
vote_data = self.vote_messages[guild_id][payload.message_id]
if payload.emoji.name == '':
vote_data['positive_votes'].add(payload.user_id)
elif payload.emoji.name == '':
vote_data['negative_votes'].add(payload.user_id)
total_members = len(channel.members)
if total_members <= 5:
required_votes = 2
elif total_members <= 10:
required_votes = 4
elif total_members <= 15:
required_votes = 6
else:
required_votes = 9
if len(vote_data['positive_votes']) >= required_votes:
if vote_data['action'] == 'next':
self.db.update(guild_id, {'is_stopped': False})
title = await self.next_track(payload)
await message.clear_reactions()
if title is not None:
await message.edit(content=f"Сейчас играет: **{title}**!", delete_after=15)
del self.vote_messages[guild_id][payload.message_id]
elif len(vote_data['negative_votes']) >= required_votes:
channel = cast(discord.VoiceChannel, self.bot.get_channel(payload.channel_id))
message = await channel.fetch_message(payload.message_id)
await message.clear_reactions()
await message.edit(content='Запрос был отклонён.', delete_after=15)
del self.vote_messages[guild_id][payload.message_id]
@Cog.listener()
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent) -> None:
guild_id = payload.guild_id
if guild_id not in self.vote_messages:
return
if payload.message_id not in self.vote_messages[guild_id]:
return
channel = cast(discord.VoiceChannel, self.bot.get_channel(payload.channel_id))
if not channel:
return
message = await channel.fetch_message(payload.message_id)
if not message or message.author.id != self.bot.user.id: # type: ignore
return
vote_data = self.vote_messages[guild_id][payload.message_id]
if payload.emoji.name == '✔️':
vote_data['positive_votes'].discard(payload.user_id)
elif payload.emoji.name == '':
vote_data['negative_votes'].discard(payload.user_id)
@voice.command(name="menu", description="Создать меню проигрывателя. Доступно только если вы единственный в голосовом канале.")
async def menu(self, ctx: discord.ApplicationContext) -> None:
@@ -24,11 +141,16 @@ class Voice(Cog, VoiceExtension):
return
guild = self.db.get_guild(ctx.guild.id)
channel = cast(discord.VoiceChannel, ctx.channel)
embed = None
if len(channel.members) > 2 and not guild['always_allow_menu']:
await ctx.respond("Вы не единственный в голосовом канале.", ephemeral=True)
return
if guild['current_track']:
embed = await generate_player_embed(Track.de_json(guild['current_track'], client=ClientAsync())) # type: ignore
vc = self.get_voice_client(ctx)
vc = await self.get_voice_client(ctx)
if vc and vc.is_paused():
embed.set_footer(text='Приостановлено')
else:
@@ -41,36 +163,47 @@ class Voice(Cog, VoiceExtension):
interaction = cast(discord.Interaction, await ctx.respond(view=Player(ctx), embed=embed, delete_after=3600))
response = await interaction.original_response()
self.db.update(ctx.guild.id, {'current_player': response.id})
@voice.command(name="join", description="Подключиться к голосовому каналу, в котором вы сейчас находитесь.")
async def join(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx)
if vc and vc.is_playing():
member = cast(discord.Member, ctx.author)
vc = await self.get_voice_client(ctx)
if not member.guild_permissions.manage_channels:
response_message = "У вас нет прав для выполнения этой команды."
elif vc and vc.is_playing():
response_message = "❌ Бот уже находится в голосовом канале. Выключите его с помощью команды /voice leave."
elif isinstance(ctx.channel, discord.VoiceChannel):
await ctx.channel.connect(timeout=15)
response_message = "Подключение успешно!"
else:
response_message = "❌ Вы должны отправить команду в голосовом канале."
await ctx.respond(response_message, delete_after=15, ephemeral=True)
@voice.command(description="Заставить бота покинуть голосовой канал.")
async def leave(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx)
if vc and await self.voice_check(ctx):
self.stop_playing(ctx)
member = cast(discord.Member, ctx.author)
if not member.guild_permissions.manage_channels:
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
return
vc = await self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc:
await self.stop_playing(ctx)
self.db.clear_history(ctx.guild.id)
await vc.disconnect(force=True)
await ctx.respond("Отключение успешно!", delete_after=15, ephemeral=True)
@queue.command(description="Очистить очередь треков и историю прослушивания.")
async def clear(self, ctx: discord.ApplicationContext) -> None:
if not await self.voice_check(ctx):
return
self.db.clear_history(ctx.guild.id)
await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True)
member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
await ctx.respond("У вас нет прав для выполнения этой команды.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx) and (len(channel.members) == 2 or member.guild_permissions.manage_channels):
self.db.clear_history(ctx.guild.id)
await ctx.respond("Очередь и история сброшены.", delete_after=15, ephemeral=True)
@queue.command(description="Получить очередь треков.")
async def get(self, ctx: discord.ApplicationContext) -> None:
if not await self.voice_check(ctx):
@@ -79,58 +212,95 @@ class Voice(Cog, VoiceExtension):
self.users_db.update(ctx.user.id, {'queue_page': 0})
embed = generate_queue_embed(0, tracks)
await ctx.respond(embed=embed, view=QueueView(ctx), ephemeral=True)
@track.command(description="Приостановить текущий трек.")
async def pause(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc is not None:
member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)) is not None:
if not vc.is_paused():
vc.pause()
player = self.db.get_current_player(ctx.guild.id)
if player:
await self.update_player_embed(ctx, player)
await ctx.respond("Воспроизведение приостановлено.", delete_after=15, ephemeral=True)
else:
await ctx.respond("Воспроизведение уже приостановлено.", delete_after=15, ephemeral=True)
@track.command(description="Возобновить текущий трек.")
async def resume(self, ctx: discord.ApplicationContext) -> None:
vc = self.get_voice_client(ctx)
if await self.voice_check(ctx) and vc is not None:
member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx) and (vc := await self.get_voice_client(ctx)) is not None:
if vc.is_paused():
vc.resume()
player = self.db.get_current_player(ctx.guild.id)
if player:
await self.update_player_embed(ctx, player)
await ctx.respond("Воспроизведение восстановлено.", delete_after=15, ephemeral=True)
else:
await ctx.respond("Воспроизведение не на паузе.", delete_after=15, ephemeral=True)
@track.command(description="Прервать проигрывание, удалить историю, очередь и текущий плеер.")
async def stop(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx):
member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel)
if len(channel.members) > 2 and not member.guild_permissions.manage_channels:
await ctx.respond("❌ Вы не можете остановить воспроизведение, пока в канале находятся другие пользователи.", delete_after=15, ephemeral=True)
elif await self.voice_check(ctx):
self.db.clear_history(ctx.guild.id)
self.stop_playing(ctx)
current_player = self.db.get_guild(ctx.guild.id)['current_player']
await self.stop_playing(ctx)
current_player = self.db.get_current_player(ctx.guild.id)
if current_player is not None:
self.db.update(ctx.guild.id, {'current_player': None, 'repeat': False, 'shuffle': False})
message = await ctx.fetch_message(current_player)
await message.delete()
try:
message = await ctx.fetch_message(current_player)
await message.delete()
except discord.DiscordException:
pass
self.db.update(ctx.guild.id, {'current_player': None, 'repeat': False, 'shuffle': False})
await ctx.respond("Воспроизведение остановлено.", delete_after=15, ephemeral=True)
@track.command(description="Переключиться на следующую песню в очереди.")
async def next(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx):
gid = ctx.guild.id
tracks_list = self.db.get_tracks_list(gid, 'next')
if not tracks_list:
await ctx.respond("Нет песенен в очереди.", delete_after=15, ephemeral=True)
return
if not await self.voice_check(ctx):
return
gid = ctx.guild.id
tracks_list = self.db.get_tracks_list(gid, 'next')
if not tracks_list:
await ctx.respond("❌ Нет песенен в очереди.", delete_after=15, ephemeral=True)
return
member = cast(discord.Member, ctx.author)
channel = cast(discord.VoiceChannel, ctx.channel)
if self.db.get_track(gid, 'current') and len(channel.members) > 2 and not member.guild_permissions.manage_channels:
message = cast(discord.Interaction, await ctx.respond(f"{ctx.user.mention} хочет пропустить текущий трек.\n\nВыполнить переход?", delete_after=30))
response = await message.original_response()
await response.add_reaction('')
await response.add_reaction('')
self.vote_messages[ctx.guild.id] = {
response.id: {
'positive_votes': set(),
'negative_votes': set(),
'total_members': len(channel.members),
'action': 'next'
}
}
else:
self.db.update(gid, {'is_stopped': False})
title = await self.next_track(ctx)
if title is not None:
await ctx.respond(f"Сейчас играет: **{title}**!", delete_after=15)
else:
await ctx.respond(f"Нет треков в очереди.", delete_after=15, ephemeral=True)
@voice.command(description="Добавить трек в избранное.")
@track.command(description="Добавить трек в избранное или убрать, если он уже там.")
async def like(self, ctx: discord.ApplicationContext) -> None:
if await self.voice_check(ctx):
vc = self.get_voice_client(ctx)
vc = await self.get_voice_client(ctx)
if not vc or not vc.is_playing:
await ctx.respond("Нет воспроизводимого трека.", delete_after=15, ephemeral=True)
result = await self.like_track(ctx)

View File

@@ -77,7 +77,8 @@ class BaseGuildsDatabase:
current_player=None,
is_stopped=True,
allow_explicit=True,
allow_menu=True,
always_allow_menu=False,
disable_vote=False,
shuffle=False,
repeat=False
))

View File

@@ -142,6 +142,23 @@ class VoiceGuildsDatabase(BaseGuildsDatabase):
return track
def set_current_track(self, gid: int, track: Track | dict[str, Any]) -> None:
"""Set current track.
Args:
gid (int): Guild id.
track (Track | dict[str, Any]): Track or dictionary covertable to yandex_music.Track.
"""
if isinstance(track, Track):
track = track.to_dict()
self.update(gid, {'current_track': track})
self.update(gid, {'current_track': track})
def get_current_player(self, gid: int) -> int | None:
"""Get current player.
Args:
gid (int): Guild id.
Returns: int | None: Player message id or None if not present.
"""
guild = self.get_guild(gid)
return guild['current_player']

View File

@@ -7,7 +7,8 @@ class Guild(TypedDict, total=False):
current_player: int | None
is_stopped: bool
allow_explicit: bool
allow_menu: bool
always_allow_menu: bool
disable_vote: bool
shuffle: bool
repeat: bool
@@ -19,6 +20,7 @@ class ExplicitGuild(TypedDict):
current_player: int | None
is_stopped: bool # Prevents the `after` callback of play_track
allow_explicit: bool
allow_menu: bool # /toggle menu is only available if there's only one user in the voice chat.
always_allow_menu: bool
disable_vote: bool
shuffle: bool
repeat: bool