Compare commits

...

3 Commits

Author SHA1 Message Date
4ba01ed72b Small fix (#6)
Co-authored-by: Myx <info@azaaxin.com>
2024-08-13 03:24:29 +02:00
1e2c10a7ea Require user to be in chat to queue (#5)
Co-authored-by: Myx <info@azaaxin.com>
2024-08-12 19:16:11 +02:00
8dcd4b334d Fix deadlock and auto leave voice channel after 3 min (#4)
* Fix for thread being busy handling discord commands blocking audioplayer to timeout
* Auto leave if alone in voice channel after 3 min

Co-authored-by: Myx <info@azaaxin.com>
2024-08-12 02:02:10 +02:00
7 changed files with 169 additions and 62 deletions

View File

@@ -38,6 +38,26 @@ public static class MessageModule
throw; throw;
} }
} }
public static async Task RemoveMessages(this SocketSlashCommand context, DiscordSocketClient client)
{
var guildId = context.GetGuild(client).Id;
if (GuildMessageIds.TryGetValue(guildId, out var value))
{
if (value.Count <= 0)
return;
foreach (var messageId in value)
{
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
if (messageToDelete != null)
await messageToDelete.DeleteAsync();
}
value.Clear();
}
}
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client) private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
{ {

View File

@@ -16,6 +16,7 @@ public class PlayHandler : IRequestHandler<PlayCommand>
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IAudioService _audioService; private readonly IAudioService _audioService;
private SocketSlashCommand _context; private SocketSlashCommand _context;
private const int MaxTrackDuration = 30;
public PlayHandler( public PlayHandler(
DiscordSocketClient client, DiscordSocketClient client,
@@ -27,7 +28,7 @@ public class PlayHandler : IRequestHandler<PlayCommand>
_audioService = audioService; _audioService = audioService;
_audioService.TrackStarted += OnTrackStarted; _audioService.TrackStarted += OnTrackStarted;
} }
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs) private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
{ {
var player = sender as QueuedLavalinkPlayer; var player = sender as QueuedLavalinkPlayer;
@@ -37,57 +38,74 @@ public class PlayHandler : IRequestHandler<PlayCommand>
await _musicEmbed.NowPlayingEmbed(track, _context, _client); await _musicEmbed.NowPlayingEmbed(track, _context, _client);
} }
public async Task Handle(PlayCommand command, CancellationToken cancellationToken) public Task Handle(PlayCommand command, CancellationToken cancellationToken)
{ {
await _audioService.StartAsync(cancellationToken); new Thread(PlayMusic).Start();
var context = command.Message; return Task.CompletedTask;
_context = context;
var searchQuery = context.GetOptionValueByName(Option.Input);
if (string.IsNullOrWhiteSpace(searchQuery)) { async void PlayMusic()
await context.SendMessageAsync("Please provide search terms.", _client);
return;
}
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
if (player is null)
return;
var trackLoadOptions = new TrackLoadOptions
{ {
SearchMode = TrackSearchMode.YouTube, try
};
var track = await _audioService.Tracks
.LoadTrackAsync(
searchQuery,
trackLoadOptions,
cancellationToken: cancellationToken);
if (track is null)
await context.SendMessageAsync("😖 No results.", _client);
if (player.CurrentTrack is null)
{
await player
.PlayAsync(track, cancellationToken: cancellationToken)
.ConfigureAwait(false);
await _musicEmbed.NowPlayingEmbed(track, context, _client);
}
else
{
if (track != null)
{ {
var queueTracks = new[] { new TrackQueueItem(track) }; await _audioService.StartAsync(cancellationToken);
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client); var context = command.Message;
_context = context;
if ((context.User as SocketGuildUser)?.VoiceChannel == null)
{
await context.SendMessageAsync("You must be in a voice channel to use this command.", _client);
return;
}
var searchQuery = context.GetOptionValueByName(Option.Input);
if (string.IsNullOrWhiteSpace(searchQuery))
{
await context.SendMessageAsync("Please provide search terms.", _client);
return;
}
await context.SendMessageAsync("📻 Searching...", _client);
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
if (player is null)
return;
var trackLoadOptions = new TrackLoadOptions { SearchMode = TrackSearchMode.YouTube, };
var track = await _audioService.Tracks.LoadTrackAsync(searchQuery, trackLoadOptions, cancellationToken: cancellationToken);
if (track is null)
{
await context.SendMessageAsync("😖 No results.", _client);
return;
}
if (player.CurrentTrack?.Duration.TotalMinutes > MaxTrackDuration)
{
await context.SendMessageAsync($"🔈 Sorry the track is longer than { MaxTrackDuration } minutes, to save resources this limit is active for now.", _client);
return;
}
if (player.CurrentTrack is null)
{
await player.PlayAsync(track, cancellationToken: cancellationToken)
.ConfigureAwait(false);
await _musicEmbed.NowPlayingEmbed(track, context, _client);
}
else
{
var queueTracks = new[] { new TrackQueueItem(track) };
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
}
} }
else catch (Exception error)
{ {
await context.SendMessageAsync($"Couldn't read song information", _client); throw new Exception("Error occured in the Play handler!", error);
} }
} }
} }

View File

@@ -32,4 +32,4 @@ There is also OnTrackEnd, when it get called an attempt is made to play the next
| `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. |
| `songName` | `string` | A string that represents the name or URL of a song to play. Used to search for and queue tracks. | | `songName` | `string` | A string that represents the name or URL of a song to play. Used to search for and queue tracks. |
| `searchResponse` | `SearchResponse` | An instance of the `SearchResponse` class, representing the result of a search for tracks. Used to get the tracks that were found and queue them in the player. | | `searchResponse` | `SearchResponse` | An instance of the `SearchResponse` class, representing the result of a search for tracks. Used to get the tracks that were found and queue them in the player. |

View File

@@ -6,6 +6,7 @@ using Lunaris2.Handler.ChatCommand;
using Lavalink4NET.Extensions; using Lavalink4NET.Extensions;
using Lunaris2.Handler.MusicPlayer; using Lunaris2.Handler.MusicPlayer;
using Lunaris2.Notification; using Lunaris2.Notification;
using Lunaris2.Service;
using Lunaris2.SlashCommand; using Lunaris2.SlashCommand;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -41,10 +42,7 @@ public class Program
.Build(); .Build();
services services
.AddSingleton(client)
.AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) .AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
.AddSingleton<DiscordEventListener>()
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
.AddLavalink() .AddLavalink()
.ConfigureLavalink(options => .ConfigureLavalink(options =>
{ {
@@ -58,6 +56,10 @@ public class Program
.AddSingleton<LavaNode>() .AddSingleton<LavaNode>()
.AddSingleton<MusicEmbed>() .AddSingleton<MusicEmbed>()
.AddSingleton<ChatSettings>() .AddSingleton<ChatSettings>()
.AddSingleton(client)
.AddSingleton<DiscordEventListener>()
.AddSingleton<VoiceChannelMonitorService>()
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
.Configure<ChatSettings>(configuration.GetSection("LLM")); .Configure<ChatSettings>(configuration.GetSection("LLM"));
client.Ready += () => Client_Ready(client); client.Ready += () => Client_Ready(client);
@@ -86,6 +88,8 @@ public class Program
private static Task Client_Ready(DiscordSocketClient client) private static Task Client_Ready(DiscordSocketClient client)
{ {
client.RegisterCommands(); client.RegisterCommands();
new VoiceChannelMonitorService(client).StartMonitoring();
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -0,0 +1,59 @@
using Discord.WebSocket;
namespace Lunaris2.Service;
public class VoiceChannelMonitorService
{
private readonly DiscordSocketClient _client;
private readonly Dictionary<ulong, Timer> _timers = new();
public VoiceChannelMonitorService(DiscordSocketClient client)
{
_client = client;
}
public void StartMonitoring()
{
Task.Run(async () =>
{
while (true)
{
await CheckVoiceChannels();
await Task.Delay(TimeSpan.FromMinutes(1));
}
});
}
private async Task CheckVoiceChannels()
{
foreach (var guild in _client.Guilds)
{
var voiceChannel = guild.VoiceChannels.FirstOrDefault(vc => vc.ConnectedUsers.Count == 1);
if (voiceChannel != null)
{
if (!_timers.ContainsKey(voiceChannel.Id))
{
_timers[voiceChannel.Id] = new Timer(async _ => await LeaveChannel(voiceChannel), null, TimeSpan.FromMinutes(3), Timeout.InfiniteTimeSpan);
}
}
else
{
if (voiceChannel == null || !_timers.ContainsKey(voiceChannel.Id))
continue;
await _timers[voiceChannel.Id].DisposeAsync();
_timers.Remove(voiceChannel.Id);
}
}
}
private async Task LeaveChannel(SocketVoiceChannel voiceChannel)
{
if (voiceChannel.ConnectedUsers.Count == 1 && voiceChannel.Users.Any(u => u.Id == _client.CurrentUser.Id))
{
await voiceChannel.DisconnectAsync();
await _timers[voiceChannel.Id].DisposeAsync();
_timers.Remove(voiceChannel.Id);
}
}
}

View File

@@ -9,18 +9,6 @@ public static class Option
public static class Command public static class Command
{ {
// public static class Hello
// {
// public const string Name = "hello";
// public const string Description = "Say hello to the bot!";
// }
//
// public static class Goodbye
// {
// public const string Name = "goodbye";
// public const string Description = "Say goodbye to the bot!";
// }
public static class Disconnect public static class Disconnect
{ {
public const string Name = "disconnect"; public const string Name = "disconnect";

View File

@@ -23,6 +23,24 @@ Lunaris supports AI chat using a large language model, this is done by hosting t
The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). Running LLM locally requires much resources from your system, minimum requirements is at least 8GB of ram. If your don't have enought ram, select a LLM model in the [appsettings file](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json#L15) that requires less of your system. The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). Running LLM locally requires much resources from your system, minimum requirements is at least 8GB of ram. If your don't have enought ram, select a LLM model in the [appsettings file](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json#L15) that requires less of your system.
*NOTE: you need to download the model from the Ollama ui, the model name which is preselected in the code is called ``gemma``.*
## PM2 Setup
- Install PM2 and configure it following their setup guide
#### Lavalink
* Download Lavalink 4.X.X (.jar)
* Install Java 17
If using Linux run following command to start Lavalink with PM2:
``pm2 start "sudo java -Xmx1G -jar Lavalink.jar" --name Lavalink4.0.7``
For me I have Lavalink.jar downloaded in ``/opt`` folder from Linux root. By running Lavalink using PM2, you can monitor it and manage it from a page in your browser instead of having to access the server terminal.
#### Lunaris
* Install dotnet
Register the Lunaris bot with PM2:
``pm2 start "dotnet Lunaris2.dll"``
## Usage ## Usage
- `/play <song>`: Plays the specified song in the voice channel you're currently in. - `/play <song>`: Plays the specified song in the voice channel you're currently in.