Add scheduler

This commit is contained in:
Myx
2025-02-17 03:54:18 +01:00
parent 5726c110a1
commit ae1a4e14d6
22 changed files with 535 additions and 220 deletions

View File

@@ -4,12 +4,12 @@ using MediatR;
using Microsoft.Extensions.Options;
using OllamaSharp;
namespace Lunaris2.Handler.ChatCommand
{
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
namespace Lunaris2.Handler.ChatCommand;
public class ChatHandler : IRequestHandler<ChatCommand>
{
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
public class ChatHandler : IRequestHandler<ChatCommand>
{
private readonly OllamaApiClient _ollama;
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
private readonly ChatSettings _chatSettings;
@@ -64,5 +64,4 @@ namespace Lunaris2.Handler.ChatCommand
return response.ToString();
}
}
}

View File

@@ -68,9 +68,9 @@ public static class Extensions
await message.RespondAsync(content, ephemeral: true);
}
public static string GetOptionValueByName(this SocketSlashCommand command, string optionName)
public static T? GetOptionValueByName<T>(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;
}
var searchQuery = context.GetOptionValueByName(Option.Input);
var searchQuery = context.GetOptionValueByName<string>(Option.Input);
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.ResumeCommand;
using Lunaris2.Handler.MusicPlayer.SkipCommand;
using Lunaris2.Handler.Scheduler;
using Lunaris2.Notification;
using Lunaris2.SlashCommand;
using MediatR;
@@ -36,6 +37,9 @@ public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandle
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;
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -16,17 +16,25 @@
<PackageReference Include="Discord.Net.Core" Version="3.16.0" />
<PackageReference Include="Discord.Net.Interactions" 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.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.Binder" Version="8.0.2" />
<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="NCrontab" Version="3.3.3" />
<PackageReference Include="OllamaSharp" Version="1.1.10" />
</ItemGroup>
@@ -37,6 +45,12 @@
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Resource Include="wwwroot\index.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="wwwroot\logotype.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
</ItemGroup>
</Project>

View File

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

View File

@@ -1,33 +1,27 @@
using Lavalink4NET.Integrations.SponsorBlock.Extensions;
using Lunaris2.Registration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace Lunaris2;
var builder = WebApplication.CreateBuilder(args);
public class Program
{
public static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.ExceptionObject);
};
var app = CreateHostBuilder(args).Build();
app.UseSponsorBlock();
app.Run();
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
{
var configuration = new ConfigurationBuilder()
// Build configuration (using appsettings.json)
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json")
.Build();
services.AddDiscordBot(configuration);
});
}
// Register your services
builder.Services.AddDiscordBot(configuration);
builder.Services.AddScheduler(configuration);
builder.Services.AddControllers();
var app = builder.Build();
// Call your custom middleware (e.g., for SponsorBlock functionality)
app.UseSponsorBlock();
// Serve static files
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHangfireDashboardAndServer();
app.Run();

View File

@@ -1,6 +1,4 @@
using Lunaris2.Handler.ChatCommand;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Lunaris2.Registration;

View File

@@ -5,13 +5,11 @@ using Discord.WebSocket;
using Lunaris2.Notification;
using Lunaris2.Service;
using Lunaris2.SlashCommand;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Lunaris2.Registration
namespace Lunaris2.Registration;
public static class DiscordBotRegistration
{
public static class DiscordBotRegistration
{
public static IServiceCollection AddDiscordBot(this IServiceCollection services, IConfiguration configuration)
{
var config = new DiscordSocketConfig
@@ -68,5 +66,4 @@ namespace Lunaris2.Registration
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

@@ -1,8 +1,6 @@
using Lavalink4NET.Extensions;
using Lunaris2.Handler.MusicPlayer;
using Lunaris2.Service;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Lunaris2.Registration;

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,10 +1,10 @@
using Discord;
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();
@@ -92,5 +92,4 @@ namespace Lunaris2.Service
_timers.Remove(voiceChannel.Id); // Clean up after leaving
}
}
}
}

View File

@@ -5,6 +5,9 @@ namespace Lunaris2.SlashCommand;
public static class Option
{
public const string Input = "input";
public const string Time = "time";
public const string IsRecurring = "repeating";
public const string Message = "message";
}
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()
{
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.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(

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

View File

@@ -64,6 +64,15 @@ services:
networks:
- 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:
ollama: {}