mirror of
https://github.com/Myxelium/Lunaris2.0.git
synced 2026-04-17 03:35:53 +00:00
Compare commits
21 Commits
1713131952
...
Lavalink4n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34375f52bd | ||
| 05b7324ecc | |||
| d72676c7e0 | |||
| b30d47e351 | |||
| 3ce0df7eaf | |||
| e88e67f913 | |||
| 5053553182 | |||
| 327ccc9675 | |||
| cbc99c2773 | |||
| d56215f685 | |||
| 967bee923a | |||
| f8e6854569 | |||
| 03150a3d04 | |||
| 32b6e09336 | |||
| e3df4505fe | |||
| 3daf18e053 | |||
| 54c5c68ba6 | |||
| e16ff9cfaf | |||
| a1d20fd732 | |||
| 3d7655a902 | |||
| 8ddcf31da7 |
33
.github/workflows/dotnet.yml
vendored
33
.github/workflows/dotnet.yml
vendored
@@ -7,10 +7,13 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # required for github-action-get-previous-tag
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
@@ -27,15 +30,19 @@ jobs:
|
|||||||
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
|
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
|
||||||
|
|
||||||
- name: Zip the build
|
- name: Zip the build
|
||||||
run: 7z a -tzip ./out/Bot.zip ./out/*
|
run: 7z a -tzip ./out/Lunaris.zip ./out/*
|
||||||
|
|
||||||
- name: Get the tag name
|
- name: Get previous tag
|
||||||
id: get_tag
|
id: previoustag
|
||||||
run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}"
|
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Get the version
|
- name: Get next minor version
|
||||||
id: get_version
|
id: semver
|
||||||
run: echo "::set-output name=version::$(date +%s).${{ github.run_id }}"
|
uses: 'WyriHaximus/github-action-next-semvers@v1'
|
||||||
|
with:
|
||||||
|
version: ${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
@@ -43,8 +50,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.get_version.outputs.version }}
|
tag_name: ${{ steps.semver.outputs.patch }}
|
||||||
release_name: Release v${{ steps.get_version.outputs.version }}
|
release_name: Release ${{ steps.semver.outputs.patch }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
@@ -55,6 +62,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./out/Bot.zip
|
asset_path: ./out/Lunaris.zip
|
||||||
asset_name: Bot.zip
|
asset_name: Lunaris.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|||||||
68
Bot/Handler/ChatCommand/ChatHandler.cs
Normal file
68
Bot/Handler/ChatCommand/ChatHandler.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using OllamaSharp;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.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;
|
||||||
|
|
||||||
|
public ChatHandler(IOptions<ChatSettings> chatSettings)
|
||||||
|
{
|
||||||
|
_chatSettings = chatSettings.Value;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Bot/Handler/ChatCommand/ChatSettings.cs
Normal file
14
Bot/Handler/ChatCommand/ChatSettings.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Lunaris2.Handler.ChatCommand;
|
||||||
|
|
||||||
|
public class ChatSettings
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Model { get; set; }
|
||||||
|
public List<Personality> Personalities { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Personality
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Instruction { get; set; }
|
||||||
|
}
|
||||||
8
Bot/Handler/ChatCommand/readme.md
Normal file
8
Bot/Handler/ChatCommand/readme.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
## Ollama - Large Language Model Chat - Handler
|
||||||
|
|
||||||
|
This handler "owns" the logic for accessing the ollama api, which runs the transformer model.
|
||||||
|
|
||||||
|
> How to get started with a local chat bot see: [Run LLMs Locally using Ollama](https://marccodess.medium.com/run-llms-locally-using-ollama-8f04dd9b14f9)
|
||||||
|
|
||||||
|
Assuming you are on the same network as the Ollama server you should configure it to be accessible to other machines on the network, however this is only required if you aren't running it from localhost relative to the bot.
|
||||||
|
See: [How do I configure Ollama server?](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using Discord.WebSocket;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Lunaris2.Handler.GoodByeCommand;
|
|
||||||
|
|
||||||
public record GoodbyeCommand(SocketSlashCommand Message) : IRequest;
|
|
||||||
|
|
||||||
public class GoodbyeHandler : IRequestHandler<GoodbyeCommand>
|
|
||||||
{
|
|
||||||
public async Task Handle(GoodbyeCommand message, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await message.Message.RespondAsync($"Goodbye, {message.Message.User.Username}! :c");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using Discord.WebSocket;
|
|
||||||
using Lunaris2.SlashCommand;
|
|
||||||
using MediatR;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Lunaris2.Handler.HelloCommand;
|
|
||||||
|
|
||||||
public record HelloCommand(SocketSlashCommand Message) : IRequest;
|
|
||||||
|
|
||||||
public class HelloHandler : IRequestHandler<HelloCommand>
|
|
||||||
{
|
|
||||||
public async Task Handle(HelloCommand message, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Console.WriteLine(JsonConvert.SerializeObject(Command.GetAllCommands()));
|
|
||||||
|
|
||||||
await message.Message.RespondAsync($"Hello, {message.Message.User.Username}!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
using Lunaris2.Handler.GoodByeCommand;
|
using System.Text.RegularExpressions;
|
||||||
using Lunaris2.Handler.MusicPlayer.JoinCommand;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
|
||||||
using Lunaris2.Handler.MusicPlayer.SkipCommand;
|
|
||||||
using Lunaris2.Notification;
|
using Lunaris2.Notification;
|
||||||
using Lunaris2.SlashCommand;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Lunaris2.Handler;
|
namespace Lunaris2.Handler;
|
||||||
|
|
||||||
public class MessageReceivedHandler(ISender mediator) : INotificationHandler<MessageReceivedNotification>
|
public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotification>
|
||||||
{
|
{
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly ISender _mediatir;
|
||||||
|
|
||||||
|
public MessageReceivedHandler(DiscordSocketClient client, ISender mediatir)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_mediatir = mediatir;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
switch (notification.Message.CommandName)
|
await BotMentioned(notification, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
case Command.Hello.Name:
|
if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
|
||||||
await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken);
|
{
|
||||||
break;
|
// The bot was mentioned
|
||||||
case Command.Goodbye.Name:
|
const string pattern = "<.*?>";
|
||||||
await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken);
|
const string replacement = "";
|
||||||
break;
|
var regex = new Regex(pattern);
|
||||||
case Command.Join.Name:
|
var messageContent = regex.Replace(notification.Message.Content, replacement);
|
||||||
await mediator.Send(new JoinCommand(notification.Message), cancellationToken);
|
|
||||||
break;
|
await _mediatir.Send(new ChatCommand.ChatCommand(notification.Message, messageContent), cancellationToken);
|
||||||
case Command.Play.Name:
|
|
||||||
await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
|
|
||||||
break;
|
|
||||||
case Command.Skip.Name:
|
|
||||||
await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
||||||
|
|
||||||
|
public record DisconnectCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class DisconnectHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<DisconnectCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DisconnectCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var context = command.Message;
|
||||||
|
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await player.DisconnectAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await context.RespondAsync("Disconnected.").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,46 @@
|
|||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Victoria.Node;
|
using Lavalink4NET;
|
||||||
|
using Lavalink4NET.Players;
|
||||||
|
using Lavalink4NET.Players.Queued;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer;
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
public static async ValueTask<QueuedLavalinkPlayer?> GetPlayerAsync(
|
||||||
|
this IAudioService audioService,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
SocketSlashCommand context,
|
||||||
|
bool connectToVoiceChannel = true)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
|
var retrieveOptions = new PlayerRetrieveOptions(
|
||||||
|
ChannelBehavior: connectToVoiceChannel ? PlayerChannelBehavior.Join : PlayerChannelBehavior.None);
|
||||||
|
|
||||||
|
var playerOptions = new QueuedLavalinkPlayerOptions { HistoryCapacity = 10000 };
|
||||||
|
|
||||||
|
var result = await audioService.Players
|
||||||
|
.RetrieveAsync(context.GetGuild(client).Id, context.GetGuild(client).GetUser(context.User.Id).VoiceChannel.Id, playerFactory: PlayerFactory.Queued, Options.Create(playerOptions), retrieveOptions)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
var errorMessage = result.Status switch
|
||||||
|
{
|
||||||
|
PlayerRetrieveStatus.UserNotInVoiceChannel => "You are not connected to a voice channel.",
|
||||||
|
PlayerRetrieveStatus.BotNotConnected => "The bot is currently not connected.",
|
||||||
|
_ => "Unknown error.",
|
||||||
|
};
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Player;
|
||||||
|
}
|
||||||
|
|
||||||
public static SocketGuild GetGuild(this SocketSlashCommand message, DiscordSocketClient client)
|
public static SocketGuild GetGuild(this SocketSlashCommand message, DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
if (message.GuildId == null)
|
if (message.GuildId == null)
|
||||||
@@ -33,24 +68,6 @@ public static class Extensions
|
|||||||
await message.RespondAsync(content, ephemeral: true);
|
await message.RespondAsync(content, ephemeral: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task EnsureConnected(this LavaNode lavaNode)
|
|
||||||
{
|
|
||||||
if(!lavaNode.IsConnected)
|
|
||||||
await lavaNode.ConnectAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task JoinVoiceChannel(this SocketSlashCommand context, LavaNode lavaNode)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var textChannel = context.Channel as ITextChannel;
|
|
||||||
await lavaNode.JoinAsync(context.GetVoiceState().VoiceChannel, textChannel);
|
|
||||||
await context.RespondAsync($"Joined {context.GetVoiceState().VoiceChannel.Name}!");
|
|
||||||
}
|
|
||||||
catch (Exception exception) {
|
|
||||||
Console.WriteLine(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetOptionValueByName(this SocketSlashCommand command, string optionName)
|
public static string GetOptionValueByName(this SocketSlashCommand command, string optionName)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
using Discord.WebSocket;
|
|
||||||
using MediatR;
|
|
||||||
using Victoria.Node;
|
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer.JoinCommand;
|
|
||||||
|
|
||||||
public record JoinCommand(SocketSlashCommand Message) : IRequest;
|
|
||||||
|
|
||||||
public class JoinHandler : IRequestHandler<JoinCommand>
|
|
||||||
{
|
|
||||||
private readonly LavaNode _lavaNode;
|
|
||||||
private readonly DiscordSocketClient _client;
|
|
||||||
|
|
||||||
public JoinHandler(LavaNode lavaNode, DiscordSocketClient client)
|
|
||||||
{
|
|
||||||
_lavaNode = lavaNode;
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(JoinCommand command, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var context = command.Message;
|
|
||||||
|
|
||||||
await _lavaNode.EnsureConnected();
|
|
||||||
|
|
||||||
if (_lavaNode.HasPlayer(context.GetGuild(_client))) {
|
|
||||||
await context.RespondAsync("I'm already connected to a voice channel!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.JoinVoiceChannel(_lavaNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,65 @@
|
|||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.Handler.MusicPlayer;
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
|
|
||||||
public static class MessageModule
|
public static class MessageModule
|
||||||
{
|
{
|
||||||
private static Dictionary<ulong, List<ulong>> guildMessageIds = new Dictionary<ulong, List<ulong>>();
|
private static readonly Dictionary<ulong, List<ulong>> GuildMessageIds = new();
|
||||||
|
|
||||||
public static async Task SendMessageAsync(this SocketSlashCommand context, string message, DiscordSocketClient client)
|
public static async Task SendMessageAsync(this SocketSlashCommand context, string message, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var guildId = await StoreForRemoval(context, client);
|
var guildId = await StoreForRemoval(context, client);
|
||||||
|
|
||||||
await context.RespondAsync(message);
|
var sentMessage = await context.FollowupAsync(message);
|
||||||
var sentMessage = await context.GetOriginalResponseAsync();
|
GuildMessageIds[guildId].Add(sentMessage.Id);
|
||||||
|
}
|
||||||
guildMessageIds[guildId].Add(sentMessage.Id);
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task SendMessageAsync(this SocketSlashCommand context, Embed message, DiscordSocketClient client)
|
public static async Task SendMessageAsync(this SocketSlashCommand context, Embed message, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var guildId = await StoreForRemoval(context, client);
|
var guildId = await StoreForRemoval(context, client);
|
||||||
|
|
||||||
await context.RespondAsync(embed: message);
|
var sentMessage = await context.FollowupAsync(embed: message);
|
||||||
|
GuildMessageIds[guildId].Add(sentMessage.Id);
|
||||||
var sentMessage = await context.GetOriginalResponseAsync();
|
}
|
||||||
|
catch (Exception e)
|
||||||
guildMessageIds[guildId].Add(sentMessage.Id);
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
|
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
var guildId = context.GetGuild(client).Id;
|
var guildId = context.GetGuild(client).Id;
|
||||||
|
|
||||||
if (guildMessageIds.ContainsKey(guildId))
|
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
||||||
{
|
{
|
||||||
foreach (var messageId in guildMessageIds[guildId])
|
if (value.Count <= 0)
|
||||||
|
return guildId;
|
||||||
|
|
||||||
|
foreach (var messageId in value)
|
||||||
{
|
{
|
||||||
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
||||||
if (messageToDelete != null)
|
if (messageToDelete != null)
|
||||||
await messageToDelete.DeleteAsync();
|
await messageToDelete.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
guildMessageIds[guildId].Clear();
|
value.Clear();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
guildMessageIds.Add(guildId, []);
|
GuildMessageIds.Add(guildId, new List<ulong>());
|
||||||
}
|
}
|
||||||
|
|
||||||
return guildId;
|
return guildId;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Victoria;
|
using Lavalink4NET.Tracks;
|
||||||
using Victoria.Player;
|
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer;
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
|
|
||||||
@@ -12,32 +11,29 @@ public class MusicEmbed
|
|||||||
string title,
|
string title,
|
||||||
string length,
|
string length,
|
||||||
string artist,
|
string artist,
|
||||||
string queuedBy,
|
string queuedBy)
|
||||||
string? nextInQueue)
|
|
||||||
{
|
{
|
||||||
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}\nNext in queue: {nextInQueue}")
|
.WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}")
|
||||||
.WithColor(Color.Magenta)
|
.WithColor(Color.Magenta)
|
||||||
.WithThumbnailUrl(imageUrl)
|
.WithThumbnailUrl(imageUrl)
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task NowPlayingEmbed(
|
public async Task NowPlayingEmbed(
|
||||||
LavaPlayer<LavaTrack> player,
|
LavalinkTrack player,
|
||||||
SocketSlashCommand context,
|
SocketSlashCommand context,
|
||||||
DiscordSocketClient client)
|
DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
var artwork = await player.Track.FetchArtworkAsync();
|
var artwork = player.ArtworkUri;
|
||||||
var getNextTrack = player.Vueue.Count > 1 ? player.Vueue.ToArray()[1].Title : "No songs in queue.";
|
|
||||||
var embed = SendMusicEmbed(
|
var embed = SendMusicEmbed(
|
||||||
artwork,
|
artwork.ToString(),
|
||||||
player.Track.Title,
|
player.Title,
|
||||||
player.Track.Duration.ToString(),
|
player.Duration.ToString(),
|
||||||
player.Track.Author,
|
player.Author,
|
||||||
context.User.Username,
|
context.User.Username);
|
||||||
getNextTrack);
|
|
||||||
|
|
||||||
await context.SendMessageAsync(embed, client);
|
await context.SendMessageAsync(embed, client);
|
||||||
}
|
}
|
||||||
|
|||||||
31
Bot/Handler/MusicPlayer/PauseCommand/PauseHandler.cs
Normal file
31
Bot/Handler/MusicPlayer/PauseCommand/PauseHandler.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using Lavalink4NET.Players;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.PauseCommand;
|
||||||
|
|
||||||
|
public record PauseCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class PauseHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<PauseCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(PauseCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var context = command.Message;
|
||||||
|
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.State is PlayerState.Paused)
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("Player is already paused.", client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await player.PauseAsync(cancellationToken);
|
||||||
|
await context.SendMessageAsync("Paused.", client);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
using Discord;
|
|
||||||
using Discord.Commands;
|
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.SlashCommand;
|
using Lunaris2.SlashCommand;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Victoria.Node;
|
using Lavalink4NET;
|
||||||
using Victoria.Node.EventArgs;
|
using Lavalink4NET.Events.Players;
|
||||||
using Victoria.Player;
|
using Lavalink4NET.Players.Queued;
|
||||||
using Victoria.Responses.Search;
|
using Lavalink4NET.Rest.Entities.Tracks;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
|
||||||
@@ -15,105 +13,82 @@ public record PlayCommand(SocketSlashCommand Message) : IRequest;
|
|||||||
public class PlayHandler : IRequestHandler<PlayCommand>
|
public class PlayHandler : IRequestHandler<PlayCommand>
|
||||||
{
|
{
|
||||||
private readonly MusicEmbed _musicEmbed;
|
private readonly MusicEmbed _musicEmbed;
|
||||||
private readonly LavaNode _lavaNode;
|
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IAudioService _audioService;
|
||||||
private SocketSlashCommand _context;
|
private SocketSlashCommand _context;
|
||||||
|
|
||||||
public PlayHandler(
|
public PlayHandler(
|
||||||
LavaNode lavaNode,
|
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
MusicEmbed musicEmbed)
|
MusicEmbed musicEmbed,
|
||||||
|
IAudioService audioService)
|
||||||
{
|
{
|
||||||
_lavaNode = lavaNode;
|
|
||||||
_client = client;
|
_client = client;
|
||||||
_musicEmbed = musicEmbed;
|
_musicEmbed = musicEmbed;
|
||||||
|
_audioService = audioService;
|
||||||
|
_audioService.TrackStarted += OnTrackStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
|
||||||
|
{
|
||||||
|
var player = sender as QueuedLavalinkPlayer;
|
||||||
|
var track = player?.CurrentTrack;
|
||||||
|
|
||||||
|
if (track != null)
|
||||||
|
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(RunMode = RunMode.Async)]
|
|
||||||
public async Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
public async Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_context = command.Message;
|
await _audioService.StartAsync(cancellationToken);
|
||||||
|
var context = command.Message;
|
||||||
|
_context = context;
|
||||||
|
|
||||||
await _lavaNode.EnsureConnected();
|
var searchQuery = context.GetOptionValueByName(Option.Input);
|
||||||
|
|
||||||
var songName = _context.GetOptionValueByName(Option.Input);
|
if (string.IsNullOrWhiteSpace(searchQuery)) {
|
||||||
|
await context.SendMessageAsync("Please provide search terms.", _client);
|
||||||
if (string.IsNullOrWhiteSpace(songName)) {
|
|
||||||
await _context.RespondAsync("Please provide search terms.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var player = await GetPlayer();
|
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
if (player == null)
|
if (player is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var searchResponse = await _lavaNode.SearchAsync(
|
var trackLoadOptions = new TrackLoadOptions
|
||||||
Uri.IsWellFormedUriString(songName, UriKind.Absolute)
|
|
||||||
? SearchType.Direct
|
|
||||||
: SearchType.YouTube, songName);
|
|
||||||
|
|
||||||
if (!await SearchResponse(searchResponse, player, songName))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await PlayTrack(player);
|
|
||||||
|
|
||||||
await _musicEmbed.NowPlayingEmbed(player, _context, _client);
|
|
||||||
|
|
||||||
_lavaNode.OnTrackEnd += OnTrackEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnTrackEnd(TrackEndEventArg<LavaPlayer<LavaTrack>, LavaTrack> arg)
|
|
||||||
{
|
{
|
||||||
var player = arg.Player;
|
SearchMode = TrackSearchMode.YouTube,
|
||||||
if (!player.Vueue.TryDequeue(out var nextTrack))
|
};
|
||||||
return;
|
|
||||||
|
|
||||||
await player.PlayAsync(nextTrack);
|
var track = await _audioService.Tracks
|
||||||
|
.LoadTrackAsync(
|
||||||
|
searchQuery,
|
||||||
|
trackLoadOptions,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
await _musicEmbed.NowPlayingEmbed(player, _context, _client);
|
if (track is null)
|
||||||
}
|
await context.SendMessageAsync("😖 No results.", _client);
|
||||||
|
|
||||||
private static async Task PlayTrack(LavaPlayer<LavaTrack> player)
|
if (player.CurrentTrack is null)
|
||||||
{
|
{
|
||||||
if (player.PlayerState is PlayerState.Playing or PlayerState.Paused) {
|
await player
|
||||||
return;
|
.PlayAsync(track, cancellationToken: cancellationToken)
|
||||||
}
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
player.Vueue.TryDequeue(out var lavaTrack);
|
await _musicEmbed.NowPlayingEmbed(track, context, _client);
|
||||||
await player.PlayAsync(lavaTrack);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private async Task<LavaPlayer<LavaTrack>?> GetPlayer()
|
|
||||||
{
|
{
|
||||||
var voiceState = _context.User as IVoiceState;
|
if (track != null)
|
||||||
|
|
||||||
if (voiceState?.VoiceChannel != null)
|
|
||||||
return await _lavaNode.JoinAsync(voiceState.VoiceChannel, _context.Channel as ITextChannel);
|
|
||||||
|
|
||||||
await _context.RespondAsync("You must be connected to a voice channel!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> SearchResponse(
|
|
||||||
SearchResponse searchResponse, LavaPlayer<LavaTrack> player,
|
|
||||||
string songName)
|
|
||||||
{
|
{
|
||||||
if (searchResponse.Status is SearchStatus.LoadFailed or SearchStatus.NoMatches) {
|
var queueTracks = new[] { new TrackQueueItem(track) };
|
||||||
await _context.RespondAsync($"I wasn't able to find anything for `{songName}`.");
|
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
|
||||||
return false;
|
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync($"Couldn't read song information", _client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(searchResponse.Playlist.Name)) {
|
|
||||||
player.Vueue.Enqueue(searchResponse.Tracks);
|
|
||||||
|
|
||||||
await _context.RespondAsync($"Enqueued {searchResponse.Tracks.Count} songs.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var track = searchResponse.Tracks.FirstOrDefault()!;
|
|
||||||
player.Vueue.Enqueue(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,8 @@ flowchart TD
|
|||||||
PlayTrack --> NowPlayingEmbed
|
PlayTrack --> NowPlayingEmbed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Steps in the code
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|--|--|
|
|--|--|
|
||||||
| PlayHandler | Holds the logic for playing songs |
|
| PlayHandler | Holds the logic for playing songs |
|
||||||
@@ -19,13 +21,13 @@ flowchart TD
|
|||||||
|
|
||||||
There is also OnTrackEnd, when it get called an attempt is made to play the next song in queue.
|
There is also OnTrackEnd, when it get called an attempt is made to play the next song in queue.
|
||||||
|
|
||||||
Short explaination for some of the variables used:
|
## Short explaination for some of the variables used:
|
||||||
|
|
||||||
| Variable | Type | Description |
|
| Variable | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `_lavaNode` | `LavaNode` | An instance of the `LavaNode` class, used to interact with the LavaLink server for playing music in Discord voice channels. |
|
| `_lavaNode` | `LavaNode` | An instance of the `LavaNode` class, used to interact with the LavaLink server for playing music in Discord voice channels. |
|
||||||
| `_client` | `DiscordSocketClient` | An instance of the `DiscordSocketClient` class, used to interact with the Discord API for sending messages, joining voice channels, etc. |
|
| `_client` | `DiscordSocketClient` | An instance of the `DiscordSocketClient` class, used to interact with the Discord API for sending messages, joining voice channels, etc. |
|
||||||
| `_musicEmbed` | `MusicEmbed` | An instance of a custom `MusicEmbed` class, likely used to create and send rich embed messages related to the music player's current status. |
|
| `_musicEmbed` | `MusicEmbed` | An instance of a custom `MusicEmbed` class, used to create and send embed messages related to the music player's current status. |
|
||||||
| `context` | `SocketSlashCommand` | An instance of the `SocketSlashCommand` class, representing a slash command received from Discord. Used to get information about the command and to respond to it. |
|
| `context` | `SocketSlashCommand` | An instance of the `SocketSlashCommand` class, representing a slash command received from Discord. Used to get information about the command and to respond to it. |
|
||||||
| `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. |
|
||||||
|
|||||||
29
Bot/Handler/MusicPlayer/ResumeCommand/ResumeHandler.cs
Normal file
29
Bot/Handler/MusicPlayer/ResumeCommand/ResumeHandler.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using Lavalink4NET.Players;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.ResumeCommand;
|
||||||
|
|
||||||
|
public record ResumeCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class ResumeHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<ResumeCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(ResumeCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var context = command.Message;
|
||||||
|
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (player.State is not PlayerState.Paused)
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("Player is not paused.", client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await player.ResumeAsync(cancellationToken);
|
||||||
|
await context.SendMessageAsync("Resumed.", client);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +1,34 @@
|
|||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Victoria.Node;
|
|
||||||
using Victoria.Player;
|
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer.SkipCommand;
|
namespace Lunaris2.Handler.MusicPlayer.SkipCommand;
|
||||||
|
|
||||||
public record SkipCommand(SocketSlashCommand Message) : IRequest;
|
public record SkipCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
public class SkipHandler : IRequestHandler<SkipCommand>
|
public class SkipHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<SkipCommand>
|
||||||
{
|
{
|
||||||
private readonly LavaNode _lavaNode;
|
public async Task Handle(SkipCommand command, CancellationToken cancellationToken)
|
||||||
private readonly DiscordSocketClient _client;
|
|
||||||
private readonly MusicEmbed _musicEmbed;
|
|
||||||
|
|
||||||
public SkipHandler(LavaNode lavaNode, DiscordSocketClient client, MusicEmbed musicEmbed)
|
|
||||||
{
|
{
|
||||||
_lavaNode = lavaNode;
|
var context = command.Message;
|
||||||
_client = client;
|
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
|
||||||
_musicEmbed = musicEmbed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(SkipCommand message, CancellationToken cancellationToken)
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (player.CurrentItem is null)
|
||||||
{
|
{
|
||||||
var context = message.Message;
|
await context.SendMessageAsync("Nothing playing!", client).ConfigureAwait(false);
|
||||||
|
|
||||||
await _lavaNode.EnsureConnected();
|
|
||||||
|
|
||||||
if (!_lavaNode.TryGetPlayer(context.GetGuild(_client), out var player)) {
|
|
||||||
await context.RespondAsync("I'm not connected to a voice channel.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.PlayerState != PlayerState.Playing) {
|
await player.SkipAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
await context.RespondAsync("Woaaah there, I can't skip when nothing is playing.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
var track = player.CurrentItem;
|
||||||
await player.SkipAsync();
|
|
||||||
await _musicEmbed.NowPlayingEmbed(player, context, _client);
|
if (track is not null)
|
||||||
}
|
await context.SendMessageAsync($"Skipped. Now playing: {track.Track!.Title}", client).ConfigureAwait(false);
|
||||||
catch (Exception exception) {
|
else
|
||||||
await context.RespondAsync("There is not more tracks to skip.");
|
await context.SendMessageAsync("Skipped. Stopped playing because the queue is now empty.", client).ConfigureAwait(false);
|
||||||
Console.WriteLine(exception);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
37
Bot/Handler/SlashCommandReceivedHandler.cs
Normal file
37
Bot/Handler/SlashCommandReceivedHandler.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
||||||
|
using Lunaris2.Handler.MusicPlayer.PauseCommand;
|
||||||
|
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
using Lunaris2.Handler.MusicPlayer.ResumeCommand;
|
||||||
|
using Lunaris2.Handler.MusicPlayer.SkipCommand;
|
||||||
|
using Lunaris2.Notification;
|
||||||
|
using Lunaris2.SlashCommand;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler;
|
||||||
|
|
||||||
|
public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandler<SlashCommandReceivedNotification>
|
||||||
|
{
|
||||||
|
public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await notification.Message.DeferAsync();
|
||||||
|
|
||||||
|
switch (notification.Message.CommandName)
|
||||||
|
{
|
||||||
|
case Command.Resume.Name:
|
||||||
|
await mediator.Send(new ResumeCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
|
case Command.Pause.Name:
|
||||||
|
await mediator.Send(new PauseCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
|
case Command.Disconnect.Name:
|
||||||
|
await mediator.Send(new DisconnectCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
|
case Command.Play.Name:
|
||||||
|
await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
|
case Command.Skip.Name:
|
||||||
|
await mediator.Send(new SkipCommand(notification.Message), cancellationToken);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lunaris2.Helper;
|
|
||||||
|
|
||||||
public static class Async
|
|
||||||
{
|
|
||||||
public static void Run(Func<Task> task)
|
|
||||||
{
|
|
||||||
_ = Task.Run(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,16 +9,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Discord.Net" Version="3.13.1" />
|
<PackageReference Include="Discord.Net" Version="3.15.3" />
|
||||||
<PackageReference Include="Discord.Net.Commands" Version="3.13.1" />
|
<PackageReference Include="Discord.Net.Commands" Version="3.15.3" />
|
||||||
<PackageReference Include="Discord.Net.Core" Version="3.13.1" />
|
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
|
||||||
<PackageReference Include="Discord.Net.Interactions" Version="3.13.1" />
|
<PackageReference Include="Discord.Net.Interactions" Version="3.15.3" />
|
||||||
<PackageReference Include="Discord.Net.Rest" Version="3.13.1" />
|
<PackageReference Include="Discord.Net.Rest" Version="3.15.3" />
|
||||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
<PackageReference Include="Lavalink4NET" Version="4.0.20" />
|
||||||
|
<PackageReference Include="Lavalink4NET.Artwork" Version="4.0.20" />
|
||||||
|
<PackageReference Include="Lavalink4NET.Discord.NET" Version="4.0.20" />
|
||||||
|
<PackageReference Include="MediatR" Version="12.4.0" />
|
||||||
<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.Json" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
|
<PackageReference Include="OllamaSharp" Version="1.1.10" />
|
||||||
<PackageReference Include="Victoria" Version="6.0.23.324" />
|
<PackageReference Include="Victoria" Version="6.0.23.324" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||