Compare commits

...

3 Commits

Author SHA1 Message Date
Myx
ae1a4e14d6 Add scheduler 2025-02-18 01:15:55 +01:00
Myx
5726c110a1 Refactor 2025-02-17 03:23:41 +01:00
3362c6bf8c Update README.md 2024-10-25 23:28:55 +02:00
22 changed files with 609 additions and 241 deletions

View File

@@ -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;
public ChatHandler(IOptions<ChatSettings> chatSettings) _ollama = new OllamaApiClient(uri)
{ {
_chatSettings = chatSettings.Value; SelectedModel = chatSettings.Value.Model
var uri = new Uri(chatSettings.Value.Url); };
}
_ollama = new OllamaApiClient(uri) public async Task Handle(ChatCommand command, CancellationToken cancellationToken)
{ {
SelectedModel = chatSettings.Value.Model var channelId = command.Message.Channel.Id;
}; _chatContexts.TryAdd(channelId, null);
}
public async Task Handle(ChatCommand command, CancellationToken cancellationToken) 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))
{ {
var channelId = command.Message.Channel.Id; await command.Message.Channel.SendMessageAsync("Am I expected to read your mind?");
_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(); setTyping.Dispose();
return;
} }
private async Task<string> GenerateResponse(string userMessage, ulong channelId, CancellationToken cancellationToken) 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)
{ {
var response = new StringBuilder(); _chatContexts[channelId] = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? ""));
if (_chatContexts[channelId] == null)
{
_chatContexts[channelId] = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? ""));
}
await _chatContexts[channelId].Send(userMessage, cancellationToken);
return response.ToString();
} }
await _chatContexts[channelId].Send(userMessage, cancellationToken);
return response.ToString();
} }
} }

View File

@@ -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));
} }
} }

View File

@@ -85,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))
{ {

View 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);
}
}

View 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();
}
}

View File

@@ -4,6 +4,7 @@ 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;
@@ -36,6 +37,9 @@ public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandle
case Command.Clear.Name: case Command.Clear.Name:
await mediator.Send(new ClearQueueCommand(notification.Message), cancellationToken); await mediator.Send(new ClearQueueCommand(notification.Message), cancellationToken);
break; break;
case Command.Scheduler.Name:
await mediator.Send(new ScheduleMessageCommand(notification.Message), cancellationToken);
break;
} }
} }
} }

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
@@ -16,17 +16,25 @@
<PackageReference Include="Discord.Net.Core" Version="3.16.0" /> <PackageReference Include="Discord.Net.Core" Version="3.16.0" />
<PackageReference Include="Discord.Net.Interactions" Version="3.16.0" /> <PackageReference Include="Discord.Net.Interactions" Version="3.16.0" />
<PackageReference Include="Discord.Net.Rest" Version="3.16.0" /> <PackageReference Include="Discord.Net.Rest" Version="3.16.0" />
<PackageReference Include="Hangfire" Version="1.8.17" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.17" />
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
<PackageReference Include="Lavalink4NET" Version="4.0.25" /> <PackageReference Include="Lavalink4NET" Version="4.0.25" />
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.25" /> <PackageReference Include="Lavalink4NET.Artwork" Version="4.0.25" />
<PackageReference Include="Lavalink4NET.Discord.NET" 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.Lavasrc" Version="4.0.25" />
<PackageReference Include="Lavalink4NET.Integrations.SponsorBlock" Version="4.0.25" /> <PackageReference Include="Lavalink4NET.Integrations.SponsorBlock" Version="4.0.25" />
<PackageReference Include="MediatR" Version="12.4.1" /> <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.2" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" /> <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" />
</ItemGroup> </ItemGroup>
@@ -37,6 +45,12 @@
<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>

View File

@@ -1,6 +1,5 @@
using Discord.WebSocket; using Discord.WebSocket;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace Lunaris2.Notification; namespace Lunaris2.Notification;

View File

@@ -1,103 +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 Lavalink4NET.Integrations.SponsorBlock.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;
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);
};
var app = CreateHostBuilder(args).Build();
app.UseSponsorBlock(); // Register your services
app.Run(); builder.Services.AddDiscordBot(configuration);
} builder.Services.AddScheduler(configuration);
builder.Services.AddControllers();
private static IHostBuilder CreateHostBuilder(string[] args) => var app = builder.Build();
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
{
var config = new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.All
};
var client = new DiscordSocketClient(config); // Call your custom middleware (e.g., for SponsorBlock functionality)
var configuration = new ConfigurationBuilder() app.UseSponsorBlock();
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json")
.Build();
services // Serve static files
.AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) app.UseDefaultFiles();
.AddLavalink() app.UseStaticFiles();
.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<ChatSettings>()
.AddSingleton(client)
.AddSingleton<DiscordEventListener>()
.AddSingleton<VoiceChannelMonitorService>()
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
.Configure<ChatSettings>(configuration.GetSection("LLM"));
client.Ready += () => Client_Ready(client); app.UseHangfireDashboardAndServer();
client.Log += Log; app.Run();
client
.LoginAsync(TokenType.Bot, configuration["Token"])
.GetAwaiter()
.GetResult();
client
.StartAsync()
.GetAwaiter()
.GetResult();
var listener = services
.BuildServiceProvider()
.GetRequiredService<DiscordEventListener>();
listener
.StartAsync()
.GetAwaiter()
.GetResult();
});
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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

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

View File

@@ -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
@@ -56,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)

View File

@@ -14,6 +14,7 @@ public static class SlashCommandRegistration
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.Clear.Name, Command.Clear.Description);
RegisterCommand(client, Command.Scheduler.Name, Command.Scheduler.Description, Command.Scheduler.Options);
} }
private static void RegisterCommand( private static void RegisterCommand(

View File

@@ -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;"
} }

55
Bot/wwwroot/index.html Normal file
View 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>

View File

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

View File

@@ -1,17 +1,17 @@
![Lunaris Logotype](https://github.com/Myxelium/Lunaris2.0/blob/master/LOGOTYPE.png?raw=true) ![Lunaris Logotype](./Bot/wwwroot/logotype.png)
# Lunaris - Discord 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.
@@ -29,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
@@ -48,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.

View File

@@ -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: {}