Initial commit
Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
This commit is contained in:
commit
db55ca7fb1
15 changed files with 2519 additions and 0 deletions
1766
.editorconfig
Normal file
1766
.editorconfig
Normal file
File diff suppressed because it is too large
Load diff
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
.idea/
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
16
Cassette.sln
Normal file
16
Cassette.sln
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cassette", "Cassette\Cassette.csproj", "{74933F3C-F5CA-4592-8C4B-76329F1ED311}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{74933F3C-F5CA-4592-8C4B-76329F1ED311}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74933F3C-F5CA-4592-8C4B-76329F1ED311}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74933F3C-F5CA-4592-8C4B-76329F1ED311}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74933F3C-F5CA-4592-8C4B-76329F1ED311}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
83
Cassette/Cassette.cs
Normal file
83
Cassette/Cassette.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using Lavalink4NET.Remora.Discord;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Cassette.Commands;
|
||||
using Remora.Commands.Extensions;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Commands;
|
||||
using Remora.Discord.Commands.Extensions;
|
||||
using Remora.Discord.Commands.Services;
|
||||
using Remora.Discord.Gateway;
|
||||
using Remora.Discord.Hosting.Extensions;
|
||||
|
||||
namespace Cassette;
|
||||
|
||||
file class Cassette
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var host = CreateHostBuilder(args)
|
||||
.UseConsoleLifetime()
|
||||
.Build();
|
||||
|
||||
var services = host.Services;
|
||||
var log = services.GetRequiredService<ILogger<Cassette>>();
|
||||
|
||||
var slashService = services.GetRequiredService<SlashService>();
|
||||
var updateSlash = await slashService.UpdateSlashCommandsAsync();
|
||||
if (!updateSlash.IsSuccess)
|
||||
{
|
||||
log.LogWarning("Failed to update slash commands: {Reason}", updateSlash.Error.Message);
|
||||
}
|
||||
|
||||
await host.RunAsync();
|
||||
}
|
||||
|
||||
private static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.AddDiscordService
|
||||
(
|
||||
services =>
|
||||
{
|
||||
var configuration = services.GetRequiredService<IConfiguration>();
|
||||
|
||||
return configuration.GetValue<string?>("BOT_TOKEN") ??
|
||||
throw new InvalidOperationException
|
||||
(
|
||||
"No bot token has been provided. " +
|
||||
"Set the BOT_TOKEN environment variable to a valid token."
|
||||
);
|
||||
}
|
||||
)
|
||||
.ConfigureServices
|
||||
(
|
||||
(_, services) =>
|
||||
{
|
||||
services.Configure<DiscordGatewayClientOptions>(g =>
|
||||
{
|
||||
g.Intents
|
||||
|= GatewayIntents.MessageContents
|
||||
| GatewayIntents.GuildVoiceStates
|
||||
| GatewayIntents.Guilds;
|
||||
});
|
||||
services
|
||||
.AddDiscordCommands(true, false)
|
||||
.AddLavalink()
|
||||
.AddCommandTree()
|
||||
.WithCommandGroup<BotCommandGroup>()
|
||||
.WithCommandGroup<ControlsCommandGroup>()
|
||||
.WithCommandGroup<InfoCommandGroup>()
|
||||
.WithCommandGroup<TrustedCommandGroup>();
|
||||
}
|
||||
)
|
||||
.ConfigureLogging
|
||||
(
|
||||
c => c
|
||||
.AddConsole()
|
||||
.AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
|
||||
.AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
|
||||
);
|
||||
}
|
||||
}
|
22
Cassette/Cassette.csproj
Normal file
22
Cassette/Cassette.csproj
Normal file
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Cassette</AssemblyName>
|
||||
<RootNamespace>Cassette</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||
<PackageReference Include="Lavalink4NET.Remora.Discord" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Remora.Commands" Version="10.0.5" />
|
||||
<PackageReference Include="Remora.Discord.Extensions" Version="5.3.2" />
|
||||
<PackageReference Include="Remora.Discord.Hosting" Version="6.0.7" />
|
||||
<PackageReference Include="Remora.Discord.Interactivity" Version="4.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
61
Cassette/Commands/BotCommandGroup.cs
Normal file
61
Cassette/Commands/BotCommandGroup.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using Cassette.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Commands.Feedback.Messages;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Cassette.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="CommandGroup"/> that shows information about the bot.
|
||||
/// </summary>
|
||||
public sealed class BotCommandGroup(
|
||||
IFeedbackService feedbackService,
|
||||
IDiscordRestUserAPI userApi) : CommandGroup
|
||||
{
|
||||
private const string RepositoryUrl = "https://git.mctaylors.ru/mctaylors/Cassette";
|
||||
|
||||
[Command("about")]
|
||||
[Description("Who developed this bot?")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> AboutCommandAsync()
|
||||
{
|
||||
var botResult = await userApi.GetCurrentUserAsync();
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
}
|
||||
|
||||
var description = new StringBuilder()
|
||||
.AppendLine("A Discord bot written by [@mctaylors](https://git.mctaylors.ru/mctaylors) " +
|
||||
"that plays your music in your voice channels.")
|
||||
.AppendLine("Powered by Remora.Discord & Lavalink4NET.");
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithUserAuthor($"About {bot.Username}", bot)
|
||||
.WithDescription(description.ToString())
|
||||
.WithColour(Color.Teal)
|
||||
.Build();
|
||||
|
||||
var button = new ButtonComponent(
|
||||
ButtonComponentStyle.Link,
|
||||
"Open Repository",
|
||||
URL: RepositoryUrl
|
||||
);
|
||||
|
||||
return await feedbackService.SendContextualEmbedResult(embed,
|
||||
new FeedbackMessageOptions(MessageComponents: new[]
|
||||
{
|
||||
new ActionRowComponent(new[] { button })
|
||||
}));
|
||||
}
|
||||
}
|
194
Cassette/Commands/ControlsCommandGroup.cs
Normal file
194
Cassette/Commands/ControlsCommandGroup.cs
Normal file
|
@ -0,0 +1,194 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Cassette.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Lavalink4NET;
|
||||
using Lavalink4NET.Rest.Entities.Tracks;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.Commands.Attributes;
|
||||
using Remora.Discord.Commands.Conditions;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Cassette.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="CommandGroup"/> that controls the music player.
|
||||
/// </summary>
|
||||
public sealed class ControlsCommandGroup(
|
||||
ICommandContext commandContext,
|
||||
IAudioService audioService,
|
||||
FeedbackService feedbackService) : CommandGroup
|
||||
{
|
||||
[Command("play")]
|
||||
[Description("Plays or adds the requested track to the queue")]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.Connect)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> PlayCommandAsync(
|
||||
[Description("URL or YouTube query")] string query)
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(commandContext, audioService, feedbackService, true);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
var track = await audioService.Tracks.LoadTrackAsync(query, TrackSearchMode.YouTube);
|
||||
if (track is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"Not found.", feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
await player.PlayAsync(track);
|
||||
|
||||
var message = new StringBuilder().Append($"Added {track.Display()}");
|
||||
|
||||
if (player.Queue.IsEmpty)
|
||||
{
|
||||
message.Append(" to begin playing");
|
||||
}
|
||||
|
||||
if (!player.Queue.IsEmpty)
|
||||
{
|
||||
message.Append(" to the queue at position ").Append(player.Queue.Count);
|
||||
}
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
message.ToString(), feedbackService.Theme.Success);
|
||||
}
|
||||
|
||||
[Command("pause")]
|
||||
[Description("Pauses the current track")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> PauseCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (player.CurrentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
if (player.IsPaused)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"Player is currently paused",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
await player.PauseAsync();
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"Paused {player.CurrentTrack.Display()}", feedbackService.Theme.Success);
|
||||
}
|
||||
|
||||
[Command("resume")]
|
||||
[Description("Resumes the current track")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ResumeCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (player.CurrentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
if (!player.IsPaused)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"Player is currently not paused.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
await player.ResumeAsync();
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"Resumed {player.CurrentTrack.Display()}", feedbackService.Theme.Success);
|
||||
}
|
||||
|
||||
[Command("seek")]
|
||||
[Description("Rewinds the current track to a specific position")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> SeekCommandAsync(
|
||||
[Description("Position to rewind")] TimeSpan position)
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (player.CurrentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
if (player.CurrentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
var track = player.CurrentTrack;
|
||||
|
||||
await player.SeekAsync(position);
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"{track.Display()} rewound to {position.ReadableDuration()}", feedbackService.Theme.Success);
|
||||
}
|
||||
|
||||
[Command("skip")]
|
||||
[Description("Skips the current track")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> SkipCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (player.CurrentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
var track = player.CurrentTrack;
|
||||
|
||||
await player.SkipAsync();
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"Skipped {track.Display()}", feedbackService.Theme.Success);
|
||||
}
|
||||
}
|
114
Cassette/Commands/InfoCommandGroup.cs
Normal file
114
Cassette/Commands/InfoCommandGroup.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using Cassette.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Lavalink4NET;
|
||||
using Lavalink4NET.Players.Queued;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
using Remora.Discord.Commands.Attributes;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Cassette.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="CommandGroup"/> that shows information about the music player.
|
||||
/// </summary>
|
||||
public sealed class InfoCommandGroup(
|
||||
ICommandContext commandContext,
|
||||
IAudioService audioService,
|
||||
FeedbackService feedbackService) : CommandGroup
|
||||
{
|
||||
[Command("nowplaying")]
|
||||
[Description("Shows the currently playing track")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> NowPlayingCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
var currentTrack = player.CurrentTrack;
|
||||
if (currentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"You're listening to {currentTrack.Display(true)}", feedbackService.Theme.Text);
|
||||
}
|
||||
|
||||
[Command("queue")]
|
||||
[Description("Shows the current track queue")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> QueueCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var getResult = await GetQueueAsync(player, builder);
|
||||
if (!getResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(getResult.Error);
|
||||
}
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(builder.ToString());
|
||||
}
|
||||
|
||||
private async Task<Result> GetQueueAsync(IQueuedLavalinkPlayer player, StringBuilder builder)
|
||||
{
|
||||
var queue = player.Queue;
|
||||
if (queue.IsEmpty)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing in queue right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
for (var i = 0; i < queue.Count; i++)
|
||||
{
|
||||
var track = queue[i].Track;
|
||||
if (track is null)
|
||||
{
|
||||
return Result.FromSuccess(); // dunno
|
||||
}
|
||||
|
||||
builder.Append($"{i + 1}. ").Append(track.Display(true));
|
||||
}
|
||||
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
[Command("volume")]
|
||||
[Description("Shows the current volume of the music player")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> VolumeCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"Current volume is {Markdown.Bold($"{player.Volume * 100}%")}", feedbackService.Theme.Text);
|
||||
}
|
||||
}
|
98
Cassette/Commands/TrustedCommandGroup.cs
Normal file
98
Cassette/Commands/TrustedCommandGroup.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
using System.ComponentModel;
|
||||
using Cassette.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Lavalink4NET;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.Commands.Attributes;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Cassette.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="CommandGroup"/> that controls the music player and requires higher permissions.
|
||||
/// <remarks>Trusted role support is not implemented yet.</remarks>
|
||||
/// </summary>
|
||||
public sealed class TrustedCommandGroup(
|
||||
ICommandContext commandContext,
|
||||
IAudioService audioService,
|
||||
FeedbackService feedbackService) : CommandGroup
|
||||
{
|
||||
private const DiscordPermission RequiredPermission = DiscordPermission.MuteMembers;
|
||||
|
||||
[Command("setvolume")]
|
||||
[Description("Adjusts the volume of the music player")]
|
||||
[DiscordDefaultMemberPermissions(RequiredPermission)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> SetVolumeCommandAsync(
|
||||
[MinValue(1)] [MaxValue(200)] float percentage)
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
var previousVolume = player.Volume / 100;
|
||||
await player.SetVolumeAsync(percentage / 100);
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"Volume changed {Markdown.Bold($"{previousVolume}%")} to {Markdown.Bold($"{percentage}%")}",
|
||||
feedbackService.Theme.Success);
|
||||
}
|
||||
|
||||
[Command("stop")]
|
||||
[Description("Stops the current track and clears the queue")]
|
||||
[DiscordDefaultMemberPermissions(RequiredPermission)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> StopCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
if (player.CurrentTrack is null)
|
||||
{
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"There's nothing playing right now.",
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
}
|
||||
|
||||
var track = player.CurrentTrack;
|
||||
|
||||
await player.StopAsync();
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
$"Stopped {track.Display()} and cleared the queue.", feedbackService.Theme.Success);
|
||||
}
|
||||
|
||||
[Command("disconnect")]
|
||||
[Description("Disconnects the bot and clears the queue")]
|
||||
[DiscordDefaultMemberPermissions(RequiredPermission)]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> DisconnectCommandAsync()
|
||||
{
|
||||
var player = await LavalinkPlayer.GetPlayerAsync(
|
||||
commandContext, audioService, feedbackService);
|
||||
if (player is null)
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
await player.DisconnectAsync();
|
||||
|
||||
return await feedbackService.SendContextualMessageResult(
|
||||
"Disconnected.", feedbackService.Theme.Success);
|
||||
}
|
||||
}
|
22
Cassette/Extensions/EmbedBuilderExtensions.cs
Normal file
22
Cassette/Extensions/EmbedBuilderExtensions.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
|
||||
namespace Cassette.Extensions;
|
||||
|
||||
public static class EmbedBuilderExtensions
|
||||
{
|
||||
public static EmbedBuilder WithUserAuthor(
|
||||
this EmbedBuilder builder, string name, IUser? user, string? url = null)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
return builder.WithAuthor(name, url);
|
||||
}
|
||||
|
||||
var iconResult = CDN.GetUserAvatarUrl(user);
|
||||
return builder.WithAuthor(name, url, !iconResult.IsDefined(out var iconUri)
|
||||
? CDN.GetDefaultUserAvatarUrl(user).Entity.AbsoluteUri
|
||||
: iconUri.AbsoluteUri);
|
||||
}
|
||||
}
|
30
Cassette/Extensions/FeedbackServiceExtensions.cs
Normal file
30
Cassette/Extensions/FeedbackServiceExtensions.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Drawing;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Commands.Feedback.Messages;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Cassette.Extensions;
|
||||
|
||||
public static class FeedbackServiceExtensions
|
||||
{
|
||||
public static async Task<Result> SendContextualMessageResult(
|
||||
this IFeedbackService feedbackService, string message, Color? color = null)
|
||||
{
|
||||
return (Result)await feedbackService.SendContextualMessageAsync(
|
||||
new FeedbackMessage(message, color ?? feedbackService.Theme.Secondary));
|
||||
}
|
||||
|
||||
public static async Task<Result> SendContextualEmbedResult(
|
||||
this IFeedbackService feedbackService, Result<Embed> embedResult,
|
||||
FeedbackMessageOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
if (!embedResult.IsDefined(out var embed))
|
||||
{
|
||||
return Result.FromError(embedResult);
|
||||
}
|
||||
|
||||
return (Result)await feedbackService.SendContextualEmbedAsync(embed, options, ct);
|
||||
// @Octol1ttle i didn't stole that i swear
|
||||
}
|
||||
}
|
37
Cassette/Extensions/LavalinkTrackExtensions.cs
Normal file
37
Cassette/Extensions/LavalinkTrackExtensions.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Lavalink4NET.Tracks;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
|
||||
namespace Cassette.Extensions;
|
||||
|
||||
public static class LavalinkTrackExtensions
|
||||
{
|
||||
public static string Display(this LavalinkTrack track, bool detailed = false)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var titleAppend = track.Title;
|
||||
if (track is { Title: "Unknown title", Uri: not null, Provider: StreamProvider.Http })
|
||||
{
|
||||
titleAppend = WebUtility.UrlDecode(track.Uri.Segments.Last());
|
||||
}
|
||||
|
||||
builder.Append(Markdown.Bold(Markdown.Sanitize(titleAppend)));
|
||||
|
||||
if (!detailed)
|
||||
{
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
if (track.Author is not "Unknown artist")
|
||||
{
|
||||
builder.Append(" by ").Append(Markdown.Sanitize(track.Author));
|
||||
}
|
||||
|
||||
var duration = track.Duration;
|
||||
builder.Append($" ({duration.ReadableDuration()})");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
9
Cassette/Extensions/TimeSpanExtensions.cs
Normal file
9
Cassette/Extensions/TimeSpanExtensions.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Cassette.Extensions;
|
||||
|
||||
public static class TimeSpanExtensions
|
||||
{
|
||||
public static string ReadableDuration(this TimeSpan duration)
|
||||
{
|
||||
return $"{duration.TotalMinutes:N0}:{duration.Seconds:00}";
|
||||
}
|
||||
}
|
40
Cassette/LavalinkPlayer.cs
Normal file
40
Cassette/LavalinkPlayer.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Cassette.Extensions;
|
||||
using Lavalink4NET;
|
||||
using Lavalink4NET.Players;
|
||||
using Lavalink4NET.Players.Queued;
|
||||
using Lavalink4NET.Remora.Discord;
|
||||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
|
||||
namespace Cassette;
|
||||
|
||||
public abstract class LavalinkPlayer
|
||||
{
|
||||
public static async ValueTask<QueuedLavalinkPlayer?> GetPlayerAsync(
|
||||
ICommandContext commandContext, IAudioService audioService,
|
||||
FeedbackService feedbackService, bool connectToVoiceChannel = false)
|
||||
{
|
||||
var retrieveOptions = new PlayerRetrieveOptions(
|
||||
ChannelBehavior: connectToVoiceChannel ? PlayerChannelBehavior.Join : PlayerChannelBehavior.None);
|
||||
|
||||
var result = await audioService.Players
|
||||
.RetrieveAsync(commandContext, PlayerFactory.Queued, retrieveOptions);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result.Player;
|
||||
}
|
||||
|
||||
var errorMessage = result.Status switch
|
||||
{
|
||||
PlayerRetrieveStatus.UserNotInVoiceChannel => "You are not connected to a voice channel.",
|
||||
PlayerRetrieveStatus.BotNotConnected => "The bot is currently not connected to a voice channel.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
|
||||
await feedbackService.SendContextualMessageResult(errorMessage,
|
||||
feedbackService.Theme.FaultOrDanger);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 mctaylors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
Loading…
Reference in a new issue