Compare commits

..

21 Commits

Author SHA1 Message Date
Myx
34375f52bd Small fix 2024-08-11 16:03:42 +02:00
05b7324ecc Update readme.md
Test

Migrate from Victoria
2024-08-11 15:45:24 +02:00
d72676c7e0 Improve chat (#2)
Co-authored-by: Myx <info@azaaxin.com>
2024-08-11 01:18:29 +02:00
b30d47e351 Add LLM info into main Readme 2024-06-19 12:33:19 +02:00
3ce0df7eaf Added Ollama and Ollama Web UI 2024-06-19 12:21:05 +02:00
e88e67f913 Fix broken appsettings file
Invalid json
2024-06-19 12:12:05 +02:00
5053553182 Update dotnet.yml 2024-06-02 19:12:45 +02:00
327ccc9675 Update dotnet.yml 2024-06-02 18:57:56 +02:00
cbc99c2773 Update dotnet.yml 2024-06-02 18:55:14 +02:00
d56215f685 Update README.md 2024-06-02 18:53:02 +02:00
967bee923a Update dotnet.yml 2024-06-02 18:51:38 +02:00
f8e6854569 Create readme.md 2024-06-02 00:04:14 +02:00
03150a3d04 Update README.md 2024-06-01 23:53:15 +02:00
32b6e09336 Update README.md 2024-06-01 23:42:17 +02:00
e3df4505fe Update README.md 2024-06-01 23:37:47 +02:00
3daf18e053 Update README.md 2024-06-01 23:36:07 +02:00
54c5c68ba6 Update README.md 2024-06-01 23:35:40 +02:00
e16ff9cfaf Update README.md 2024-06-01 23:35:16 +02:00
a1d20fd732 Add chat functionality (#1)
* Working chatbot

* Clean

* Working LLM chatbot

---------

Co-authored-by: Myx <info@azaaxin.com>
2024-06-01 23:22:47 +02:00
3d7655a902 Update readme.md 2024-04-15 00:00:38 +02:00
8ddcf31da7 Update readme.md 2024-04-14 23:59:03 +02:00
33 changed files with 626 additions and 359 deletions

View File

@@ -7,10 +7,13 @@ on:
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0 # required for github-action-get-previous-tag
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
@@ -27,15 +30,19 @@ jobs:
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
- name: Zip the build - name: Zip the build
run: 7z a -tzip ./out/Bot.zip ./out/* run: 7z a -tzip ./out/Lunaris.zip ./out/*
- name: Get the tag name - name: Get previous tag
id: get_tag id: previoustag
run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}" uses: 'WyriHaximus/github-action-get-previous-tag@v1'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get the version - name: Get next minor version
id: get_version id: semver
run: echo "::set-output name=version::$(date +%s).${{ github.run_id }}" uses: 'WyriHaximus/github-action-next-semvers@v1'
with:
version: ${{ steps.previoustag.outputs.tag }}
- name: Create Release - name: Create Release
id: create_release id: create_release
@@ -43,8 +50,8 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with: with:
tag_name: ${{ steps.get_version.outputs.version }} tag_name: ${{ steps.semver.outputs.patch }}
release_name: Release v${{ steps.get_version.outputs.version }} release_name: Release ${{ steps.semver.outputs.patch }}
draft: false draft: false
prerelease: false prerelease: false
@@ -55,6 +62,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./out/Bot.zip asset_path: ./out/Lunaris.zip
asset_name: Bot.zip asset_name: Lunaris.zip
asset_content_type: application/zip asset_content_type: application/zip

View File

@@ -0,0 +1,68 @@
using System.Text;
using Discord.WebSocket;
using MediatR;
using Microsoft.Extensions.Options;
using OllamaSharp;
namespace Lunaris2.Handler.ChatCommand
{
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
public class ChatHandler : IRequestHandler<ChatCommand>
{
private readonly OllamaApiClient _ollama;
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
private readonly ChatSettings _chatSettings;
public ChatHandler(IOptions<ChatSettings> chatSettings)
{
_chatSettings = chatSettings.Value;
var uri = new Uri(chatSettings.Value.Url);
_ollama = new OllamaApiClient(uri)
{
SelectedModel = chatSettings.Value.Model
};
}
public async Task Handle(ChatCommand command, CancellationToken cancellationToken)
{
var channelId = command.Message.Channel.Id;
_chatContexts.TryAdd(channelId, null);
var userMessage = command.FilteredMessage;
var randomPersonality = _chatSettings.Personalities[new Random().Next(_chatSettings.Personalities.Count)];
userMessage = $"{randomPersonality.Instruction} {userMessage}";
using var setTyping = command.Message.Channel.EnterTypingState();
if (string.IsNullOrWhiteSpace(userMessage))
{
await command.Message.Channel.SendMessageAsync("Am I expected to read your mind?");
setTyping.Dispose();
return;
}
var response = await GenerateResponse(userMessage, channelId, cancellationToken);
await command.Message.Channel.SendMessageAsync(response);
setTyping.Dispose();
}
private async Task<string> GenerateResponse(string userMessage, ulong channelId, CancellationToken cancellationToken)
{
var response = new StringBuilder();
if (_chatContexts[channelId] == null)
{
_chatContexts[channelId] = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? ""));
}
await _chatContexts[channelId].Send(userMessage, cancellationToken);
return response.ToString();
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Lunaris2.Handler.ChatCommand;
public class ChatSettings
{
public string Url { get; set; }
public string Model { get; set; }
public List<Personality> Personalities { get; set; }
}
public class Personality
{
public string Name { get; set; }
public string Instruction { get; set; }
}

View File

@@ -0,0 +1,8 @@
## Ollama - Large Language Model Chat - Handler
This handler "owns" the logic for accessing the ollama api, which runs the transformer model.
> How to get started with a local chat bot see: [Run LLMs Locally using Ollama](https://marccodess.medium.com/run-llms-locally-using-ollama-8f04dd9b14f9)
Assuming you are on the same network as the Ollama server you should configure it to be accessible to other machines on the network, however this is only required if you aren't running it from localhost relative to the bot.
See: [How do I configure Ollama server?](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)

View File

@@ -1,14 +0,0 @@
using Discord.WebSocket;
using MediatR;
namespace Lunaris2.Handler.GoodByeCommand;
public record GoodbyeCommand(SocketSlashCommand Message) : IRequest;
public class GoodbyeHandler : IRequestHandler<GoodbyeCommand>
{
public async Task Handle(GoodbyeCommand message, CancellationToken cancellationToken)
{
await message.Message.RespondAsync($"Goodbye, {message.Message.User.Username}! :c");
}
}

View File

@@ -1,18 +0,0 @@
using Discord.WebSocket;
using Lunaris2.SlashCommand;
using MediatR;
using Newtonsoft.Json;
namespace Lunaris2.Handler.HelloCommand;
public record HelloCommand(SocketSlashCommand Message) : IRequest;
public class HelloHandler : IRequestHandler<HelloCommand>
{
public async Task Handle(HelloCommand message, CancellationToken cancellationToken)
{
Console.WriteLine(JsonConvert.SerializeObject(Command.GetAllCommands()));
await message.Message.RespondAsync($"Hello, {message.Message.User.Username}!");
}
}

View File

@@ -1,34 +1,37 @@
using Lunaris2.Handler.GoodByeCommand; using System.Text.RegularExpressions;
using Lunaris2.Handler.MusicPlayer.JoinCommand; using Discord.WebSocket;
using Lunaris2.Handler.MusicPlayer.PlayCommand;
using Lunaris2.Handler.MusicPlayer.SkipCommand;
using Lunaris2.Notification; using Lunaris2.Notification;
using Lunaris2.SlashCommand;
using MediatR; using MediatR;
namespace Lunaris2.Handler; namespace Lunaris2.Handler;
public class MessageReceivedHandler(ISender mediator) : INotificationHandler<MessageReceivedNotification> public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotification>
{ {
private readonly DiscordSocketClient _client;
private readonly ISender _mediatir;
public MessageReceivedHandler(DiscordSocketClient client, ISender mediatir)
{
_client = client;
_mediatir = mediatir;
}
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
{ {
switch (notification.Message.CommandName) await BotMentioned(notification, cancellationToken);
}
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
{ {
case Command.Hello.Name: if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken); {
break; // The bot was mentioned
case Command.Goodbye.Name: const string pattern = "<.*?>";
await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken); const string replacement = "";
break; var regex = new Regex(pattern);
case Command.Join.Name: var messageContent = regex.Replace(notification.Message.Content, replacement);
await mediator.Send(new JoinCommand(notification.Message), cancellationToken);
break; await _mediatir.Send(new ChatCommand.ChatCommand(notification.Message, messageContent), cancellationToken);
case Command.Play.Name:
await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
break;
case Command.Skip.Name:
await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
break;
} }
} }
} }

View File

@@ -0,0 +1,22 @@
using Discord.WebSocket;
using Lavalink4NET;
using MediatR;
namespace Lunaris2.Handler.MusicPlayer.DisconnectCommand;
public record DisconnectCommand(SocketSlashCommand Message) : IRequest;
public class DisconnectHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<DisconnectCommand>
{
public async Task Handle(DisconnectCommand command, CancellationToken cancellationToken)
{
var context = command.Message;
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
if (player is null)
return;
await player.DisconnectAsync(cancellationToken).ConfigureAwait(false);
await context.RespondAsync("Disconnected.").ConfigureAwait(false);
}
}

View File

@@ -1,11 +1,46 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Victoria.Node; using Lavalink4NET;
using Lavalink4NET.Players;
using Lavalink4NET.Players.Queued;
using Microsoft.Extensions.Options;
namespace Lunaris2.Handler.MusicPlayer; namespace Lunaris2.Handler.MusicPlayer;
public static class Extensions public static class Extensions
{ {
public static async ValueTask<QueuedLavalinkPlayer?> GetPlayerAsync(
this IAudioService audioService,
DiscordSocketClient client,
SocketSlashCommand context,
bool connectToVoiceChannel = true)
{
ArgumentNullException.ThrowIfNull(context);
var retrieveOptions = new PlayerRetrieveOptions(
ChannelBehavior: connectToVoiceChannel ? PlayerChannelBehavior.Join : PlayerChannelBehavior.None);
var playerOptions = new QueuedLavalinkPlayerOptions { HistoryCapacity = 10000 };
var result = await audioService.Players
.RetrieveAsync(context.GetGuild(client).Id, context.GetGuild(client).GetUser(context.User.Id).VoiceChannel.Id, playerFactory: PlayerFactory.Queued, Options.Create(playerOptions), retrieveOptions)
.ConfigureAwait(false);
if (!result.IsSuccess)
{
var errorMessage = result.Status switch
{
PlayerRetrieveStatus.UserNotInVoiceChannel => "You are not connected to a voice channel.",
PlayerRetrieveStatus.BotNotConnected => "The bot is currently not connected.",
_ => "Unknown error.",
};
return null;
}
return result.Player;
}
public static SocketGuild GetGuild(this SocketSlashCommand message, DiscordSocketClient client) public static SocketGuild GetGuild(this SocketSlashCommand message, DiscordSocketClient client)
{ {
if (message.GuildId == null) if (message.GuildId == null)
@@ -33,24 +68,6 @@ public static class Extensions
await message.RespondAsync(content, ephemeral: true); await message.RespondAsync(content, ephemeral: true);
} }
public static async Task EnsureConnected(this LavaNode lavaNode)
{
if(!lavaNode.IsConnected)
await lavaNode.ConnectAsync();
}
public static async Task JoinVoiceChannel(this SocketSlashCommand context, LavaNode lavaNode)
{
try
{
var textChannel = context.Channel as ITextChannel;
await lavaNode.JoinAsync(context.GetVoiceState().VoiceChannel, textChannel);
await context.RespondAsync($"Joined {context.GetVoiceState().VoiceChannel.Name}!");
}
catch (Exception exception) {
Console.WriteLine(exception);
}
}
public static string GetOptionValueByName(this SocketSlashCommand command, string optionName) public static string GetOptionValueByName(this SocketSlashCommand command, string optionName)
{ {

View File

@@ -1,33 +0,0 @@
using Discord.WebSocket;
using MediatR;
using Victoria.Node;
namespace Lunaris2.Handler.MusicPlayer.JoinCommand;
public record JoinCommand(SocketSlashCommand Message) : IRequest;
public class JoinHandler : IRequestHandler<JoinCommand>
{
private readonly LavaNode _lavaNode;
private readonly DiscordSocketClient _client;
public JoinHandler(LavaNode lavaNode, DiscordSocketClient client)
{
_lavaNode = lavaNode;
_client = client;
}
public async Task Handle(JoinCommand command, CancellationToken cancellationToken)
{
var context = command.Message;
await _lavaNode.EnsureConnected();
if (_lavaNode.HasPlayer(context.GetGuild(_client))) {
await context.RespondAsync("I'm already connected to a voice channel!");
return;
}
await context.JoinVoiceChannel(_lavaNode);
}
}

View File

@@ -1,50 +1,65 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.Handler.MusicPlayer;
namespace Lunaris2.Handler.MusicPlayer;
public static class MessageModule public static class MessageModule
{ {
private static Dictionary<ulong, List<ulong>> guildMessageIds = new Dictionary<ulong, List<ulong>>(); private static readonly Dictionary<ulong, List<ulong>> GuildMessageIds = new();
public static async Task SendMessageAsync(this SocketSlashCommand context, string message, DiscordSocketClient client) public static async Task SendMessageAsync(this SocketSlashCommand context, string message, DiscordSocketClient client)
{
try
{ {
var guildId = await StoreForRemoval(context, client); var guildId = await StoreForRemoval(context, client);
await context.RespondAsync(message); var sentMessage = await context.FollowupAsync(message);
var sentMessage = await context.GetOriginalResponseAsync(); GuildMessageIds[guildId].Add(sentMessage.Id);
}
guildMessageIds[guildId].Add(sentMessage.Id); catch (Exception e)
{
Console.WriteLine(e);
throw;
}
} }
public static async Task SendMessageAsync(this SocketSlashCommand context, Embed message, DiscordSocketClient client) public static async Task SendMessageAsync(this SocketSlashCommand context, Embed message, DiscordSocketClient client)
{
try
{ {
var guildId = await StoreForRemoval(context, client); var guildId = await StoreForRemoval(context, client);
await context.RespondAsync(embed: message); var sentMessage = await context.FollowupAsync(embed: message);
GuildMessageIds[guildId].Add(sentMessage.Id);
var sentMessage = await context.GetOriginalResponseAsync(); }
catch (Exception e)
guildMessageIds[guildId].Add(sentMessage.Id); {
Console.WriteLine(e);
throw;
}
} }
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client) private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
{ {
var guildId = context.GetGuild(client).Id; var guildId = context.GetGuild(client).Id;
if (guildMessageIds.ContainsKey(guildId)) if (GuildMessageIds.TryGetValue(guildId, out var value))
{ {
foreach (var messageId in guildMessageIds[guildId]) if (value.Count <= 0)
return guildId;
foreach (var messageId in value)
{ {
var messageToDelete = await context.Channel.GetMessageAsync(messageId); var messageToDelete = await context.Channel.GetMessageAsync(messageId);
if (messageToDelete != null) if (messageToDelete != null)
await messageToDelete.DeleteAsync(); await messageToDelete.DeleteAsync();
} }
guildMessageIds[guildId].Clear(); value.Clear();
} }
else else
{ {
guildMessageIds.Add(guildId, []); GuildMessageIds.Add(guildId, new List<ulong>());
} }
return guildId; return guildId;

View File

@@ -1,7 +1,6 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Victoria; using Lavalink4NET.Tracks;
using Victoria.Player;
namespace Lunaris2.Handler.MusicPlayer; namespace Lunaris2.Handler.MusicPlayer;
@@ -12,32 +11,29 @@ public class MusicEmbed
string title, string title,
string length, string length,
string artist, string artist,
string queuedBy, string queuedBy)
string? nextInQueue)
{ {
return new EmbedBuilder() return new EmbedBuilder()
.WithAuthor("Lunaris", "https://media.tenor.com/GqAwMt01UXgAAAAi/cd.gif") .WithAuthor("Lunaris", "https://media.tenor.com/GqAwMt01UXgAAAAi/cd.gif")
.WithTitle(title) .WithTitle(title)
.WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}\nNext in queue: {nextInQueue}") .WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}")
.WithColor(Color.Magenta) .WithColor(Color.Magenta)
.WithThumbnailUrl(imageUrl) .WithThumbnailUrl(imageUrl)
.Build(); .Build();
} }
public async Task NowPlayingEmbed( public async Task NowPlayingEmbed(
LavaPlayer<LavaTrack> player, LavalinkTrack player,
SocketSlashCommand context, SocketSlashCommand context,
DiscordSocketClient client) DiscordSocketClient client)
{ {
var artwork = await player.Track.FetchArtworkAsync(); var artwork = player.ArtworkUri;
var getNextTrack = player.Vueue.Count > 1 ? player.Vueue.ToArray()[1].Title : "No songs in queue.";
var embed = SendMusicEmbed( var embed = SendMusicEmbed(
artwork, artwork.ToString(),
player.Track.Title, player.Title,
player.Track.Duration.ToString(), player.Duration.ToString(),
player.Track.Author, player.Author,
context.User.Username, context.User.Username);
getNextTrack);
await context.SendMessageAsync(embed, client); await context.SendMessageAsync(embed, client);
} }

View File

@@ -0,0 +1,31 @@
using Discord.WebSocket;
using Lavalink4NET;
using Lavalink4NET.Players;
using MediatR;
namespace Lunaris2.Handler.MusicPlayer.PauseCommand;
public record PauseCommand(SocketSlashCommand Message) : IRequest;
public class PauseHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<PauseCommand>
{
public async Task Handle(PauseCommand command, CancellationToken cancellationToken)
{
var context = command.Message;
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
if (player is null)
{
return;
}
if (player.State is PlayerState.Paused)
{
await context.SendMessageAsync("Player is already paused.", client);
return;
}
await player.PauseAsync(cancellationToken);
await context.SendMessageAsync("Paused.", client);
}
}

View File

@@ -1,12 +1,10 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.SlashCommand; using Lunaris2.SlashCommand;
using MediatR; using MediatR;
using Victoria.Node; using Lavalink4NET;
using Victoria.Node.EventArgs; using Lavalink4NET.Events.Players;
using Victoria.Player; using Lavalink4NET.Players.Queued;
using Victoria.Responses.Search; using Lavalink4NET.Rest.Entities.Tracks;
namespace Lunaris2.Handler.MusicPlayer.PlayCommand; namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
@@ -15,105 +13,82 @@ public record PlayCommand(SocketSlashCommand Message) : IRequest;
public class PlayHandler : IRequestHandler<PlayCommand> public class PlayHandler : IRequestHandler<PlayCommand>
{ {
private readonly MusicEmbed _musicEmbed; private readonly MusicEmbed _musicEmbed;
private readonly LavaNode _lavaNode;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IAudioService _audioService;
private SocketSlashCommand _context; private SocketSlashCommand _context;
public PlayHandler( public PlayHandler(
LavaNode lavaNode,
DiscordSocketClient client, DiscordSocketClient client,
MusicEmbed musicEmbed) MusicEmbed musicEmbed,
IAudioService audioService)
{ {
_lavaNode = lavaNode;
_client = client; _client = client;
_musicEmbed = musicEmbed; _musicEmbed = musicEmbed;
_audioService = audioService;
_audioService.TrackStarted += OnTrackStarted;
}
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
{
var player = sender as QueuedLavalinkPlayer;
var track = player?.CurrentTrack;
if (track != null)
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
} }
[Command(RunMode = RunMode.Async)]
public async Task Handle(PlayCommand command, CancellationToken cancellationToken) public async Task Handle(PlayCommand command, CancellationToken cancellationToken)
{ {
_context = command.Message; await _audioService.StartAsync(cancellationToken);
var context = command.Message;
_context = context;
await _lavaNode.EnsureConnected(); var searchQuery = context.GetOptionValueByName(Option.Input);
var songName = _context.GetOptionValueByName(Option.Input); if (string.IsNullOrWhiteSpace(searchQuery)) {
await context.SendMessageAsync("Please provide search terms.", _client);
if (string.IsNullOrWhiteSpace(songName)) {
await _context.RespondAsync("Please provide search terms.");
return; return;
} }
var player = await GetPlayer(); var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
if (player == null) if (player is null)
return; return;
var searchResponse = await _lavaNode.SearchAsync( var trackLoadOptions = new TrackLoadOptions
Uri.IsWellFormedUriString(songName, UriKind.Absolute)
? SearchType.Direct
: SearchType.YouTube, songName);
if (!await SearchResponse(searchResponse, player, songName))
return;
await PlayTrack(player);
await _musicEmbed.NowPlayingEmbed(player, _context, _client);
_lavaNode.OnTrackEnd += OnTrackEnd;
}
private async Task OnTrackEnd(TrackEndEventArg<LavaPlayer<LavaTrack>, LavaTrack> arg)
{ {
var player = arg.Player; SearchMode = TrackSearchMode.YouTube,
if (!player.Vueue.TryDequeue(out var nextTrack)) };
return;
await player.PlayAsync(nextTrack); var track = await _audioService.Tracks
.LoadTrackAsync(
searchQuery,
trackLoadOptions,
cancellationToken: cancellationToken);
await _musicEmbed.NowPlayingEmbed(player, _context, _client); if (track is null)
} await context.SendMessageAsync("😖 No results.", _client);
private static async Task PlayTrack(LavaPlayer<LavaTrack> player) if (player.CurrentTrack is null)
{ {
if (player.PlayerState is PlayerState.Playing or PlayerState.Paused) { await player
return; .PlayAsync(track, cancellationToken: cancellationToken)
} .ConfigureAwait(false);
player.Vueue.TryDequeue(out var lavaTrack); await _musicEmbed.NowPlayingEmbed(track, context, _client);
await player.PlayAsync(lavaTrack);
} }
else
private async Task<LavaPlayer<LavaTrack>?> GetPlayer()
{ {
var voiceState = _context.User as IVoiceState; if (track != null)
if (voiceState?.VoiceChannel != null)
return await _lavaNode.JoinAsync(voiceState.VoiceChannel, _context.Channel as ITextChannel);
await _context.RespondAsync("You must be connected to a voice channel!");
return null;
}
private async Task<bool> SearchResponse(
SearchResponse searchResponse, LavaPlayer<LavaTrack> player,
string songName)
{ {
if (searchResponse.Status is SearchStatus.LoadFailed or SearchStatus.NoMatches) { var queueTracks = new[] { new TrackQueueItem(track) };
await _context.RespondAsync($"I wasn't able to find anything for `{songName}`."); await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
return false; await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
}
else
{
await context.SendMessageAsync($"Couldn't read song information", _client);
}
} }
if (!string.IsNullOrWhiteSpace(searchResponse.Playlist.Name)) {
player.Vueue.Enqueue(searchResponse.Tracks);
await _context.RespondAsync($"Enqueued {searchResponse.Tracks.Count} songs.");
}
else {
var track = searchResponse.Tracks.FirstOrDefault()!;
player.Vueue.Enqueue(track);
}
return true;
} }
} }

View File

@@ -8,6 +8,8 @@ flowchart TD
PlayTrack --> NowPlayingEmbed PlayTrack --> NowPlayingEmbed
``` ```
## Steps in the code
| Name | Description | | Name | Description |
|--|--| |--|--|
| PlayHandler | Holds the logic for playing songs | | PlayHandler | Holds the logic for playing songs |
@@ -19,13 +21,13 @@ flowchart TD
There is also OnTrackEnd, when it get called an attempt is made to play the next song in queue. There is also OnTrackEnd, when it get called an attempt is made to play the next song in queue.
Short explaination for some of the variables used: ## Short explaination for some of the variables used:
| Variable | Type | Description | | Variable | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `_lavaNode` | `LavaNode` | An instance of the `LavaNode` class, used to interact with the LavaLink server for playing music in Discord voice channels. | | `_lavaNode` | `LavaNode` | An instance of the `LavaNode` class, used to interact with the LavaLink server for playing music in Discord voice channels. |
| `_client` | `DiscordSocketClient` | An instance of the `DiscordSocketClient` class, used to interact with the Discord API for sending messages, joining voice channels, etc. | | `_client` | `DiscordSocketClient` | An instance of the `DiscordSocketClient` class, used to interact with the Discord API for sending messages, joining voice channels, etc. |
| `_musicEmbed` | `MusicEmbed` | An instance of a custom `MusicEmbed` class, likely used to create and send rich embed messages related to the music player's current status. | | `_musicEmbed` | `MusicEmbed` | An instance of a custom `MusicEmbed` class, used to create and send embed messages related to the music player's current status. |
| `context` | `SocketSlashCommand` | An instance of the `SocketSlashCommand` class, representing a slash command received from Discord. Used to get information about the command and to respond to it. | | `context` | `SocketSlashCommand` | An instance of the `SocketSlashCommand` class, representing a slash command received from Discord. Used to get information about the command and to respond to it. |
| `player` | `LavaPlayer` | An instance of the `LavaPlayer` class, representing a music player connected to a specific voice channel. Used to play, pause, skip, and queue tracks. | | `player` | `LavaPlayer` | An instance of the `LavaPlayer` class, representing a music player connected to a specific voice channel. Used to play, pause, skip, and queue tracks. |
| `guildMessageIds` | `Dictionary<ulong, List<ulong>>` | A dictionary that maps guild IDs to lists of message IDs. Used to keep track of messages sent by the bot in each guild, allowing the bot to delete its old messages when it sends new ones. | | `guildMessageIds` | `Dictionary<ulong, List<ulong>>` | A dictionary that maps guild IDs to lists of message IDs. Used to keep track of messages sent by the bot in each guild, allowing the bot to delete its old messages when it sends new ones. |

View File

@@ -0,0 +1,29 @@
using Discord.WebSocket;
using Lavalink4NET;
using Lavalink4NET.Players;
using MediatR;
namespace Lunaris2.Handler.MusicPlayer.ResumeCommand;
public record ResumeCommand(SocketSlashCommand Message) : IRequest;
public class ResumeHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<ResumeCommand>
{
public async Task Handle(ResumeCommand command, CancellationToken cancellationToken)
{
var context = command.Message;
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
if (player is null)
return;
if (player.State is not PlayerState.Paused)
{
await context.SendMessageAsync("Player is not paused.", client);
return;
}
await player.ResumeAsync(cancellationToken);
await context.SendMessageAsync("Resumed.", client);
}
}

View File

@@ -1,48 +1,34 @@
using Discord.WebSocket; using Discord.WebSocket;
using Lavalink4NET;
using MediatR; using MediatR;
using Victoria.Node;
using Victoria.Player;
namespace Lunaris2.Handler.MusicPlayer.SkipCommand; namespace Lunaris2.Handler.MusicPlayer.SkipCommand;
public record SkipCommand(SocketSlashCommand Message) : IRequest; public record SkipCommand(SocketSlashCommand Message) : IRequest;
public class SkipHandler : IRequestHandler<SkipCommand> public class SkipHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<SkipCommand>
{ {
private readonly LavaNode _lavaNode; public async Task Handle(SkipCommand command, CancellationToken cancellationToken)
private readonly DiscordSocketClient _client;
private readonly MusicEmbed _musicEmbed;
public SkipHandler(LavaNode lavaNode, DiscordSocketClient client, MusicEmbed musicEmbed)
{ {
_lavaNode = lavaNode; var context = command.Message;
_client = client; var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
_musicEmbed = musicEmbed;
}
public async Task Handle(SkipCommand message, CancellationToken cancellationToken) if (player is null)
return;
if (player.CurrentItem is null)
{ {
var context = message.Message; await context.SendMessageAsync("Nothing playing!", client).ConfigureAwait(false);
await _lavaNode.EnsureConnected();
if (!_lavaNode.TryGetPlayer(context.GetGuild(_client), out var player)) {
await context.RespondAsync("I'm not connected to a voice channel.");
return; return;
} }
if (player.PlayerState != PlayerState.Playing) { await player.SkipAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
await context.RespondAsync("Woaaah there, I can't skip when nothing is playing.");
return;
}
try { var track = player.CurrentItem;
await player.SkipAsync();
await _musicEmbed.NowPlayingEmbed(player, context, _client); if (track is not null)
} await context.SendMessageAsync($"Skipped. Now playing: {track.Track!.Title}", client).ConfigureAwait(false);
catch (Exception exception) { else
await context.RespondAsync("There is not more tracks to skip."); await context.SendMessageAsync("Skipped. Stopped playing because the queue is now empty.", client).ConfigureAwait(false);
Console.WriteLine(exception);
}
} }
} }

View File

@@ -0,0 +1,37 @@
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
using Lunaris2.Handler.MusicPlayer.PauseCommand;
using Lunaris2.Handler.MusicPlayer.PlayCommand;
using Lunaris2.Handler.MusicPlayer.ResumeCommand;
using Lunaris2.Handler.MusicPlayer.SkipCommand;
using Lunaris2.Notification;
using Lunaris2.SlashCommand;
using MediatR;
namespace Lunaris2.Handler;
public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandler<SlashCommandReceivedNotification>
{
public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken)
{
await notification.Message.DeferAsync();
switch (notification.Message.CommandName)
{
case Command.Resume.Name:
await mediator.Send(new ResumeCommand(notification.Message), cancellationToken);
break;
case Command.Pause.Name:
await mediator.Send(new PauseCommand(notification.Message), cancellationToken);
break;
case Command.Disconnect.Name:
await mediator.Send(new DisconnectCommand(notification.Message), cancellationToken);
break;
case Command.Play.Name:
await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
break;
case Command.Skip.Name:
await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
break;
}
}
}

View File

@@ -1,9 +0,0 @@
namespace Lunaris2.Helper;
public static class Async
{
public static void Run(Func<Task> task)
{
_ = Task.Run(task);
}
}

View File

@@ -9,16 +9,21 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="3.13.1" /> <PackageReference Include="Discord.Net" Version="3.15.3" />
<PackageReference Include="Discord.Net.Commands" Version="3.13.1" /> <PackageReference Include="Discord.Net.Commands" Version="3.15.3" />
<PackageReference Include="Discord.Net.Core" Version="3.13.1" /> <PackageReference Include="Discord.Net.Core" Version="3.15.3" />
<PackageReference Include="Discord.Net.Interactions" Version="3.13.1" /> <PackageReference Include="Discord.Net.Interactions" Version="3.15.3" />
<PackageReference Include="Discord.Net.Rest" Version="3.13.1" /> <PackageReference Include="Discord.Net.Rest" Version="3.15.3" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="Lavalink4NET" Version="4.0.20" />
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.20" />
<PackageReference Include="Lavalink4NET.Discord.NET" Version="4.0.20" />
<PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="OllamaSharp" Version="1.1.10" />
<PackageReference Include="Victoria" Version="6.0.23.324" /> <PackageReference Include="Victoria" Version="6.0.23.324" />
</ItemGroup> </ItemGroup>