Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client-support/netcord): Add support for NetCord library #144

Merged
merged 13 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>ExampleBot</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="NetCord" Version="1.0.0-alpha.270" />
<PackageReference Include="NetCord.Hosting" Version="1.0.0-alpha.40" />
<PackageReference Include="NetCord.Hosting.Services" Version="1.0.0-alpha.49" />
<PackageReference Include="NetCord.Services" Version="1.0.0-alpha.171" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Lavalink4NET.NetCord\Lavalink4NET.NetCord.csproj" />
</ItemGroup>

</Project>
45 changes: 45 additions & 0 deletions samples/Lavalink4NET.Samples.NetCord/MusicModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace ExampleBot;

using Lavalink4NET;
using Lavalink4NET.NetCord;
using Lavalink4NET.Players;
using Lavalink4NET.Rest.Entities.Tracks;
using NetCord.Services.ApplicationCommands;

public class MusicModule(IAudioService audioService) : ApplicationCommandModule<SlashCommandContext>
{
[SlashCommand("play", "Plays a track!")]
public async Task<string> PlayAsync([SlashCommandParameter(Name = "query", Description = "The query to search for")] string query)
{
var retrieveOptions = new PlayerRetrieveOptions(ChannelBehavior: PlayerChannelBehavior.Join);

var result = await audioService.Players
.RetrieveAsync(Context, playerFactory: PlayerFactory.Queued, retrieveOptions);

if (!result.IsSuccess)
{
return GetErrorMessage(result.Status);
}

var player = result.Player;

var track = await audioService.Tracks
.LoadTrackAsync(query, TrackSearchMode.YouTube);

if (track is null)
{
return "No tracks found.";
}

await player.PlayAsync(track);

return $"Now playing: {track.Title}";
}

private static string GetErrorMessage(PlayerRetrieveStatus retrieveStatus) => retrieveStatus switch
{
PlayerRetrieveStatus.UserNotInVoiceChannel => "You are not connected to a voice channel.",
PlayerRetrieveStatus.BotNotConnected => "The bot is currently not connected.",
_ => "Unknown error.",
};
}
18 changes: 18 additions & 0 deletions samples/Lavalink4NET.Samples.NetCord/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Lavalink4NET.NetCord;
using Microsoft.Extensions.Hosting;
using NetCord;
using NetCord.Hosting.Gateway;
using NetCord.Hosting.Services;
using NetCord.Hosting.Services.ApplicationCommands;
using NetCord.Services.ApplicationCommands;

var builder = Host.CreateDefaultBuilder(args)
.UseDiscordGateway()
.UseLavalink()
.UseApplicationCommands<SlashCommandInteraction, SlashCommandContext>();

var host = builder.Build()
.AddModules(typeof(Program).Assembly)
.UseGatewayEventHandlers();

host.Run();
62 changes: 62 additions & 0 deletions src/Lavalink4NET.NetCord/DiscordClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Lavalink4NET.NetCord;

using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using global::NetCord.Gateway;
using Lavalink4NET.Clients;
using Lavalink4NET.Clients.Events;
using Lavalink4NET.Events;

public sealed class DiscordClientWrapper : IDiscordClientWrapper
{
private readonly IDiscordClientWrapper _client;

public DiscordClientWrapper(GatewayClient client)
{
ArgumentNullException.ThrowIfNull(client);

_client = new GatewayClientWrapper(client);
}

public DiscordClientWrapper(ShardedGatewayClient client)
{
ArgumentNullException.ThrowIfNull(client);

_client = new ShardedGatewayClientWrapper(client);
}

public event AsyncEventHandler<VoiceServerUpdatedEventArgs>? VoiceServerUpdated
{
add => _client.VoiceServerUpdated += value;
remove => _client.VoiceServerUpdated -= value;
}

public event AsyncEventHandler<VoiceStateUpdatedEventArgs>? VoiceStateUpdated
{
add => _client.VoiceStateUpdated += value;
remove => _client.VoiceStateUpdated -= value;
}

public ValueTask<ImmutableArray<ulong>> GetChannelUsersAsync(ulong guildId, ulong voiceChannelId, bool includeBots = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return _client.GetChannelUsersAsync(guildId, voiceChannelId, includeBots, cancellationToken);
}

public ValueTask SendVoiceUpdateAsync(ulong guildId, ulong? voiceChannelId, bool selfDeaf = false, bool selfMute = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return _client.SendVoiceUpdateAsync(guildId, voiceChannelId, selfDeaf, selfMute, cancellationToken);
}

public ValueTask<ClientInformation> WaitForReadyAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

return _client.WaitForReadyAsync(cancellationToken);
}
}
49 changes: 49 additions & 0 deletions src/Lavalink4NET.NetCord/GatewayClientWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Lavalink4NET.NetCord;

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using global::NetCord.Gateway;
using Lavalink4NET.Clients;

internal sealed class GatewayClientWrapper : GatewayClientWrapperBase, IDiscordClientWrapper, IDisposable
{
private readonly GatewayClient _client;

public GatewayClientWrapper(GatewayClient client)
{
ArgumentNullException.ThrowIfNull(client);

_client = client;

_client.VoiceStateUpdate += HandleVoiceStateUpdateAsync;
_client.VoiceServerUpdate += HandleVoiceServerUpdateAsync;
}

public void Dispose()
{
_client.VoiceStateUpdate -= HandleVoiceStateUpdateAsync;
_client.VoiceServerUpdate -= HandleVoiceServerUpdateAsync;
}

public override async ValueTask<ClientInformation> WaitForReadyAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

await _client.ReadyAsync
.WaitAsync(cancellationToken)
.ConfigureAwait(false);

var shardCount = _client.Shard?.Count ?? 1;

return new ClientInformation("NetCord", _client.Id, shardCount);
}

protected override GatewayClient GetClient(ulong guildId) => _client;

protected override bool TryGetGuild(ulong guildId, [MaybeNullWhen(false)] out Guild guild)
{
return _client.Cache.Guilds.TryGetValue(guildId, out guild);
}
}
104 changes: 104 additions & 0 deletions src/Lavalink4NET.NetCord/GatewayClientWrapperBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace Lavalink4NET.NetCord;

using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using global::NetCord.Gateway;
using Lavalink4NET.Clients;
using Lavalink4NET.Clients.Events;
using Lavalink4NET.Events;

internal abstract class GatewayClientWrapperBase : IDiscordClientWrapper
{
public event AsyncEventHandler<VoiceServerUpdatedEventArgs>? VoiceServerUpdated;

public event AsyncEventHandler<VoiceStateUpdatedEventArgs>? VoiceStateUpdated;

public ValueTask<ImmutableArray<ulong>> GetChannelUsersAsync(ulong guildId, ulong voiceChannelId, bool includeBots = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (!TryGetGuild(guildId, out var guild))
{
return new ValueTask<ImmutableArray<ulong>>([]);
}

var currentUserId = GetClient(guildId).Id;

var voiceStates = guild.VoiceStates
.Where(x => x.Value.ChannelId == voiceChannelId)
.Where(x => x.Value.UserId != currentUserId);

if (!includeBots)
{
voiceStates = voiceStates.Where(x => x.Value.User is not { IsBot: true, });
}

var userIds = voiceStates.Select(x => x.Value.UserId).ToImmutableArray();
return new ValueTask<ImmutableArray<ulong>>(userIds);
}

public async ValueTask SendVoiceUpdateAsync(ulong guildId, ulong? voiceChannelId, bool selfDeaf = false, bool selfMute = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

var voiceStateProperties = new VoiceStateProperties(guildId, voiceChannelId) { SelfDeaf = selfDeaf, SelfMute = selfMute, };

await GetClient(guildId)
.UpdateVoiceStateAsync(voiceStateProperties)
.ConfigureAwait(false);
}

protected abstract bool TryGetGuild(ulong guildId, [MaybeNullWhen(false)] out Guild guild);

protected abstract GatewayClient GetClient(ulong guildId);

protected ValueTask HandleVoiceServerUpdateAsync(VoiceServerUpdateEventArgs eventArgs)
{
ArgumentNullException.ThrowIfNull(eventArgs);

if (eventArgs.Endpoint is null)
{
return default;
}

var voiceServerUpdatedEventArgs = new VoiceServerUpdatedEventArgs(
guildId: eventArgs.GuildId,
voiceServer: new VoiceServer(eventArgs.Token, eventArgs.Endpoint));

return VoiceServerUpdated.InvokeAsync(this, voiceServerUpdatedEventArgs);
}

protected async ValueTask HandleVoiceStateUpdateAsync(global::NetCord.Gateway.VoiceState eventArgs)
{
ArgumentNullException.ThrowIfNull(eventArgs);

// Retrieve previous voice state from cache
var previousVoiceState = TryGetGuild(eventArgs.GuildId, out var guild)
&& guild.VoiceStates.TryGetValue(eventArgs.UserId, out var previousVoiceStateData)
? new Clients.VoiceState(VoiceChannelId: previousVoiceStateData.ChannelId, SessionId: previousVoiceStateData.SessionId)
: default;

var currentUserId = GetClient(eventArgs.GuildId).Id;

var updatedVoiceState = new Clients.VoiceState(
VoiceChannelId: eventArgs.ChannelId,
SessionId: eventArgs.SessionId);

var voiceStateUpdatedEventArgs = new VoiceStateUpdatedEventArgs(
eventArgs.GuildId,
eventArgs.UserId,
eventArgs.UserId == currentUserId,
updatedVoiceState,
previousVoiceState);

await VoiceStateUpdated
.InvokeAsync(this, voiceStateUpdatedEventArgs)
.ConfigureAwait(false);
}

public abstract ValueTask<ClientInformation> WaitForReadyAsync(CancellationToken cancellationToken = default);
}
14 changes: 14 additions & 0 deletions src/Lavalink4NET.NetCord/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Lavalink4NET.NetCord;

using System;
using Microsoft.Extensions.Hosting;

public static class HostBuilderExtensions
{
public static IHostBuilder UseLavalink(this IHostBuilder hostBuilder)
{
ArgumentNullException.ThrowIfNull(hostBuilder);

return hostBuilder.ConfigureServices(static (_, services) => services.AddLavalink());
}
}
23 changes: 23 additions & 0 deletions src/Lavalink4NET.NetCord/Lavalink4NET.NetCord.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>

<!-- Package Description -->
<Description>High performance Lavalink wrapper for .NET | Add powerful audio playback to your NetCord-based applications with this integration for Lavalink4NET. Suitable for end users developing with NetCord.</Description>
<PackageTags>lavalink,lavalink-wrapper,discord,discord-music,discord-music-bot,netcord</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NetCord" Version="1.0.0-alpha.270" />
<PackageReference Include="NetCord.Services" Version="1.0.0-alpha.171" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Lavalink4NET\Lavalink4NET.csproj" />
</ItemGroup>

<Import Project="../Lavalink4NET.targets" />
</Project>
Loading
Loading