mirror of
https://github.com/Myxelium/Lunaris2.0.git
synced 2026-04-13 08:00:37 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae1a4e14d6 | ||
|
|
5726c110a1 | ||
| 3362c6bf8c | |||
| a864944318 | |||
| 146455c1bd | |||
| 56eee11fc9 | |||
| e01746a343 | |||
| e847c1579a | |||
| 1ccc31d3d2 | |||
| 7c4d8c246d | |||
| 43f0191752 | |||
| 872b6d3138 | |||
| f292124228 | |||
| 4cbee9a625 | |||
| b79e56d3a1 | |||
| fa19f8d938 | |||
| ac869c43da | |||
| e2fdd9a2d7 | |||
| 98761fc91d | |||
| 373d482906 | |||
|
|
e044f2f91b | ||
| 4855d37d76 | |||
| 4ba01ed72b |
38
.github/workflows/dotnet.yml
vendored
38
.github/workflows/dotnet.yml
vendored
@@ -14,24 +14,7 @@ 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'
|
||||||
@@ -44,6 +27,23 @@ 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.zip
|
asset_name: Lunaris_${{steps.semver.outputs.patch}}.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
|||||||
.vs
|
.vs
|
||||||
bin
|
bin
|
||||||
appsettings.json
|
appsettings.json
|
||||||
|
ollama
|
||||||
|
plugins
|
||||||
|
|||||||
@@ -4,65 +4,64 @@ using MediatR;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using OllamaSharp;
|
using OllamaSharp;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.ChatCommand
|
namespace Lunaris2.Handler.ChatCommand;
|
||||||
|
|
||||||
|
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
|
||||||
|
|
||||||
|
public class ChatHandler : IRequestHandler<ChatCommand>
|
||||||
{
|
{
|
||||||
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
|
private readonly OllamaApiClient _ollama;
|
||||||
|
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
|
||||||
|
private readonly ChatSettings _chatSettings;
|
||||||
|
|
||||||
public class ChatHandler : IRequestHandler<ChatCommand>
|
public ChatHandler(IOptions<ChatSettings> chatSettings)
|
||||||
{
|
{
|
||||||
private readonly OllamaApiClient _ollama;
|
_chatSettings = chatSettings.Value;
|
||||||
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
|
var uri = new Uri(chatSettings.Value.Url);
|
||||||
private readonly ChatSettings _chatSettings;
|
|
||||||
|
_ollama = new OllamaApiClient(uri)
|
||||||
public ChatHandler(IOptions<ChatSettings> chatSettings)
|
|
||||||
{
|
{
|
||||||
_chatSettings = chatSettings.Value;
|
SelectedModel = chatSettings.Value.Model
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
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;
|
||||||
@@ -19,13 +21,43 @@ 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.ConnectedUsers.Any(guildUser => guildUser.Id == _client.CurrentUser.Id) &&
|
||||||
|
channel.Users.Count != 1);
|
||||||
|
|
||||||
|
var statsList = new StringBuilder();
|
||||||
|
statsList.AppendLine("➡️ Servers");
|
||||||
|
|
||||||
|
foreach (var server in servers)
|
||||||
|
statsList.AppendLine($"* {server}");
|
||||||
|
|
||||||
|
statsList.AppendLine("➡️ Now playing channels: ");
|
||||||
|
|
||||||
|
foreach (var channel in channels)
|
||||||
|
statsList.AppendLine($"* {channel.Name} in {channel.Guild.Name}");
|
||||||
|
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.WithTitle("Lunaris Statistics")
|
||||||
|
.WithDescription(statsList.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);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,6 @@ public class DisconnectHandler(DiscordSocketClient client, IAudioService audioSe
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
await player.DisconnectAsync(cancellationToken).ConfigureAwait(false);
|
await player.DisconnectAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await context.RespondAsync("Disconnected.").ConfigureAwait(false);
|
await context.SendMessageAsync("Disconnected.", client).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,9 +68,9 @@ public static class Extensions
|
|||||||
await message.RespondAsync(content, ephemeral: true);
|
await message.RespondAsync(content, ephemeral: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static T? GetOptionValueByName<T>(this SocketSlashCommand command, string optionName)
|
||||||
public static string GetOptionValueByName(this SocketSlashCommand command, string optionName)
|
|
||||||
{
|
{
|
||||||
return command.Data.Options.FirstOrDefault(option => option.Name == optionName)?.Value.ToString() ?? string.Empty;
|
return (T?)(command.Data?.Options?
|
||||||
|
.FirstOrDefault(option => option.Name == optionName)?.Value ?? default(T));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System.Net;
|
||||||
using Discord;
|
using Discord;
|
||||||
|
using Discord.Net;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer;
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
@@ -65,20 +67,35 @@ public static class MessageModule
|
|||||||
|
|
||||||
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
||||||
{
|
{
|
||||||
if (value.Count <= 0)
|
if (value.Count <= 0)
|
||||||
return guildId;
|
return guildId;
|
||||||
|
|
||||||
foreach (var messageId in value)
|
// Create a copy of the list to avoid modifying it during iteration
|
||||||
|
var messagesToDelete = new List<ulong>(value);
|
||||||
|
|
||||||
|
foreach (var messageId in messagesToDelete)
|
||||||
{
|
{
|
||||||
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
try
|
||||||
if (messageToDelete != null)
|
{
|
||||||
await messageToDelete.DeleteAsync();
|
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
||||||
|
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
|
||||||
value.Clear();
|
value.Clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// If the guildId does not exist, add it to the dictionary
|
||||||
GuildMessageIds.Add(guildId, new List<ulong>());
|
GuildMessageIds.Add(guildId, new List<ulong>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET.Players.Queued;
|
||||||
using Lavalink4NET.Tracks;
|
using Lavalink4NET.Tracks;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer;
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
@@ -11,30 +12,38 @@ public class MusicEmbed
|
|||||||
string title,
|
string title,
|
||||||
string length,
|
string length,
|
||||||
string artist,
|
string artist,
|
||||||
string queuedBy)
|
string queuedBy,
|
||||||
|
string? nextSong = null)
|
||||||
{
|
{
|
||||||
|
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}")
|
.WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}{getNextSong}")
|
||||||
.WithColor(Color.Magenta)
|
.WithColor(Color.Magenta)
|
||||||
.WithThumbnailUrl(imageUrl)
|
.WithThumbnailUrl(imageUrl)
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task NowPlayingEmbed(
|
public async Task NowPlayingEmbed(
|
||||||
LavalinkTrack player,
|
LavalinkTrack track,
|
||||||
SocketSlashCommand context,
|
SocketSlashCommand context,
|
||||||
DiscordSocketClient client)
|
DiscordSocketClient client,
|
||||||
|
ITrackQueue? queue = null)
|
||||||
{
|
{
|
||||||
var artwork = player.ArtworkUri;
|
var duration = TimeSpan.Parse(track.Duration.ToString());
|
||||||
|
|
||||||
|
var artwork = track.ArtworkUri;
|
||||||
|
var nextSong = queue?.Count > 1 ? queue[1].Track?.Title : null;
|
||||||
var embed = SendMusicEmbed(
|
var embed = SendMusicEmbed(
|
||||||
artwork.ToString(),
|
artwork.ToString(),
|
||||||
player.Title,
|
track.Title,
|
||||||
player.Duration.ToString(),
|
duration.ToString(@"hh\:mm\:ss"),
|
||||||
player.Author,
|
track.Author,
|
||||||
context.User.Username);
|
context.User.Username,
|
||||||
|
nextSong);
|
||||||
|
|
||||||
await context.SendMessageAsync(embed, client);
|
await context.SendMessageAsync(embed, client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
Bot/Handler/MusicPlayer/PlayCommand/NormalizationFilter.cs
Normal file
30
Bot/Handler/MusicPlayer/PlayCommand/NormalizationFilter.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.SlashCommand;
|
using Lunaris2.SlashCommand;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Lavalink4NET;
|
using Lavalink4NET;
|
||||||
using Lavalink4NET.Events.Players;
|
using Lavalink4NET.Events.Players;
|
||||||
|
using Lavalink4NET.Integrations.SponsorBlock;
|
||||||
|
using Lavalink4NET.Integrations.SponsorBlock.Extensions;
|
||||||
using Lavalink4NET.Players.Queued;
|
using Lavalink4NET.Players.Queued;
|
||||||
using Lavalink4NET.Rest.Entities.Tracks;
|
using Lavalink4NET.Rest.Entities.Tracks;
|
||||||
|
using Lavalink4NET.Tracks;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
|
||||||
@@ -17,6 +21,8 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
private readonly IAudioService _audioService;
|
private readonly IAudioService _audioService;
|
||||||
private SocketSlashCommand _context;
|
private SocketSlashCommand _context;
|
||||||
private const int MaxTrackDuration = 30;
|
private const int MaxTrackDuration = 30;
|
||||||
|
private LavalinkTrack? _previousTrack;
|
||||||
|
private static readonly HashSet<ulong> SubscribedGuilds = new();
|
||||||
|
|
||||||
public PlayHandler(
|
public PlayHandler(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
@@ -26,18 +32,37 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
_client = client;
|
_client = client;
|
||||||
_musicEmbed = musicEmbed;
|
_musicEmbed = musicEmbed;
|
||||||
_audioService = audioService;
|
_audioService = audioService;
|
||||||
_audioService.TrackStarted += OnTrackStarted;
|
}
|
||||||
|
|
||||||
|
private async Task OnTrackEnded(object sender, TrackEndedEventArgs eventargs)
|
||||||
|
{
|
||||||
|
// Reset the previous track when the track ends.
|
||||||
|
_previousTrack = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
var track = player?.CurrentTrack;
|
|
||||||
|
|
||||||
if (track != null)
|
if (player?.CurrentTrack is null)
|
||||||
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
|
{
|
||||||
|
return; // No track is currently playing.
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTrack = player.CurrentTrack;
|
||||||
|
|
||||||
|
// Check if the current track is the same as the previous one
|
||||||
|
if (_previousTrack?.Identifier == currentTrack.Identifier)
|
||||||
|
{
|
||||||
|
// The same track is playing, so we don't need to create a new embed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track has changed, update the previous track and send the embed
|
||||||
|
_previousTrack = currentTrack;
|
||||||
|
await _musicEmbed.NowPlayingEmbed(currentTrack, _context, _client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
new Thread(PlayMusic).Start();
|
new Thread(PlayMusic).Start();
|
||||||
@@ -47,6 +72,8 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
RegisterTrackStartedEventListerner(command);
|
||||||
|
|
||||||
await _audioService.StartAsync(cancellationToken);
|
await _audioService.StartAsync(cancellationToken);
|
||||||
|
|
||||||
var context = command.Message;
|
var context = command.Message;
|
||||||
@@ -58,7 +85,7 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchQuery = context.GetOptionValueByName(Option.Input);
|
var searchQuery = context.GetOptionValueByName<string>(Option.Input);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(searchQuery))
|
if (string.IsNullOrWhiteSpace(searchQuery))
|
||||||
{
|
{
|
||||||
@@ -69,38 +96,66 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
await context.SendMessageAsync("📻 Searching...", _client);
|
await context.SendMessageAsync("📻 Searching...", _client);
|
||||||
|
|
||||||
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
|
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
if (player is null)
|
if (player is null)
|
||||||
return;
|
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 ApplyFilters(cancellationToken, player);
|
||||||
{
|
await ConfigureSponsorBlock(cancellationToken, player);
|
||||||
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)
|
var trackLoadOptions = new TrackLoadOptions
|
||||||
{
|
{
|
||||||
await player.PlayAsync(track, cancellationToken: cancellationToken)
|
SearchMode = TrackSearchMode.YouTubeMusic,
|
||||||
.ConfigureAwait(false);
|
};
|
||||||
|
|
||||||
await _musicEmbed.NowPlayingEmbed(track, context, _client);
|
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
|
else
|
||||||
{
|
{
|
||||||
var queueTracks = new[] { new TrackQueueItem(track) };
|
// It's just a single track or a search result.
|
||||||
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
|
var track = trackCollection.Track;
|
||||||
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
|
|
||||||
|
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)
|
catch (Exception error)
|
||||||
@@ -109,4 +164,33 @@ public class PlayHandler : IRequestHandler<PlayCommand>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterTrackStartedEventListerner(PlayCommand command)
|
||||||
|
{
|
||||||
|
if (SubscribedGuilds.Contains((ulong)command.Message.GuildId!))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_audioService.TrackStarted += OnTrackStarted;
|
||||||
|
_audioService.TrackEnded += OnTrackEnded;
|
||||||
|
SubscribedGuilds.Add((ulong)command.Message.GuildId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ApplyFilters(CancellationToken cancellationToken, QueuedLavalinkPlayer player)
|
||||||
|
{
|
||||||
|
var normalizationFilter = new NormalizationFilter(0.5, true);
|
||||||
|
player.Filters.SetFilter(normalizationFilter);
|
||||||
|
await player.Filters.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ConfigureSponsorBlock(CancellationToken cancellationToken, QueuedLavalinkPlayer player)
|
||||||
|
{
|
||||||
|
var categories = ImmutableArray.Create(
|
||||||
|
SegmentCategory.Intro,
|
||||||
|
SegmentCategory.Sponsor,
|
||||||
|
SegmentCategory.SelfPromotion,
|
||||||
|
SegmentCategory.Outro,
|
||||||
|
SegmentCategory.Filler);
|
||||||
|
|
||||||
|
await player.UpdateSponsorBlockCategoriesAsync(categories, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,29 @@ flowchart TD
|
|||||||
PlayTrack --> NowPlayingEmbed
|
PlayTrack --> NowPlayingEmbed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Bot
|
||||||
|
participant DiscordSocketClient
|
||||||
|
participant IAudioService
|
||||||
|
participant SocketSlashCommand
|
||||||
|
participant LavalinkPlayer
|
||||||
|
|
||||||
|
User->>Bot: /play [song]
|
||||||
|
Bot->>DiscordSocketClient: Get user voice channel
|
||||||
|
DiscordSocketClient-->>Bot: Voice channel info
|
||||||
|
Bot->>IAudioService: Get or create player
|
||||||
|
IAudioService-->>Bot: Player instance
|
||||||
|
Bot->>SocketSlashCommand: Get search query
|
||||||
|
SocketSlashCommand-->>Bot: Search query
|
||||||
|
Bot->>IAudioService: Load tracks
|
||||||
|
IAudioService-->>Bot: Track collection
|
||||||
|
Bot->>LavalinkPlayer: Play track
|
||||||
|
LavalinkPlayer-->>Bot: Track started
|
||||||
|
Bot->>User: Now playing embed
|
||||||
|
```
|
||||||
|
|
||||||
## Steps in the code
|
## Steps in the code
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
@@ -32,4 +55,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. |
|
||||||
|
|||||||
239
Bot/Handler/MusicPlayer/README.md
Normal file
239
Bot/Handler/MusicPlayer/README.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
### README.md
|
||||||
|
|
||||||
|
# Handlers
|
||||||
|
|
||||||
|
Handlers for the Lunaris2 bot, which is built using C#, Discord.Net, and Lavalink4NET. Below is a detailed description of each handler and their responsibilities.
|
||||||
|
|
||||||
|
## Handlers
|
||||||
|
|
||||||
|
### ClearQueueHandler
|
||||||
|
|
||||||
|
Handles the command to clear the music queue.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ClearQueueHandler : IRequestHandler<ClearQueueCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### DisconnectHandler
|
||||||
|
|
||||||
|
Handles the command to disconnect the bot from the voice channel.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DisconnectHandler : IRequestHandler<DisconnectCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### PauseHandler
|
||||||
|
|
||||||
|
Handles the command to pause the currently playing track.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PauseHandler : IRequestHandler<PauseCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### PlayHandler
|
||||||
|
|
||||||
|
Handles the command to play a track or playlist.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlayHandler : IRequestHandler<PlayCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ResumeHandler
|
||||||
|
|
||||||
|
Handles the command to resume the currently paused track.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ResumeHandler : IRequestHandler<ResumeCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### SkipHandler
|
||||||
|
|
||||||
|
Handles the command to skip the currently playing track.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SkipHandler : IRequestHandler<SkipCommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
### MessageReceivedHandler
|
||||||
|
|
||||||
|
Handles incoming messages and processes commands or statistics requests.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotification>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mermaid Diagrams
|
||||||
|
|
||||||
|
### Class Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User as User
|
||||||
|
participant DiscordSocketClient as DiscordSocketClient
|
||||||
|
participant MessageReceivedHandler as MessageReceivedHandler
|
||||||
|
participant MessageReceivedNotification as MessageReceivedNotification
|
||||||
|
participant EmbedBuilder as EmbedBuilder
|
||||||
|
participant Channel as Channel
|
||||||
|
|
||||||
|
User->>DiscordSocketClient: Send message "!LunarisStats"
|
||||||
|
DiscordSocketClient->>MessageReceivedHandler: MessageReceivedNotification
|
||||||
|
MessageReceivedHandler->>MessageReceivedNotification: Handle(notification, cancellationToken)
|
||||||
|
MessageReceivedNotification->>MessageReceivedHandler: BotMentioned(notification, cancellationToken)
|
||||||
|
MessageReceivedHandler->>DiscordSocketClient: Get guilds and voice channels
|
||||||
|
DiscordSocketClient-->>MessageReceivedHandler: List of guilds and voice channels
|
||||||
|
MessageReceivedHandler->>EmbedBuilder: Create embed with statistics
|
||||||
|
EmbedBuilder-->>MessageReceivedHandler: Embed
|
||||||
|
MessageReceivedHandler->>Channel: Send embed message
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequence Diagram for PlayHandler
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Bot
|
||||||
|
participant DiscordSocketClient
|
||||||
|
participant IAudioService
|
||||||
|
participant SocketSlashCommand
|
||||||
|
participant LavalinkPlayer
|
||||||
|
|
||||||
|
User->>Bot: /play [song]
|
||||||
|
Bot->>DiscordSocketClient: Get user voice channel
|
||||||
|
DiscordSocketClient-->>Bot: Voice channel info
|
||||||
|
Bot->>IAudioService: Get or create player
|
||||||
|
IAudioService-->>Bot: Player instance
|
||||||
|
Bot->>SocketSlashCommand: Get search query
|
||||||
|
SocketSlashCommand-->>Bot: Search query
|
||||||
|
Bot->>IAudioService: Load tracks
|
||||||
|
IAudioService-->>Bot: Track collection
|
||||||
|
Bot->>LavalinkPlayer: Play track
|
||||||
|
LavalinkPlayer-->>Bot: Track started
|
||||||
|
Bot->>User: Now playing embed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequence Diagram for MessageReceivedHandler
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Bot
|
||||||
|
participant DiscordSocketClient
|
||||||
|
participant ISender
|
||||||
|
participant MessageReceivedNotification
|
||||||
|
|
||||||
|
User->>Bot: Send message
|
||||||
|
Bot->>MessageReceivedNotification: Create notification
|
||||||
|
Bot->>DiscordSocketClient: Check if bot is mentioned
|
||||||
|
DiscordSocketClient-->>Bot: Mention info
|
||||||
|
alt Bot is mentioned
|
||||||
|
Bot->>ISender: Send ChatCommand
|
||||||
|
end
|
||||||
|
Bot->>DiscordSocketClient: Check for statistics command
|
||||||
|
alt Statistics command found
|
||||||
|
Bot->>DiscordSocketClient: Get server and channel info
|
||||||
|
DiscordSocketClient-->>Bot: Server and channel info
|
||||||
|
Bot->>User: Send statistics embed
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extensions.cs
|
||||||
|
|
||||||
|
#### Namespaces
|
||||||
|
- **Discord**: Provides classes for interacting with Discord.
|
||||||
|
- **Discord.WebSocket**: Provides WebSocket-specific classes for Discord.
|
||||||
|
- **Lavalink4NET**: Provides classes for interacting with Lavalink.
|
||||||
|
- **Lavalink4NET.Players**: Provides player-related classes for Lavalink.
|
||||||
|
- **Lavalink4NET.Players.Queued**: Provides queued player-related classes for Lavalink.
|
||||||
|
- **Microsoft.Extensions.Options**: Provides classes for handling options and configurations.
|
||||||
|
|
||||||
|
#### Class: `Extensions`
|
||||||
|
This static class contains extension methods for various Discord and Lavalink operations.
|
||||||
|
|
||||||
|
##### Method: `GetPlayerAsync`
|
||||||
|
- **Parameters**:
|
||||||
|
- `IAudioService audioService`: The audio service to retrieve the player from.
|
||||||
|
- `DiscordSocketClient client`: The Discord client.
|
||||||
|
- `SocketSlashCommand context`: The context of the slash command.
|
||||||
|
- `bool connectToVoiceChannel`: Whether to connect to the voice channel (default is true).
|
||||||
|
- **Returns**: `ValueTask<QueuedLavalinkPlayer?>`
|
||||||
|
- **Description**: Retrieves a `QueuedLavalinkPlayer` for the given context. If the retrieval fails, it returns null and sends an appropriate error message.
|
||||||
|
|
||||||
|
##### Method: `GetGuild`
|
||||||
|
- **Parameters**:
|
||||||
|
- `SocketSlashCommand message`: The slash command message.
|
||||||
|
- `DiscordSocketClient client`: The Discord client.
|
||||||
|
- **Returns**: `SocketGuild`
|
||||||
|
- **Description**: Retrieves the guild associated with the given slash command message. Throws an exception if the guild ID is null.
|
||||||
|
|
||||||
|
##### Method: `GetVoiceState`
|
||||||
|
- **Parameters**:
|
||||||
|
- `SocketSlashCommand message`: The slash command message.
|
||||||
|
- **Returns**: `IVoiceState`
|
||||||
|
- **Description**: Retrieves the voice state of the user who issued the slash command. Throws an exception if the user is not connected to a voice channel.
|
||||||
|
|
||||||
|
##### Method: `RespondAsync`
|
||||||
|
- **Parameters**:
|
||||||
|
- `SocketSlashCommand message`: The slash command message.
|
||||||
|
- `string content`: The content of the response.
|
||||||
|
- **Returns**: `Task`
|
||||||
|
- **Description**: Sends an ephemeral response to the slash command.
|
||||||
|
|
||||||
|
##### Method: `GetOptionValueByName`
|
||||||
|
- **Parameters**:
|
||||||
|
- `SocketSlashCommand command`: The slash command.
|
||||||
|
- `string optionName`: The name of the option to retrieve the value for.
|
||||||
|
- **Returns**: `string`
|
||||||
|
- **Description**: Retrieves the value of the specified option from the slash command. Returns an empty string if the option is not found.
|
||||||
|
|
||||||
|
# MessageModule
|
||||||
|
|
||||||
|
The `MessageModule` class provides utility methods for sending and removing messages in a Discord guild using the Discord.Net library. It maintains a dictionary to keep track of message IDs for each guild, allowing for easy removal of messages when needed.
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### `SendMessageAsync(SocketSlashCommand context, string message, DiscordSocketClient client)`
|
||||||
|
|
||||||
|
Sends a follow-up message with the specified text content in response to a slash command.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `context`: The `SocketSlashCommand` context in which the command was executed.
|
||||||
|
- `message`: The text content of the message to be sent.
|
||||||
|
- `client`: The `DiscordSocketClient` instance.
|
||||||
|
|
||||||
|
### `SendMessageAsync(SocketSlashCommand context, Embed message, DiscordSocketClient client)`
|
||||||
|
|
||||||
|
Sends a follow-up message with the specified embed content in response to a slash command.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `context`: The `SocketSlashCommand` context in which the command was executed.
|
||||||
|
- `message`: The `Embed` content of the message to be sent.
|
||||||
|
- `client`: The `DiscordSocketClient` instance.
|
||||||
|
|
||||||
|
### `RemoveMessages(SocketSlashCommand context, DiscordSocketClient client)`
|
||||||
|
|
||||||
|
Removes all tracked messages for the guild in which the command was executed.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `context`: The `SocketSlashCommand` context in which the command was executed.
|
||||||
|
- `client`: The `DiscordSocketClient` instance.
|
||||||
|
|
||||||
|
### `StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)`
|
||||||
|
|
||||||
|
Stores the message ID for removal and deletes any previously tracked messages for the guild.
|
||||||
|
|
||||||
|
- **Parameters:**
|
||||||
|
- `context`: The `SocketSlashCommand` context in which the command was executed.
|
||||||
|
- `client`: The `DiscordSocketClient` instance.
|
||||||
|
|
||||||
|
- **Returns:**
|
||||||
|
- The guild ID as a `ulong`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use the `MessageModule` class, simply call the appropriate method from your command handling logic. For example:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await context.SendMessageAsync("Hello, world!", client);
|
||||||
|
```
|
||||||
|
|
||||||
|
This will send a follow-up message with the text "Hello, world!" in response to the slash command.
|
||||||
30
Bot/Handler/Scheduler/ProcessMessageCommand.cs
Normal file
30
Bot/Handler/Scheduler/ProcessMessageCommand.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.Scheduler;
|
||||||
|
|
||||||
|
public class ProcessMessageCommand : IRequest
|
||||||
|
{
|
||||||
|
public ulong? Context { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProcessMessageHandler(DiscordSocketClient client) : IRequestHandler<ProcessMessageCommand>
|
||||||
|
{
|
||||||
|
public Task Handle(ProcessMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.Context == null)
|
||||||
|
return Task.FromResult(Unit.Value);
|
||||||
|
|
||||||
|
var channel = client.GetChannel(request.Context.Value) as ISocketMessageChannel;
|
||||||
|
|
||||||
|
if (channel == null)
|
||||||
|
return Task.FromResult(Unit.Value);
|
||||||
|
|
||||||
|
using var setTyping = channel.EnterTypingState();
|
||||||
|
channel.SendMessageAsync(request.Content);
|
||||||
|
setTyping.Dispose();
|
||||||
|
|
||||||
|
return Task.FromResult(Unit.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
Bot/Handler/Scheduler/ScheduleMessageCommand.cs
Normal file
137
Bot/Handler/Scheduler/ScheduleMessageCommand.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Hangfire;
|
||||||
|
using Lunaris2.Handler.ChatCommand;
|
||||||
|
using Lunaris2.Handler.MusicPlayer;
|
||||||
|
using Lunaris2.SlashCommand;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using NCrontab;
|
||||||
|
using OllamaSharp;
|
||||||
|
using static System.DateTime;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.Scheduler;
|
||||||
|
|
||||||
|
public record ScheduleMessageCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class ScheduleMessageHandler : IRequestHandler<ScheduleMessageCommand>
|
||||||
|
{
|
||||||
|
private readonly ChatSettings _chatSettings;
|
||||||
|
private readonly OllamaApiClient _ollama;
|
||||||
|
private readonly ISender _mediator;
|
||||||
|
|
||||||
|
private readonly string _cronInstruction = "You are only able to respond in CRON Format. " +
|
||||||
|
"Current time is: " + Now.ToString("yyyy-MM-dd HH:mm") + ". and it is " +
|
||||||
|
Now.DayOfWeek + ". " +
|
||||||
|
"Please use the a format parsable by ncrontab." +
|
||||||
|
"The user will describe the CRON format and you can only answer with the CRON format the user describes.";
|
||||||
|
|
||||||
|
private readonly string _dateInstruction = "You are only able to respond in Date Format. " +
|
||||||
|
"Current time is: " + Now.ToString("dd/MM/yyyy HH:mm:ss") + ". and it is " +
|
||||||
|
Now.DayOfWeek + ". " +
|
||||||
|
"Please use the following format: dd/MM/yyyy HH:mm:ss. Convert following to date string with the current time as a context";
|
||||||
|
|
||||||
|
public ScheduleMessageHandler(
|
||||||
|
IOptions<ChatSettings> chatSettings,
|
||||||
|
ISender mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
_chatSettings = chatSettings.Value;
|
||||||
|
|
||||||
|
var uri = new Uri(_chatSettings.Url);
|
||||||
|
_ollama = new OllamaApiClient(uri)
|
||||||
|
{
|
||||||
|
SelectedModel = _chatSettings.Model
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(ScheduleMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userDateInput = request.Message.GetOptionValueByName<string>(Option.Time);
|
||||||
|
var userMessage = request.Message.GetOptionValueByName<string>(Option.Message);
|
||||||
|
var recurring = request.Message.GetOptionValueByName<bool>(Option.IsRecurring);
|
||||||
|
|
||||||
|
if (recurring)
|
||||||
|
{
|
||||||
|
await ScheduleRecurringJob(request, userMessage, userDateInput, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ScheduleJob(request, userMessage, userDateInput, cancellationToken);
|
||||||
|
|
||||||
|
await request.Message.Channel.SendMessageAsync("Message scheduled successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScheduleRecurringJob(
|
||||||
|
ScheduleMessageCommand request,
|
||||||
|
string message,
|
||||||
|
string userDateInput,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var setTyping = request.Message.Channel.EnterTypingState();
|
||||||
|
var cron = string.Empty;
|
||||||
|
var jobManager = new RecurringJobManager();
|
||||||
|
const int retries = 5;
|
||||||
|
var userMessage = $"{_cronInstruction}: {userDateInput}";
|
||||||
|
|
||||||
|
for (var tries = 0; tries < retries; tries++)
|
||||||
|
{
|
||||||
|
var textToCronResponse = await GenerateResponse(userMessage, cancellationToken);
|
||||||
|
var isValid = CrontabSchedule.TryParse(textToCronResponse).ToString().IsNullOrEmpty();
|
||||||
|
|
||||||
|
if(isValid)
|
||||||
|
{
|
||||||
|
await request.Message.Channel.SendMessageAsync("Sorry, I didn't understand that date format. Please try again.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cron = textToCronResponse;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var recurringJobId = $"channel_{request.Message.ChannelId}_{request.Message.Id}";
|
||||||
|
|
||||||
|
jobManager.AddOrUpdate(
|
||||||
|
recurringJobId,
|
||||||
|
() => _mediator.Send(new ProcessMessageCommand { Context = request.Message.ChannelId, Content = message}, cancellationToken),
|
||||||
|
cron
|
||||||
|
);
|
||||||
|
|
||||||
|
setTyping.Dispose();
|
||||||
|
await request.Message.Channel.SendMessageAsync("Message scheduled successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScheduleJob(
|
||||||
|
ScheduleMessageCommand request,
|
||||||
|
string userMessage,
|
||||||
|
string executeAt,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var dateFormat = $"{_dateInstruction}: {executeAt}";
|
||||||
|
|
||||||
|
var formattedDate = await GenerateResponse(dateFormat, cancellationToken);
|
||||||
|
|
||||||
|
var date = ParseExact(formattedDate, "dd/MM/yyyy HH:mm:ss", CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
BackgroundJob.Schedule(
|
||||||
|
() => _mediator.Send(
|
||||||
|
new ProcessMessageCommand { Context = request.Message.ChannelId, Content = userMessage },
|
||||||
|
cancellationToken),
|
||||||
|
date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateResponse(string userMessage, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var response = new StringBuilder();
|
||||||
|
|
||||||
|
var chatContext = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? ""));
|
||||||
|
|
||||||
|
await chatContext.Send(userMessage, cancellationToken);
|
||||||
|
|
||||||
|
return response.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using Lunaris2.Handler.MusicPlayer.ClearQueueCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.PauseCommand;
|
using Lunaris2.Handler.MusicPlayer.PauseCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.ResumeCommand;
|
using Lunaris2.Handler.MusicPlayer.ResumeCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.SkipCommand;
|
using Lunaris2.Handler.MusicPlayer.SkipCommand;
|
||||||
|
using Lunaris2.Handler.Scheduler;
|
||||||
using Lunaris2.Notification;
|
using Lunaris2.Notification;
|
||||||
using Lunaris2.SlashCommand;
|
using Lunaris2.SlashCommand;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -32,6 +34,12 @@ 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;
|
||||||
|
case Command.Scheduler.Name:
|
||||||
|
await mediator.Send(new ScheduleMessageCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@@ -6,31 +6,51 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId>
|
<UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId>
|
||||||
|
<AssemblyVersion>0.0.1337</AssemblyVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Lavalink4net 4.0.25 seems to break the Message Module-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Discord.Net" Version="3.15.3" />
|
<PackageReference Include="Discord.Net" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Commands" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Commands" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Core" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Interactions" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Interactions" Version="3.16.0" />
|
||||||
<PackageReference Include="Discord.Net.Rest" Version="3.15.3" />
|
<PackageReference Include="Discord.Net.Rest" Version="3.16.0" />
|
||||||
<PackageReference Include="Lavalink4NET" Version="4.0.20" />
|
<PackageReference Include="Hangfire" Version="1.8.17" />
|
||||||
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.20" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.17" />
|
||||||
<PackageReference Include="Lavalink4NET.Discord.NET" Version="4.0.20" />
|
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
|
||||||
<PackageReference Include="MediatR" Version="12.4.0" />
|
<PackageReference Include="Lavalink4NET" Version="4.0.25" />
|
||||||
|
<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="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore" Version="2.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.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.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
|
<PackageReference Include="NCrontab" Version="3.3.3" />
|
||||||
<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>
|
||||||
|
<Resource Include="wwwroot\index.html">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Resource>
|
||||||
|
<Resource Include="wwwroot\logotype.png">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Resource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Lunaris2.Notification;
|
namespace Lunaris2.Notification;
|
||||||
|
|
||||||
|
|||||||
114
Bot/Program.cs
114
Bot/Program.cs
@@ -1,101 +1,27 @@
|
|||||||
using System.Reflection;
|
using Lavalink4NET.Integrations.SponsorBlock.Extensions;
|
||||||
using Discord;
|
using Lunaris2.Registration;
|
||||||
using Discord.Interactions;
|
|
||||||
using Discord.WebSocket;
|
|
||||||
using Lunaris2.Handler.ChatCommand;
|
|
||||||
using Lavalink4NET.Extensions;
|
|
||||||
using Lunaris2.Handler.MusicPlayer;
|
|
||||||
using Lunaris2.Notification;
|
|
||||||
using Lunaris2.Service;
|
|
||||||
using Lunaris2.SlashCommand;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Victoria.Node;
|
|
||||||
|
|
||||||
namespace Lunaris2;
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
public class Program
|
// Build configuration (using appsettings.json)
|
||||||
{
|
var configuration = new ConfigurationBuilder()
|
||||||
public static void Main(string[] args)
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
{
|
.AddJsonFile("appsettings.json")
|
||||||
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
.Build();
|
||||||
{
|
|
||||||
Console.WriteLine(eventArgs.ExceptionObject);
|
|
||||||
};
|
|
||||||
CreateHostBuilder(args).Build().Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
// Register your services
|
||||||
Host.CreateDefaultBuilder(args)
|
builder.Services.AddDiscordBot(configuration);
|
||||||
.ConfigureServices((_, services) =>
|
builder.Services.AddScheduler(configuration);
|
||||||
{
|
builder.Services.AddControllers();
|
||||||
var config = new DiscordSocketConfig
|
|
||||||
{
|
|
||||||
GatewayIntents = GatewayIntents.All
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = new DiscordSocketClient(config);
|
|
||||||
var configuration = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(AppContext.BaseDirectory)
|
|
||||||
.AddJsonFile("appsettings.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
services
|
var app = builder.Build();
|
||||||
.AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
|
|
||||||
.AddLavalink()
|
|
||||||
.ConfigureLavalink(options =>
|
|
||||||
{
|
|
||||||
options.BaseAddress = new Uri(
|
|
||||||
$"http://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}"
|
|
||||||
);
|
|
||||||
options.WebSocketUri = new Uri($"ws://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}/v4/websocket");
|
|
||||||
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
|
|
||||||
options.Label = "Node";
|
|
||||||
})
|
|
||||||
.AddSingleton<LavaNode>()
|
|
||||||
.AddSingleton<MusicEmbed>()
|
|
||||||
.AddSingleton<ChatSettings>()
|
|
||||||
.AddSingleton(client)
|
|
||||||
.AddSingleton<DiscordEventListener>()
|
|
||||||
.AddSingleton<VoiceChannelMonitorService>()
|
|
||||||
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
|
|
||||||
.Configure<ChatSettings>(configuration.GetSection("LLM"));
|
|
||||||
|
|
||||||
client.Ready += () => Client_Ready(client);
|
// Call your custom middleware (e.g., for SponsorBlock functionality)
|
||||||
client.Log += Log;
|
app.UseSponsorBlock();
|
||||||
|
|
||||||
client
|
|
||||||
.LoginAsync(TokenType.Bot, configuration["Token"])
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
|
|
||||||
client
|
|
||||||
.StartAsync()
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
|
|
||||||
var listener = services
|
// Serve static files
|
||||||
.BuildServiceProvider()
|
app.UseDefaultFiles();
|
||||||
.GetRequiredService<DiscordEventListener>();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
listener
|
|
||||||
.StartAsync()
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
});
|
|
||||||
|
|
||||||
private static Task Client_Ready(DiscordSocketClient client)
|
app.UseHangfireDashboardAndServer();
|
||||||
{
|
app.Run();
|
||||||
client.RegisterCommands();
|
|
||||||
|
|
||||||
new VoiceChannelMonitorService(client).StartMonitoring();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task Log(LogMessage arg)
|
|
||||||
{
|
|
||||||
Console.WriteLine(arg);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,15 @@
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
Program[Program] -->|Register| EventListener
|
Program[Program] -->|Register| EventListener
|
||||||
|
Program --> Intervals[VoiceChannelMonitorService]
|
||||||
|
Intervals --> SetStatus[SetStatus, Updates status with amount of playing bots]
|
||||||
|
Intervals --> LeaveChannel[LeaveOnAlone, Leaves channel when alone for a time]
|
||||||
EventListener[DiscordEventListener] --> A[MessageReceivedHandler]
|
EventListener[DiscordEventListener] --> A[MessageReceivedHandler]
|
||||||
|
|
||||||
EventListener[DiscordEventListener] --> A2[SlashCommandReceivedHandler]
|
EventListener[DiscordEventListener] --> A2[SlashCommandReceivedHandler]
|
||||||
|
|
||||||
A --> |Message| f{If bot is mentioned}
|
A --> |Message| f{If bot is mentioned}
|
||||||
|
A --> |Message '!LunarisStats'| p[Responds with Server and Channel Statistics.]
|
||||||
f --> |ChatCommand| v[ChatHandler]
|
f --> |ChatCommand| v[ChatHandler]
|
||||||
|
|
||||||
A2[SlashCommandReceivedHandler] -->|Message| C{Send to correct command by
|
A2[SlashCommandReceivedHandler] -->|Message| C{Send to correct command by
|
||||||
@@ -14,8 +18,11 @@ flowchart TD
|
|||||||
|
|
||||||
C -->|JoinCommand| D[JoinHandler]
|
C -->|JoinCommand| D[JoinHandler]
|
||||||
C -->|PlayCommand| E[PlayHandler]
|
C -->|PlayCommand| E[PlayHandler]
|
||||||
C -->|HelloCommand| F[HelloHandler]
|
C -->|PauseCommand| F[PauseHandler]
|
||||||
C -->|GoodbyeCommand| G[GoodbyeHandler]
|
C -->|DisconnectCommand| H[DisconnectHandler]
|
||||||
|
C -->|ResumeCommand| J[ResumeHandler]
|
||||||
|
C -->|SkipCommand| K[SkipHandler]
|
||||||
|
C -->|ClearQueueCommand| L[ClearQueueHandler]
|
||||||
```
|
```
|
||||||
Program registers an event listener ```DiscordEventListener``` which publish a message :
|
Program registers an event listener ```DiscordEventListener``` which publish a message :
|
||||||
|
|
||||||
@@ -30,20 +37,33 @@ await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken)
|
|||||||
|
|
||||||
## Handler integrations
|
## Handler integrations
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart LR
|
||||||
D[JoinHandler] --> Disc[Discord Api]
|
D[JoinHandler] --> Disc[Discord Api]
|
||||||
E[PlayHandler] --> Disc[Discord Api]
|
E[PlayHandler] --> Disc[Discord Api]
|
||||||
F[HelloHandler] --> Disc[Discord Api]
|
F[SkipHandler] --> Disc[Discord Api]
|
||||||
G[GoodbyeHandler] --> Disc[Discord Api]
|
G[PauseHandler] --> Disc[Discord Api]
|
||||||
v[ChatHandler] --> Disc[Discord Api]
|
v[ChatHandler] --> Disc[Discord Api]
|
||||||
|
ClearQueueHandler --> Disc
|
||||||
|
ClearQueuehandler --> Lava
|
||||||
|
DisconnectHandler --> Disc
|
||||||
|
Resumehandler --> Disc
|
||||||
v --> o[Ollama Server]
|
v --> o[Ollama Server]
|
||||||
o --> v
|
o --> v
|
||||||
E --> Lava[Lavalink]
|
E --> Lava[Lavalink]
|
||||||
|
F --> Lava
|
||||||
|
G --> Lava
|
||||||
```
|
```
|
||||||
|Name| Description |
|
|Name| Description |
|
||||||
|--|--|
|
|--|--|
|
||||||
| JoinHandler| Handles the logic for **just** joining a voice channel. |
|
| JoinHandler| Handles the logic for **just** joining a voice channel. |
|
||||||
| PlayHandler| Handles the logic for joining and playing music in a voice channel. |
|
| PlayHandler| Handles the logic for joining and playing music in a voice channel. |
|
||||||
| HelloHandler| Responds with Hello. (Dummy handler, will be removed)|
|
| PauseHandler | Handles the logic for pausing currently playing track. |
|
||||||
| GoodbyeHandler| Responds with Goodbye. (Dummy handler, will be removed)|
|
| DisconnectHandler | Handles the logic for disconnecting from voicechannels. |
|
||||||
|
| ClearQueueHandler | Handles the logic for clearing the queued songs, except the currently playing one. |
|
||||||
|
| SkipHandler | Handles the logic for skipping tracks that are queued. If 0 trackS is in queue, it stops the current one.|
|
||||||
|
| Resumehandler | Resumes paused tracks. |
|
||||||
| ChatHandler| Handles the logic for LLM chat with user. |
|
| ChatHandler| Handles the logic for LLM chat with user. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
Bot/Registration/ChatRegistration.cs
Normal file
14
Bot/Registration/ChatRegistration.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Lunaris2.Handler.ChatCommand;
|
||||||
|
|
||||||
|
namespace Lunaris2.Registration;
|
||||||
|
|
||||||
|
public static class ChatRegistration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddChat(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddSingleton<ChatSettings>();
|
||||||
|
services.Configure<ChatSettings>(configuration.GetSection("LLM"));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
Bot/Registration/DiscordBotRegistration.cs
Normal file
69
Bot/Registration/DiscordBotRegistration.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Lunaris2.Notification;
|
||||||
|
using Lunaris2.Service;
|
||||||
|
using Lunaris2.SlashCommand;
|
||||||
|
|
||||||
|
namespace Lunaris2.Registration;
|
||||||
|
|
||||||
|
public static class DiscordBotRegistration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddDiscordBot(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var config = new DiscordSocketConfig
|
||||||
|
{
|
||||||
|
GatewayIntents = GatewayIntents.All
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = new DiscordSocketClient(config);
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddMediatR(mediatRServiceConfiguration =>
|
||||||
|
mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
|
||||||
|
.AddMusicPlayer(configuration)
|
||||||
|
.AddSingleton(client)
|
||||||
|
.AddSingleton<DiscordEventListener>()
|
||||||
|
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
|
||||||
|
.AddChat(configuration);
|
||||||
|
|
||||||
|
client.Ready += () => Client_Ready(client);
|
||||||
|
client.Log += Log;
|
||||||
|
|
||||||
|
client
|
||||||
|
.LoginAsync(TokenType.Bot, configuration["Token"])
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
client
|
||||||
|
.StartAsync()
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
var listener = services
|
||||||
|
.BuildServiceProvider()
|
||||||
|
.GetRequiredService<DiscordEventListener>();
|
||||||
|
|
||||||
|
listener
|
||||||
|
.StartAsync()
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task Client_Ready(DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
client.RegisterCommands();
|
||||||
|
|
||||||
|
new VoiceChannelMonitorService(client).StartMonitoring();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task Log(LogMessage arg)
|
||||||
|
{
|
||||||
|
Console.WriteLine(arg);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Bot/Registration/HangfireRegistration.cs
Normal file
19
Bot/Registration/HangfireRegistration.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Hangfire;
|
||||||
|
|
||||||
|
namespace Lunaris2.Registration;
|
||||||
|
|
||||||
|
public static class HangfireRegistration
|
||||||
|
{
|
||||||
|
public static IApplicationBuilder UseHangfireDashboardAndServer(this IApplicationBuilder app, string dashboardPath = "/hangfire")
|
||||||
|
{
|
||||||
|
var dashboardOptions = new DashboardOptions
|
||||||
|
{
|
||||||
|
DarkModeEnabled = true,
|
||||||
|
DashboardTitle = "Lunaris Jobs Dashboard"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.UseHangfireDashboard(dashboardPath, dashboardOptions);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Bot/Registration/MusicPlayerRegistration.cs
Normal file
27
Bot/Registration/MusicPlayerRegistration.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Lavalink4NET.Extensions;
|
||||||
|
using Lunaris2.Handler.MusicPlayer;
|
||||||
|
using Lunaris2.Service;
|
||||||
|
|
||||||
|
namespace Lunaris2.Registration;
|
||||||
|
|
||||||
|
public static class MusicPlayerRegistration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddMusicPlayer(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddLavalink()
|
||||||
|
.ConfigureLavalink(options =>
|
||||||
|
{
|
||||||
|
options.BaseAddress = new Uri(
|
||||||
|
$"http://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}"
|
||||||
|
);
|
||||||
|
options.WebSocketUri = new Uri($"ws://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}/v4/websocket");
|
||||||
|
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
|
||||||
|
options.Label = "Node";
|
||||||
|
})
|
||||||
|
.AddSingleton<MusicEmbed>()
|
||||||
|
.AddSingleton<VoiceChannelMonitorService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Bot/Registration/SchedulerRegistration.cs
Normal file
26
Bot/Registration/SchedulerRegistration.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Hangfire;
|
||||||
|
using Hangfire.AspNetCore;
|
||||||
|
using Lunaris2.Handler.Scheduler;
|
||||||
|
|
||||||
|
namespace Lunaris2.Registration;
|
||||||
|
|
||||||
|
public static class SchedulerRegistration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddScheduler(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddHangfire((serviceProvider, config) =>
|
||||||
|
{
|
||||||
|
config.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
||||||
|
.UseSimpleAssemblyNameTypeSerializer();
|
||||||
|
|
||||||
|
config.UseSqlServerStorage(configuration.GetValue<string>("HangfireConnectionString"));
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddHangfireServer();
|
||||||
|
|
||||||
|
// Register your handler
|
||||||
|
// services.AddScoped<ScheduleMessageHandler>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
|
||||||
namespace Lunaris2.Service;
|
namespace Lunaris2.Service;
|
||||||
@@ -19,30 +20,64 @@ public class VoiceChannelMonitorService
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
await CheckVoiceChannels();
|
await CheckVoiceChannels();
|
||||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
await Task.Delay(TimeSpan.FromMinutes(1)); // Monitor every minute
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckVoiceChannels()
|
private async Task CheckVoiceChannels()
|
||||||
|
{
|
||||||
|
SetStatus();
|
||||||
|
await LeaveOnAlone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStatus()
|
||||||
|
{
|
||||||
|
var channels = _client.Guilds
|
||||||
|
.SelectMany(guild => guild.VoiceChannels)
|
||||||
|
.Count(channel =>
|
||||||
|
channel.ConnectedUsers
|
||||||
|
.Any(guildUser => guildUser.Id == _client.CurrentUser.Id) &&
|
||||||
|
channel.Users.Count > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (channels == 0)
|
||||||
|
_client.SetGameAsync(System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString(), type: ActivityType.CustomStatus);
|
||||||
|
else if(channels == 1)
|
||||||
|
_client.SetGameAsync("in 1 server", type: ActivityType.Playing);
|
||||||
|
else if(channels > 1)
|
||||||
|
_client.SetGameAsync($" in {channels} servers", type: ActivityType.Playing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LeaveOnAlone()
|
||||||
{
|
{
|
||||||
foreach (var guild in _client.Guilds)
|
foreach (var guild in _client.Guilds)
|
||||||
{
|
{
|
||||||
var voiceChannel = guild.VoiceChannels.FirstOrDefault(vc => vc.ConnectedUsers.Count == 1);
|
// 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 (voiceChannel != null)
|
||||||
{
|
{
|
||||||
|
// If timer not set for this channel, start one
|
||||||
if (!_timers.ContainsKey(voiceChannel.Id))
|
if (!_timers.ContainsKey(voiceChannel.Id))
|
||||||
{
|
{
|
||||||
_timers[voiceChannel.Id] = new Timer(async _ => await LeaveChannel(voiceChannel), null, TimeSpan.FromMinutes(3), Timeout.InfiniteTimeSpan);
|
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
|
else
|
||||||
{
|
{
|
||||||
if (voiceChannel == null || !_timers.ContainsKey(voiceChannel.Id))
|
// Clean up timer if channel is no longer active
|
||||||
continue;
|
var timersToDispose = _timers.Where(t => guild.VoiceChannels.All(vc => vc.Id != t.Key)).ToList();
|
||||||
|
foreach (var timer in timersToDispose)
|
||||||
await _timers[voiceChannel.Id].DisposeAsync();
|
{
|
||||||
_timers.Remove(voiceChannel.Id);
|
await timer.Value.DisposeAsync();
|
||||||
|
_timers.Remove(timer.Key);
|
||||||
|
Console.WriteLine($"Disposed timer for inactive voice channel ID: {timer.Key}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,9 +86,10 @@ public class VoiceChannelMonitorService
|
|||||||
{
|
{
|
||||||
if (voiceChannel.ConnectedUsers.Count == 1 && voiceChannel.Users.Any(u => u.Id == _client.CurrentUser.Id))
|
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 voiceChannel.DisconnectAsync();
|
||||||
await _timers[voiceChannel.Id].DisposeAsync();
|
await _timers[voiceChannel.Id].DisposeAsync();
|
||||||
_timers.Remove(voiceChannel.Id);
|
_timers.Remove(voiceChannel.Id); // Clean up after leaving
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@ namespace Lunaris2.SlashCommand;
|
|||||||
public static class Option
|
public static class Option
|
||||||
{
|
{
|
||||||
public const string Input = "input";
|
public const string Input = "input";
|
||||||
|
public const string Time = "time";
|
||||||
|
public const string IsRecurring = "repeating";
|
||||||
|
public const string Message = "message";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Command
|
public static class Command
|
||||||
@@ -15,6 +18,12 @@ public static class Command
|
|||||||
public const string Description = "Disconnect from the voice channel!";
|
public const string Description = "Disconnect from the voice channel!";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Clear
|
||||||
|
{
|
||||||
|
public const string Name = "clear";
|
||||||
|
public const string Description = "Clear the music queue!";
|
||||||
|
}
|
||||||
|
|
||||||
public static class Skip
|
public static class Skip
|
||||||
{
|
{
|
||||||
public const string Name = "skip";
|
public const string Name = "skip";
|
||||||
@@ -50,6 +59,38 @@ public static class Command
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Scheduler
|
||||||
|
{
|
||||||
|
public const string Name = "scheduler";
|
||||||
|
public const string Description = "Schedule a message";
|
||||||
|
|
||||||
|
public static readonly List<SlashCommandOptionBuilder>? Options =
|
||||||
|
[
|
||||||
|
new SlashCommandOptionBuilder
|
||||||
|
{
|
||||||
|
Name = "message",
|
||||||
|
Description = "The message you want to schedule",
|
||||||
|
Type = ApplicationCommandOptionType.String,
|
||||||
|
IsRequired = true
|
||||||
|
},
|
||||||
|
new SlashCommandOptionBuilder
|
||||||
|
{
|
||||||
|
Name = "time",
|
||||||
|
Description = "The time you want to schedule the message",
|
||||||
|
Type = ApplicationCommandOptionType.String,
|
||||||
|
IsRequired = true
|
||||||
|
},
|
||||||
|
new SlashCommandOptionBuilder
|
||||||
|
{
|
||||||
|
Name = "repeating",
|
||||||
|
Description = "Whether the message should repeat",
|
||||||
|
Type = ApplicationCommandOptionType.Boolean,
|
||||||
|
IsRequired = false,
|
||||||
|
IsDefault = false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public static string[] GetAllCommands()
|
public static string[] GetAllCommands()
|
||||||
{
|
{
|
||||||
return typeof(Command)
|
return typeof(Command)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public static class SlashCommandRegistration
|
|||||||
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.Resume.Name, Command.Resume.Description);
|
||||||
|
RegisterCommand(client, Command.Clear.Name, Command.Clear.Description);
|
||||||
|
RegisterCommand(client, Command.Scheduler.Name, Command.Scheduler.Description, Command.Scheduler.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterCommand(
|
private static void RegisterCommand(
|
||||||
|
|||||||
@@ -19,5 +19,6 @@
|
|||||||
"instruction": "Let's role play. Your name is Lunaris and can be quite rude. You are supposed to answer on following question with a maximum of 2 sentences:"
|
"instruction": "Let's role play. Your name is Lunaris and can be quite rude. You are supposed to answer on following question with a maximum of 2 sentences:"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"HangfireConnectionString": "Server=localhost, 1433;Database=Hangfire;User Id=sa;Password=SecretPassword!; TrustServerCertificate=True;"
|
||||||
}
|
}
|
||||||
|
|||||||
2
Bot/generate-trusted-session.sh
Normal file
2
Bot/generate-trusted-session.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
docker run quay.io/invidious/youtube-trusted-session-generator
|
||||||
|
read -p "Copy the codes and press enter to close the terminal."
|
||||||
55
Bot/wwwroot/index.html
Normal file
55
Bot/wwwroot/index.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Logotype Page</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #121212; /* Dark background */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #d3d3d3; /* Very light gray text */
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.logotype img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0px 0px 20px 20px black;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #d3d3d3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="logotype">
|
||||||
|
<img src="logotype.png" alt="Logotype">
|
||||||
|
</div>
|
||||||
|
<a href="/" id="hangfire-link">Go to Hangfire</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Update the link dynamically to include the current URL + /hangfire
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
document.getElementById('hangfire-link').href = currentUrl + 'hangfire';
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
Bot/wwwroot/logotype.png
Normal file
BIN
Bot/wwwroot/logotype.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
18
README.md
18
README.md
@@ -1,15 +1,17 @@
|
|||||||
# Lunaris2 - Discord Music Bot
|

|
||||||
|
|
||||||
|
# Lunaris - Discord BOT
|
||||||
|
|
||||||
Lunaris2 is a Discord bot designed to play music in your server's voice channels. It's built using C# and the Discord.Net library, and it uses the LavaLink music client for audio streaming.
|
Lunaris2 is a Discord bot designed to play music in your server's voice channels. It's built using C# and the Discord.Net library, and it uses the LavaLink music client for audio streaming.
|
||||||
|
|
||||||
## Features
|
## 🎮Features
|
||||||
|
|
||||||
- Play music from YouTube directly in your Discord server.
|
- Play music from YouTube directly in your Discord server.
|
||||||
- Skip tracks, pause, and resume playback.
|
- Skip tracks, pause, resume playback and more music related commands.
|
||||||
- Queue system to line up your favorite tracks.
|
- Queue system to line up your favorite tracks.
|
||||||
- Local LLM (AI chatbot) that answers on @mentions in Discord chat. See more about it below.
|
- Local LLM (AI chatbot) that answers on @mentions in Discord chat. See more about it below.
|
||||||
|
|
||||||
## Setup
|
## 🤖 Setup
|
||||||
|
|
||||||
1. Clone the repo.
|
1. Clone the repo.
|
||||||
2. Extract.
|
2. Extract.
|
||||||
@@ -27,7 +29,8 @@ The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). R
|
|||||||
|
|
||||||
## PM2 Setup
|
## PM2 Setup
|
||||||
- Install PM2 and configure it following their setup guide
|
- Install PM2 and configure it following their setup guide
|
||||||
#### Lavalink
|
|
||||||
|
#### 🐦🔥 Lavalink
|
||||||
* Download Lavalink 4.X.X (.jar)
|
* Download Lavalink 4.X.X (.jar)
|
||||||
* Install Java 17
|
* Install Java 17
|
||||||
|
|
||||||
@@ -46,6 +49,11 @@ Register the Lunaris bot with PM2:
|
|||||||
- `/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.
|
||||||
- `/skip`: Skips the currently playing song.
|
- `/skip`: Skips the currently playing song.
|
||||||
|
|
||||||
|
## Technical Documentations
|
||||||
|
- [Application Layout](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/README.md)
|
||||||
|
* 🤖 [AI CHAT](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/Handler/ChatCommand/readme.md)
|
||||||
|
* 🎵 [Music Player](https://github.com/Myxelium/Lunaris2.0/tree/master/Bot/Handler/MusicPlayer)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|||||||
@@ -2,7 +2,45 @@ server: # REST and WS server
|
|||||||
port: 2333
|
port: 2333
|
||||||
address: 0.0.0.0
|
address: 0.0.0.0
|
||||||
plugins:
|
plugins:
|
||||||
|
lavasrc:
|
||||||
|
providers: # Custom providers for track loading. This is the default
|
||||||
|
# - "dzisrc:%ISRC%" # Deezer ISRC provider
|
||||||
|
# - "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:
|
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.
|
enabled: true # Whether this source can be used.
|
||||||
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" 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.
|
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
|
||||||
@@ -11,19 +49,22 @@ plugins:
|
|||||||
# Clients are queried in the order they are given (so the first client is queried first and so on...)
|
# Clients are queried in the order they are given (so the first client is queried first and so on...)
|
||||||
clients:
|
clients:
|
||||||
- MUSIC
|
- MUSIC
|
||||||
- ANDROID_TESTSUITE
|
|
||||||
- WEB
|
- WEB
|
||||||
- TVHTML5EMBEDDED
|
- TVHTML5EMBEDDED
|
||||||
# name: # Name of the plugin
|
- ANDROID_TESTSUITE
|
||||||
# some_key: some_value # Some key-value pair for the plugin
|
|
||||||
# another_key: another_value
|
|
||||||
lavalink:
|
lavalink:
|
||||||
plugins:
|
plugins:
|
||||||
# - dependency: "group:artifact:version"
|
- dependency: com.github.devoxin:lavadspx-plugin:0.0.5 # replace {VERSION} with the latest version from the "Releases" tab.
|
||||||
# repository: "repository"
|
repository: https://jitpack.io
|
||||||
- dependency: "dev.lavalink.youtube:youtube-plugin:1.5.2"
|
- dependency: "dev.lavalink.youtube:youtube-plugin:1.8.3"
|
||||||
snapshot: false # Set to true if you want to use a snapshot version.
|
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:
|
||||||
@@ -56,20 +97,6 @@ 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:
|
||||||
@@ -79,9 +106,6 @@ metrics:
|
|||||||
sentry:
|
sentry:
|
||||||
dsn: ""
|
dsn: ""
|
||||||
environment: ""
|
environment: ""
|
||||||
# tags:
|
|
||||||
# some_key: some_value
|
|
||||||
# another_key: another_value
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
file:
|
file:
|
||||||
@@ -99,8 +123,7 @@ 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
|
||||||
@@ -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.7
|
image: ghcr.io/lavalink-devs/lavalink:4.0.8
|
||||||
container_name: lavalink
|
container_name: lavalink
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -64,6 +64,15 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- ollama-docker
|
- ollama-docker
|
||||||
|
|
||||||
|
mssql:
|
||||||
|
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||||
|
container_name: mssql
|
||||||
|
environment:
|
||||||
|
SA_PASSWORD: "SecretPassword!"
|
||||||
|
ACCEPT_EULA: "Y"
|
||||||
|
ports:
|
||||||
|
- "1433:1433"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ollama: {}
|
ollama: {}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,3 +1 @@
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
read -p "Press enter to continue"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user