Compare commits

..

1 Commits

Author SHA1 Message Date
Myx
f08d89f322 Improve chat 2024-08-11 01:18:00 +02:00
33 changed files with 368 additions and 747 deletions

View File

@@ -14,7 +14,24 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 0 # required for github-action-get-previous-tag fetch-depth: 0 # required for github-action-get-previous-tag
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore ./Bot/Lunaris2.csproj
- name: Build
run: dotnet build ./Bot/Lunaris2.csproj --no-restore -c Release -o ./out
- name: Publish
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
- name: Zip the build
run: 7z a -tzip ./out/Lunaris.zip ./out/*
- name: Get previous tag - name: Get previous tag
id: previoustag id: previoustag
uses: 'WyriHaximus/github-action-get-previous-tag@v1' uses: 'WyriHaximus/github-action-get-previous-tag@v1'
@@ -27,23 +44,6 @@ jobs:
with: with:
version: ${{ steps.previoustag.outputs.tag }} version: ${{ steps.previoustag.outputs.tag }}
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore ./Bot/Lunaris2.csproj
- name: Build
run: dotnet build ./Bot/Lunaris2.csproj --no-restore -c Release /p:AssemblyVersion=${{ steps.previoustag.outputs.tag }} -o ./out
- name: Publish
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
- name: Zip the build
run: 7z a -tzip ./out/Lunaris.zip ./out/*
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1
@@ -63,5 +63,5 @@ jobs:
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./out/Lunaris.zip asset_path: ./out/Lunaris.zip
asset_name: Lunaris_${{steps.semver.outputs.patch}}.zip asset_name: Lunaris.zip
asset_content_type: application/zip asset_content_type: application/zip

2
.gitignore vendored
View File

@@ -3,5 +3,3 @@
.vs .vs
bin bin
appsettings.json appsettings.json
ollama
plugins

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,18 @@
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,6 +1,4 @@
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.Notification; using Lunaris2.Notification;
using MediatR; using MediatR;
@@ -21,41 +19,13 @@ public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotifi
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
{ {
await BotMentioned(notification, cancellationToken); await BotMentioned(notification, cancellationToken);
await Statistics(notification, cancellationToken);
}
private async Task Statistics(MessageReceivedNotification notification, CancellationToken cancellationToken)
{
if (notification.Message.Content.Contains("!LunarisStats"))
{
var servers = _client.Guilds.Select(guild => guild.Name);
var channels = _client.Guilds
.SelectMany(guild => guild.VoiceChannels)
.Where(channel => channel.Users.Any(user => user.IsBot));
var table = new StringBuilder();
var serverColumnWidth = 25; // Width for server column
var channelColumnWidth = 25; // Width for channel column
table.AppendLine($"{"Servers".PadRight(serverColumnWidth - 1)}|{"Channels".PadRight(channelColumnWidth - 1)}");
table.AppendLine($"{new string('-', serverColumnWidth - 1)}|{new string('-', channelColumnWidth - 1)}");
foreach (var (server, channel) in servers.Zip(channels))
{
table.AppendLine($"{server.PadRight(serverColumnWidth - 1)}|{channel.Name.PadRight(channelColumnWidth - 1)}");
}
var embed = new EmbedBuilder()
.WithTitle("Lunaris Statistics")
.WithDescription(table.ToString())
.Build();
await notification.Message.Channel.SendMessageAsync(embed: embed);
}
} }
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken) private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
{ {
if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id)) if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
{ {
// The bot was mentioned
const string pattern = "<.*?>"; const string pattern = "<.*?>";
const string replacement = ""; const string replacement = "";
var regex = new Regex(pattern); var regex = new Regex(pattern);

View File

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

View File

@@ -1,22 +0,0 @@
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.SendMessageAsync("Disconnected.", client).ConfigureAwait(false);
}
}

View File

@@ -1,46 +1,11 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Lavalink4NET; using Victoria.Node;
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)
@@ -67,7 +32,25 @@ 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

@@ -0,0 +1,33 @@
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,102 +1,50 @@
using System.Net;
using Discord; using Discord;
using Discord.Net;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.Handler.MusicPlayer;
namespace Lunaris2.Handler.MusicPlayer;
public static class MessageModule public static class MessageModule
{ {
private static readonly Dictionary<ulong, List<ulong>> GuildMessageIds = new(); private static Dictionary<ulong, List<ulong>> guildMessageIds = new Dictionary<ulong, List<ulong>>();
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.GetOriginalResponseAsync();
var sentMessage = await context.FollowupAsync(message);
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)
{
try
{
var guildId = await StoreForRemoval(context, client);
var sentMessage = await context.FollowupAsync(embed: message);
GuildMessageIds[guildId].Add(sentMessage.Id);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
} }
public static async Task RemoveMessages(this SocketSlashCommand context, DiscordSocketClient client) public static async Task SendMessageAsync(this SocketSlashCommand context, Embed message, DiscordSocketClient client)
{ {
var guildId = context.GetGuild(client).Id; var guildId = await StoreForRemoval(context, client);
if (GuildMessageIds.TryGetValue(guildId, out var value)) await context.RespondAsync(embed: message);
{
if (value.Count <= 0) var sentMessage = await context.GetOriginalResponseAsync();
return;
foreach (var messageId in value)
{
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
if (messageToDelete != null)
await messageToDelete.DeleteAsync();
}
value.Clear(); guildMessageIds[guildId].Add(sentMessage.Id);
}
} }
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.TryGetValue(guildId, out var value)) if (guildMessageIds.ContainsKey(guildId))
{ {
if (value.Count <= 0) foreach (var messageId in guildMessageIds[guildId])
return guildId;
// Create a copy of the list to avoid modifying it during iteration
var messagesToDelete = new List<ulong>(value);
foreach (var messageId in messagesToDelete)
{ {
try var messageToDelete = await context.Channel.GetMessageAsync(messageId);
{ if (messageToDelete != null)
var messageToDelete = await context.Channel.GetMessageAsync(messageId); await messageToDelete.DeleteAsync();
if (messageToDelete != null)
{
await messageToDelete.DeleteAsync();
}
}
catch (HttpException ex)
{
if (ex.HttpCode != HttpStatusCode.NotFound)
throw;
}
} }
// Clear the list after we're done with the iteration guildMessageIds[guildId].Clear();
value.Clear();
} }
else else
{ {
// If the guildId does not exist, add it to the dictionary guildMessageIds.Add(guildId, []);
GuildMessageIds.Add(guildId, new List<ulong>());
} }
return guildId; return guildId;

View File

@@ -1,7 +1,7 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Lavalink4NET.Players.Queued; using Victoria;
using Lavalink4NET.Tracks; using Victoria.Player;
namespace Lunaris2.Handler.MusicPlayer; namespace Lunaris2.Handler.MusicPlayer;
@@ -12,38 +12,33 @@ public class MusicEmbed
string title, string title,
string length, string length,
string artist, string artist,
string queuedBy, string queuedBy,
string? nextSong = null) string? nextInQueue)
{ {
var getNextSong = nextSong is not null ? $"\nNext Song: {nextSong}" : string.Empty;
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}{getNextSong}") .WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}\nNext in queue: {nextInQueue}")
.WithColor(Color.Magenta) .WithColor(Color.Magenta)
.WithThumbnailUrl(imageUrl) .WithThumbnailUrl(imageUrl)
.Build(); .Build();
} }
public async Task NowPlayingEmbed( public async Task NowPlayingEmbed(
LavalinkTrack track, LavaPlayer<LavaTrack> player,
SocketSlashCommand context, SocketSlashCommand context,
DiscordSocketClient client, DiscordSocketClient client)
ITrackQueue? queue = null)
{ {
var duration = TimeSpan.Parse(track.Duration.ToString()); var artwork = await player.Track.FetchArtworkAsync();
var getNextTrack = player.Vueue.Count > 1 ? player.Vueue.ToArray()[1].Title : "No songs in queue.";
var artwork = track.ArtworkUri;
var nextSong = queue?.Count > 1 ? queue[1].Track?.Title : null;
var embed = SendMusicEmbed( var embed = SendMusicEmbed(
artwork.ToString(), artwork,
track.Title, player.Track.Title,
duration.ToString(@"hh\:mm\:ss"), player.Track.Duration.ToString(),
track.Author, player.Track.Author,
context.User.Username, context.User.Username,
nextSong); getNextTrack);
await context.SendMessageAsync(embed, client); await context.SendMessageAsync(embed, client);
} }
} }

View File

@@ -1,31 +0,0 @@
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,30 +0,0 @@
using System.Text.Json;
using Lavalink4NET.Filters;
using Lavalink4NET.Protocol.Models.Filters;
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
public class NormalizationFilter : IFilterOptions
{
private double MaxAmplitude { get; set; }
private bool Adaptive { get; set; }
public NormalizationFilter(double maxAmplitude, bool adaptive)
{
MaxAmplitude = maxAmplitude;
Adaptive = adaptive;
}
public bool IsDefault => MaxAmplitude == 1.0 && !Adaptive;
public void Apply(ref PlayerFilterMapModel filterMap)
{
filterMap.AdditionalFilters ??= new Dictionary<string, JsonElement>();
var normalizationFilter = new
{
maxAmplitude = MaxAmplitude,
adaptive = Adaptive
};
filterMap.AdditionalFilters["normalization"] = JsonSerializer.SerializeToElement(normalizationFilter);
}
}

View File

@@ -1,14 +1,12 @@
using System.Collections.Immutable; using Discord;
using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.SlashCommand; using Lunaris2.SlashCommand;
using MediatR; using MediatR;
using Lavalink4NET; using Victoria.Node;
using Lavalink4NET.Events.Players; using Victoria.Node.EventArgs;
using Lavalink4NET.Integrations.SponsorBlock; using Victoria.Player;
using Lavalink4NET.Integrations.SponsorBlock.Extensions; using Victoria.Responses.Search;
using Lavalink4NET.Players.Queued;
using Lavalink4NET.Rest.Entities.Tracks;
using Lavalink4NET.Tracks;
namespace Lunaris2.Handler.MusicPlayer.PlayCommand; namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
@@ -17,180 +15,105 @@ 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;
private const int MaxTrackDuration = 30;
private LavalinkTrack? _previousTrack;
private static readonly HashSet<ulong> SubscribedGuilds = new();
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;
} }
private async Task OnTrackEnded(object sender, TrackEndedEventArgs eventargs) [Command(RunMode = RunMode.Async)]
public async Task Handle(PlayCommand command, CancellationToken cancellationToken)
{ {
// Reset the previous track when the track ends. _context = command.Message;
_previousTrack = null;
}
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
{
var player = sender as QueuedLavalinkPlayer;
if (player?.CurrentTrack is null) await _lavaNode.EnsureConnected();
{
return; // No track is currently playing.
}
var currentTrack = player.CurrentTrack; var songName = _context.GetOptionValueByName(Option.Input);
// Check if the current track is the same as the previous one if (string.IsNullOrWhiteSpace(songName)) {
if (_previousTrack?.Identifier == currentTrack.Identifier) await _context.RespondAsync("Please provide search terms.");
{
// The same track is playing, so we don't need to create a new embed.
return; return;
} }
// Track has changed, update the previous track and send the embed var player = await GetPlayer();
_previousTrack = currentTrack;
await _musicEmbed.NowPlayingEmbed(currentTrack, _context, _client);
}
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
{
new Thread(PlayMusic).Start();
return Task.CompletedTask;
async void PlayMusic()
{
try
{
RegisterTrackStartedEventListerner(command);
await _audioService.StartAsync(cancellationToken);
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;
await ApplyFilters(cancellationToken, player);
await ConfigureSponsorBlock(cancellationToken, player);
var trackLoadOptions = new TrackLoadOptions
{
SearchMode = TrackSearchMode.YouTube,
};
var trackCollection = await _audioService.Tracks.LoadTracksAsync(searchQuery, trackLoadOptions, cancellationToken: cancellationToken);
// Check if the result is a playlist or just a single track from the search result
if (trackCollection.IsPlaylist)
{
// If it's a playlist, check if it's a free-text input.
if (!Uri.IsWellFormedUriString(searchQuery, UriKind.Absolute))
{
// Free text was used (not a direct URL to a playlist), let's prevent queueing the whole playlist.
// Queue only the first track of the search result
// var firstTrack = trackCollection.Tracks.FirstOrDefault();
if (trackCollection.Track != null)
{
await player.PlayAsync(trackCollection.Track, cancellationToken: cancellationToken).ConfigureAwait(false);
await _musicEmbed.NowPlayingEmbed(trackCollection.Track, _context, _client);
}
else
{
await context.SendMessageAsync("No tracks found.", _client);
}
}
else
{
// It's a playlist and a URL was used, so we can queue all tracks as usual
var queueTracks = trackCollection.Tracks
.Skip(1)
.Select(t => new TrackQueueItem(t))
.ToList();
await player.PlayAsync(trackCollection.Tracks.First(), cancellationToken: cancellationToken).ConfigureAwait(false);
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
await context.SendMessageAsync($"Queued playlist {trackCollection.Playlist?.Name} with {queueTracks.Count} tracks.", _client);
}
}
else
{
// It's just a single track or a search result.
var track = trackCollection.Tracks.FirstOrDefault();
if (track != null)
{
await player.PlayAsync(track, cancellationToken: cancellationToken).ConfigureAwait(false);
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
}
else
{
await context.SendMessageAsync("No tracks found.", _client);
}
}
}
catch (Exception error)
{
throw new Exception("Error occured in the Play handler!", error);
}
}
}
private void RegisterTrackStartedEventListerner(PlayCommand command)
{
if (SubscribedGuilds.Contains((ulong)command.Message.GuildId!))
return;
_audioService.TrackStarted += OnTrackStarted; if (player == null)
_audioService.TrackEnded += OnTrackEnded; return;
SubscribedGuilds.Add((ulong)command.Message.GuildId!);
var searchResponse = await _lavaNode.SearchAsync(
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 static async Task ApplyFilters(CancellationToken cancellationToken, QueuedLavalinkPlayer player) private async Task OnTrackEnd(TrackEndEventArg<LavaPlayer<LavaTrack>, LavaTrack> arg)
{ {
var normalizationFilter = new NormalizationFilter(0.5, true); var player = arg.Player;
player.Filters.SetFilter(normalizationFilter); if (!player.Vueue.TryDequeue(out var nextTrack))
await player.Filters.CommitAsync(cancellationToken); return;
await player.PlayAsync(nextTrack);
await _musicEmbed.NowPlayingEmbed(player, _context, _client);
} }
private static async Task ConfigureSponsorBlock(CancellationToken cancellationToken, QueuedLavalinkPlayer player) private static async Task PlayTrack(LavaPlayer<LavaTrack> player)
{ {
var categories = ImmutableArray.Create( if (player.PlayerState is PlayerState.Playing or PlayerState.Paused) {
SegmentCategory.Intro, return;
SegmentCategory.Sponsor, }
SegmentCategory.SelfPromotion,
SegmentCategory.Outro,
SegmentCategory.Filler);
await player.UpdateSponsorBlockCategoriesAsync(categories, cancellationToken: cancellationToken); player.Vueue.TryDequeue(out var lavaTrack);
await player.PlayAsync(lavaTrack);
}
private async Task<LavaPlayer<LavaTrack>?> GetPlayer()
{
var voiceState = _context.User as IVoiceState;
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) {
await _context.RespondAsync($"I wasn't able to find anything for `{songName}`.");
return false;
}
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

@@ -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

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

View File

@@ -1,8 +1,6 @@
using Lunaris2.Handler.MusicPlayer.ClearQueueCommand; using Lunaris2.Handler.GoodByeCommand;
using Lunaris2.Handler.MusicPlayer.DisconnectCommand; using Lunaris2.Handler.MusicPlayer.JoinCommand;
using Lunaris2.Handler.MusicPlayer.PauseCommand;
using Lunaris2.Handler.MusicPlayer.PlayCommand; using Lunaris2.Handler.MusicPlayer.PlayCommand;
using Lunaris2.Handler.MusicPlayer.ResumeCommand;
using Lunaris2.Handler.MusicPlayer.SkipCommand; using Lunaris2.Handler.MusicPlayer.SkipCommand;
using Lunaris2.Notification; using Lunaris2.Notification;
using Lunaris2.SlashCommand; using Lunaris2.SlashCommand;
@@ -14,18 +12,16 @@ public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandle
{ {
public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken) public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken)
{ {
await notification.Message.DeferAsync();
switch (notification.Message.CommandName) switch (notification.Message.CommandName)
{ {
case Command.Resume.Name: case Command.Hello.Name:
await mediator.Send(new ResumeCommand(notification.Message), cancellationToken); await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken);
break; break;
case Command.Pause.Name: case Command.Goodbye.Name:
await mediator.Send(new PauseCommand(notification.Message), cancellationToken); await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken);
break; break;
case Command.Disconnect.Name: case Command.Join.Name:
await mediator.Send(new DisconnectCommand(notification.Message), cancellationToken); await mediator.Send(new JoinCommand(notification.Message), cancellationToken);
break; break;
case Command.Play.Name: case Command.Play.Name:
await mediator.Send(new PlayCommand(notification.Message), cancellationToken); await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
@@ -33,9 +29,6 @@ public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandle
case Command.Skip.Name: case Command.Skip.Name:
await mediator.Send(new SkipCommand(notification.Message), cancellationToken); await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
break; break;
case Command.Clear.Name:
await mediator.Send(new ClearQueueCommand(notification.Message), cancellationToken);
break;
} }
} }
} }

9
Bot/Helper/RunAsync.cs Normal file
View File

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

View File

@@ -8,31 +8,23 @@
<UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId> <UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<!-- Lavalink4net 4.0.25 seems to break the Message Module-->
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="3.16.0" /> <PackageReference Include="Discord.Net" Version="3.13.1" />
<PackageReference Include="Discord.Net.Commands" Version="3.16.0" /> <PackageReference Include="Discord.Net.Commands" Version="3.13.1" />
<PackageReference Include="Discord.Net.Core" Version="3.16.0" /> <PackageReference Include="Discord.Net.Core" Version="3.13.1" />
<PackageReference Include="Discord.Net.Interactions" Version="3.16.0" /> <PackageReference Include="Discord.Net.Interactions" Version="3.13.1" />
<PackageReference Include="Discord.Net.Rest" Version="3.16.0" /> <PackageReference Include="Discord.Net.Rest" Version="3.13.1" />
<PackageReference Include="Lavalink4NET" Version="4.0.25" /> <PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.25" />
<PackageReference Include="Lavalink4NET.Discord.NET" Version="4.0.25" />
<PackageReference Include="Lavalink4NET.Integrations.Lavasrc" Version="4.0.25" />
<PackageReference Include="Lavalink4NET.Integrations.SponsorBlock" Version="4.0.25" />
<PackageReference Include="MediatR" Version="12.4.1" />
<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.2" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="OllamaSharp" Version="1.1.10" /> <PackageReference Include="OllamaSharp" Version="1.1.10" />
<PackageReference Include="Victoria" Version="6.0.23.324" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="../docker-compose.yml" pack="true" PackagePath="." />
<None Include="../application.yml" pack="true" PackagePath="." />
<None Include="../start-services.sh" pack="true" PackagePath="." />
<None Update="appsettings.json"> <None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>

View File

@@ -25,10 +25,9 @@ public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFacto
await Task.CompletedTask; await Task.CompletedTask;
} }
private Task OnMessageReceivedAsync(SocketMessage arg) private async Task OnMessageReceivedAsync(SocketMessage arg)
{ {
_ = Task.Run(() => Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken), _cancellationToken); await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
return Task.CompletedTask;
} }
private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg) private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg)

View File

@@ -1,32 +1,27 @@
using System.Reflection; using System.Reflection;
using Discord; using Discord;
using Discord.Commands;
using Discord.Interactions; using Discord.Interactions;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.Handler.ChatCommand; using Lunaris2.Handler.ChatCommand;
using Lavalink4NET.Extensions;
using Lavalink4NET.Integrations.SponsorBlock.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;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Victoria;
using Victoria.Node;
using RunMode = Discord.Commands.RunMode;
namespace Lunaris2; namespace Lunaris2;
public class Program public class Program
{ {
private static LavaNode? _lavaNode;
public static void Main(string[] args) public static void Main(string[] args)
{ {
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => CreateHostBuilder(args).Build().Run();
{
Console.WriteLine(eventArgs.ExceptionObject);
};
var app = CreateHostBuilder(args).Build();
app.UseSponsorBlock();
app.Run();
} }
private static IHostBuilder CreateHostBuilder(string[] args) => private static IHostBuilder CreateHostBuilder(string[] args) =>
@@ -37,36 +32,37 @@ public class Program
{ {
GatewayIntents = GatewayIntents.All GatewayIntents = GatewayIntents.All
}; };
var commandServiceConfig = new CommandServiceConfig{ DefaultRunMode = RunMode.Async };
var client = new DiscordSocketClient(config); var client = new DiscordSocketClient(config);
var commands = new CommandService(commandServiceConfig);
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory) .SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json") .AddJsonFile("appsettings.json")
.Build(); .Build();
services services
.AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) .AddSingleton(client)
.AddLavalink() .AddSingleton(commands)
.ConfigureLavalink(options => .AddMediatR(configuration => configuration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
.AddSingleton<DiscordEventListener>()
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
.AddLavaNode(nodeConfiguration =>
{ {
options.BaseAddress = new Uri( nodeConfiguration.SelfDeaf = false;
$"http://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}" nodeConfiguration.Hostname = configuration["LavaLinkHostname"];
); nodeConfiguration.Port = Convert.ToUInt16(configuration["LavaLinkPort"]);
options.WebSocketUri = new Uri($"ws://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}/v4/websocket"); nodeConfiguration.Authorization = configuration["LavaLinkPassword"];
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
options.Label = "Node";
}) })
.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);
client.Log += Log; client.Log += Log;
client client
.LoginAsync(TokenType.Bot, configuration["Token"]) .LoginAsync(TokenType.Bot, configuration["Token"])
.GetAwaiter() .GetAwaiter()
@@ -76,6 +72,8 @@ public class Program
.StartAsync() .StartAsync()
.GetAwaiter() .GetAwaiter()
.GetResult(); .GetResult();
_lavaNode = services.BuildServiceProvider().GetRequiredService<LavaNode>();
var listener = services var listener = services
.BuildServiceProvider() .BuildServiceProvider()
@@ -87,12 +85,10 @@ public class Program
.GetResult(); .GetResult();
}); });
private static Task Client_Ready(DiscordSocketClient client) private static async Task Client_Ready(DiscordSocketClient client)
{ {
await _lavaNode.ConnectAsync();
client.RegisterCommands(); client.RegisterCommands();
new VoiceChannelMonitorService(client).StartMonitoring();
return Task.CompletedTask;
} }
private static Task Log(LogMessage arg) private static Task Log(LogMessage arg)

View File

@@ -1,71 +0,0 @@
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)); // Monitor every minute
}
});
}
private async Task CheckVoiceChannels()
{
foreach (var guild in _client.Guilds)
{
// Find voice channels where only the bot is left
var voiceChannel = guild.VoiceChannels.FirstOrDefault(vc =>
vc.ConnectedUsers.Count == 1 &&
vc.Users.Any(u => u.Id == _client.CurrentUser.Id));
if (voiceChannel != null)
{
// If timer not set for this channel, start one
if (!_timers.ContainsKey(voiceChannel.Id))
{
Console.WriteLine($"Bot is alone in channel {voiceChannel.Name}, starting timer...");
_timers[voiceChannel.Id] = new Timer(async _ => await LeaveChannel(voiceChannel), null,
TimeSpan.FromMinutes(3), Timeout.InfiniteTimeSpan); // Set delay before leaving
}
}
else
{
// Clean up timer if channel is no longer active
var timersToDispose = _timers.Where(t => guild.VoiceChannels.All(vc => vc.Id != t.Key)).ToList();
foreach (var timer in timersToDispose)
{
await timer.Value.DisposeAsync();
_timers.Remove(timer.Key);
Console.WriteLine($"Disposed timer for inactive voice channel ID: {timer.Key}");
}
}
}
}
private async Task LeaveChannel(SocketVoiceChannel voiceChannel)
{
if (voiceChannel.ConnectedUsers.Count == 1 && voiceChannel.Users.Any(u => u.Id == _client.CurrentUser.Id))
{
Console.WriteLine($"Leaving channel {voiceChannel.Name} due to inactivity...");
await voiceChannel.DisconnectAsync();
await _timers[voiceChannel.Id].DisposeAsync();
_timers.Remove(voiceChannel.Id); // Clean up after leaving
}
}
}
}

View File

@@ -9,16 +9,22 @@ public static class Option
public static class Command public static class Command
{ {
public static class Disconnect public static class Hello
{ {
public const string Name = "disconnect"; public const string Name = "hello";
public const string Description = "Disconnect from the voice channel!"; 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 Clear public static class Join
{ {
public const string Name = "clear"; public const string Name = "join";
public const string Description = "Clear the music queue!"; public const string Description = "Join the voice channel!";
} }
public static class Skip public static class Skip
@@ -27,16 +33,10 @@ public static class Command
public const string Description = "Skip the current song!"; public const string Description = "Skip the current song!";
} }
public static class Resume public static class Stop
{ {
public const string Name = "resume"; public const string Name = "stop";
public const string Description = "Resume the music!"; public const string Description = "Stop the music!";
}
public static class Pause
{
public const string Name = "pause";
public const string Description = "Pause the music!";
} }
public static class Play public static class Play

View File

@@ -42,6 +42,7 @@ public class SlashCommandBuilder(
private static async Task RemoveUnusedCommands(string[] commands, IEnumerable<SocketApplicationCommand> registeredCommands) private static async Task RemoveUnusedCommands(string[] commands, IEnumerable<SocketApplicationCommand> registeredCommands)
{ {
// Remove commands from Discord(registeredCommands) that are not in the list of commands
foreach(var command in registeredCommands) foreach(var command in registeredCommands)
{ {
if (commands.Contains(command.Name)) if (commands.Contains(command.Name))

View File

@@ -7,13 +7,12 @@ public static class SlashCommandRegistration
{ {
public static void RegisterCommands(this DiscordSocketClient client) public static void RegisterCommands(this DiscordSocketClient client)
{ {
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description); RegisterCommand(client, Command.Hello.Name, Command.Hello.Description);
RegisterCommand(client, Command.Pause.Name, Command.Pause.Description); RegisterCommand(client, Command.Goodbye.Name, Command.Goodbye.Description);
RegisterCommand(client, Command.Disconnect.Name, Command.Disconnect.Description); RegisterCommand(client, Command.Join.Name, Command.Join.Description);
RegisterCommand(client, Command.Skip.Name, Command.Skip.Description); RegisterCommand(client, Command.Skip.Name, Command.Skip.Description);
RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options); RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options);
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description); RegisterCommand(client, Command.Stop.Name, Command.Stop.Description);
RegisterCommand(client, Command.Clear.Name, Command.Clear.Description);
} }
private static void RegisterCommand( private static void RegisterCommand(

View File

@@ -6,7 +6,7 @@
"Microsoft": "Information" "Microsoft": "Information"
} }
}, },
"Token": "TOKEN", "Token": "discordToken",
"LavaLinkPassword": "youshallnotpass", "LavaLinkPassword": "youshallnotpass",
"LavaLinkHostname": "127.0.0.1", "LavaLinkHostname": "127.0.0.1",
"LavaLinkPort": 2333, "LavaLinkPort": 2333,

View File

@@ -1,2 +0,0 @@
docker run quay.io/invidious/youtube-trusted-session-generator
read -p "Copy the codes and press enter to close the terminal."

View File

@@ -23,24 +23,6 @@ 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.

View File

@@ -2,76 +2,17 @@ server: # REST and WS server
port: 2333 port: 2333
address: 0.0.0.0 address: 0.0.0.0
plugins: plugins:
lavasrc: # name: # Name of the plugin
providers: # Custom providers for track loading. This is the default # some_key: some_value # Some key-value pair for the plugin
# - "dzisrc:%ISRC%" # Deezer ISRC provider # another_key: another_value
# - "dzsearch:%QUERY%" # Deezer search provider
- "ytsearch:\"%ISRC%\"" # Will be ignored if track does not have an ISRC. See https://en.wikipedia.org/wiki/International_Standard_Recording_Code
- "ytsearch:%QUERY%" # Will be used if track has no ISRC or no track could be found for the ISRC
# you can add multiple other fallback sources here
sources:
spotify: true # Enable Spotify source
applemusic: false # Enable Apple Music source
deezer: false # Enable Deezer source
yandexmusic: false # Enable Yandex Music source
flowerytts: false # Enable Flowery TTS source
youtube: false # Enable YouTube search source (https://github.com/topi314/LavaSearch)
vkmusic: false # Enable Vk Music source
spotify:
clientId: "ID"
clientSecret: "SECRET"
# spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
playlistLoadLimit: 6 # The number of pages at 100 tracks each
albumLoadLimit: 6 # The number of pages at 50 tracks each
resolveArtistsInSearch: true # Whether to resolve artists in track search results (can be slow)
localFiles: false # Enable local files support with Spotify playlists. Please note `uri` & `isrc` will be `null` & `identifier` will be `"local"`
youtube:
oauth:
# setting "enabled: true" is the bare minimum to get OAuth working.
enabled: true
# if you have a refresh token, you may set it below (make sure to uncomment the line to apply it).
# setting a valid refresh token will skip the OAuth flow entirely. See above note on how to retrieve
# your refreshToken.
# Set this if you don't want the OAuth flow to be triggered, if you intend to supply a refresh token later.
# Initialization is skipped automatically if a valid refresh token is supplied. Leave this commented if you're
# completing the OAuth flow for the first time/do not have a refresh token.
# skipInitialization: true
# pot: // run generate-trusted-session.sh
# token: ""
# visitorData: ""
enabled: true # Whether this source can be used.
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used.
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded.
# The clients to use for track loading. See below for a list of valid clients.
# Clients are queried in the order they are given (so the first client is queried first and so on...)
clients:
- MUSIC
- ANDROID_TESTSUITE
- WEB
- TVHTML5EMBEDDED
# name: # Name of the plugin
# some_key: some_value # Some key-value pair for the plugin
# another_key: another_value
lavalink: lavalink:
plugins: plugins:
- dependency: com.github.devoxin:lavadspx-plugin:0.0.5 # replace {VERSION} with the latest version from the "Releases" tab. # - dependency: "group:artifact:version"
repository: https://jitpack.io # repository: "repository"
- dependency: "dev.lavalink.youtube:youtube-plugin:1.8.3"
snapshot: false # Set to true if you want to use a snapshot version.
- dependency: "com.github.topi314.lavasearch:lavasearch-plugin:1.0.0"
repository: "https://maven.lavalink.dev/releases" # this is optional for lavalink v4.0.0-beta.5 or greater
snapshot: false # set to true if you want to use snapshot builds (see below)
- dependency: "com.github.topi314.sponsorblock:sponsorblock-plugin:3.0.1"
- dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.2.0"
repository: "https://maven.lavalink.dev/releases" # this is optional for lavalink v4.0.0-beta.5 or greater
snapshot: false # set to true if you want to use snapshot builds (see below)
server: server:
password: "youshallnotpass" password: "youshallnotpass"
sources: sources:
youtube: false youtube: true
bandcamp: true bandcamp: true
soundcloud: true soundcloud: true
twitch: true twitch: true
@@ -89,9 +30,9 @@ lavalink:
rotation: true rotation: true
channelMix: true channelMix: true
lowPass: true lowPass: true
bufferDurationMs: 1000 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses. bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses.
frameBufferDurationMs: 10000 # How many milliseconds of audio to keep buffered frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered
opusEncodingQuality: 5 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU. resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data. trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready. useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
@@ -100,6 +41,20 @@ lavalink:
youtubeSearchEnabled: true youtubeSearchEnabled: true
soundcloudSearchEnabled: true soundcloudSearchEnabled: true
gc-warnings: true gc-warnings: true
#ratelimit:
#ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
#excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
#strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
#searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
#retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
#youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without.
#email: "" # Email of Google account
#password: "" # Password of Google account
#httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
#proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
#proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
#proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
#proxyPassword: "" # Password for basic authentication
metrics: metrics:
prometheus: prometheus:
@@ -109,6 +64,9 @@ metrics:
sentry: sentry:
dsn: "" dsn: ""
environment: "" environment: ""
# tags:
# some_key: some_value
# another_key: another_value
logging: logging:
file: file:
@@ -126,7 +84,8 @@ logging:
includePayload: true includePayload: true
maxPayloadLength: 10000 maxPayloadLength: 10000
logback: logback:
rollingpolicy: rollingpolicy:
max-file-size: 1GB max-file-size: 1GB
max-history: 30 max-history: 30

View File

@@ -1,7 +1,7 @@
services: services:
lavalink: lavalink:
# pin the image version to Lavalink v4 # pin the image version to Lavalink v4
image: ghcr.io/lavalink-devs/lavalink:4.0.8 image: ghcr.io/lavalink-devs/lavalink:3.7.11
container_name: lavalink container_name: lavalink
restart: unless-stopped restart: unless-stopped
environment: environment:

Binary file not shown.

View File

@@ -1 +1,3 @@
docker compose up -d docker compose up -d
read -p "Press enter to continue"