mirror of
https://github.com/Myxelium/Lunaris2.0.git
synced 2026-04-13 16:10:36 +00:00
Compare commits
16 Commits
1717277838
...
Require-us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d69277868 | ||
| 8dcd4b334d | |||
| 9bcebea6b0 | |||
| d72676c7e0 | |||
| b30d47e351 | |||
| 3ce0df7eaf | |||
| e88e67f913 | |||
| 5053553182 | |||
| 327ccc9675 | |||
| cbc99c2773 | |||
| d56215f685 | |||
| 967bee923a | |||
| f8e6854569 | |||
| 03150a3d04 | |||
| 32b6e09336 | |||
| e3df4505fe |
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
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ namespace Lunaris2.Handler.ChatCommand
|
|||||||
{
|
{
|
||||||
private readonly OllamaApiClient _ollama;
|
private readonly OllamaApiClient _ollama;
|
||||||
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
|
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
|
||||||
|
private readonly ChatSettings _chatSettings;
|
||||||
|
|
||||||
public ChatHandler(IOptions<ChatSettings> chatSettings)
|
public ChatHandler(IOptions<ChatSettings> chatSettings)
|
||||||
{
|
{
|
||||||
|
_chatSettings = chatSettings.Value;
|
||||||
var uri = new Uri(chatSettings.Value.Url);
|
var uri = new Uri(chatSettings.Value.Url);
|
||||||
|
|
||||||
_ollama = new OllamaApiClient(uri)
|
_ollama = new OllamaApiClient(uri)
|
||||||
@@ -30,6 +32,10 @@ namespace Lunaris2.Handler.ChatCommand
|
|||||||
|
|
||||||
var userMessage = command.FilteredMessage;
|
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();
|
using var setTyping = command.Message.Channel.EnterTypingState();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(userMessage))
|
if (string.IsNullOrWhiteSpace(userMessage))
|
||||||
|
|||||||
@@ -4,4 +4,11 @@ public class ChatSettings
|
|||||||
{
|
{
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public string Model { 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}!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
var guildId = await StoreForRemoval(context, client);
|
try
|
||||||
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
var guildId = await StoreForRemoval(context, client);
|
try
|
||||||
|
{
|
||||||
|
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,11 @@
|
|||||||
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;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
|
||||||
@@ -15,105 +14,86 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(RunMode = RunMode.Async)]
|
private async Task OnTrackStarted(object sender, TrackStartedEventArgs eventargs)
|
||||||
public async Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
_context = command.Message;
|
var player = sender as QueuedLavalinkPlayer;
|
||||||
|
var track = player?.CurrentTrack;
|
||||||
|
|
||||||
await _lavaNode.EnsureConnected();
|
if (track != null)
|
||||||
|
await _musicEmbed.NowPlayingEmbed(track, _context, _client);
|
||||||
var songName = _context.GetOptionValueByName(Option.Input);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(songName)) {
|
|
||||||
await _context.RespondAsync("Please provide search terms.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = await GetPlayer();
|
|
||||||
|
|
||||||
if (player == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var searchResponse = await _lavaNode.SearchAsync(
|
|
||||||
Uri.IsWellFormedUriString(songName, UriKind.Absolute)
|
|
||||||
? SearchType.Direct
|
|
||||||
: SearchType.YouTube, songName);
|
|
||||||
|
|
||||||
if (!await SearchResponse(searchResponse, player, songName))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await PlayTrack(player);
|
|
||||||
|
|
||||||
await _musicEmbed.NowPlayingEmbed(player, _context, _client);
|
|
||||||
|
|
||||||
_lavaNode.OnTrackEnd += OnTrackEnd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnTrackEnd(TrackEndEventArg<LavaPlayer<LavaTrack>, LavaTrack> arg)
|
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var player = arg.Player;
|
new Thread(PlayMusic).Start();
|
||||||
if (!player.Vueue.TryDequeue(out var nextTrack))
|
return Task.CompletedTask;
|
||||||
return;
|
|
||||||
|
|
||||||
await player.PlayAsync(nextTrack);
|
async void PlayMusic()
|
||||||
|
{
|
||||||
|
var context = command.Message;
|
||||||
|
_context = context;
|
||||||
|
|
||||||
await _musicEmbed.NowPlayingEmbed(player, _context, _client);
|
if ((context.User as SocketGuildUser)?.VoiceChannel == null)
|
||||||
}
|
{
|
||||||
|
await context.SendMessageAsync("You must be in a voice channel to use this command.", _client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task PlayTrack(LavaPlayer<LavaTrack> player)
|
await _audioService.StartAsync(cancellationToken);
|
||||||
{
|
|
||||||
if (player.PlayerState is PlayerState.Playing or PlayerState.Paused) {
|
var searchQuery = context.GetOptionValueByName(Option.Input);
|
||||||
return;
|
|
||||||
|
if (string.IsNullOrWhiteSpace(searchQuery))
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("Please provide search terms.", _client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var player = await _audioService.GetPlayerAsync(_client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null) return;
|
||||||
|
|
||||||
|
var trackLoadOptions = new TrackLoadOptions { SearchMode = TrackSearchMode.YouTube, };
|
||||||
|
|
||||||
|
var track = await _audioService.Tracks.LoadTrackAsync(searchQuery, trackLoadOptions, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
if (track is null) await context.SendMessageAsync("😖 No results.", _client);
|
||||||
|
|
||||||
|
if (player.CurrentTrack is null)
|
||||||
|
{
|
||||||
|
await player.PlayAsync(track, cancellationToken: cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _musicEmbed.NowPlayingEmbed(track, context, _client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (track != null)
|
||||||
|
{
|
||||||
|
var queueTracks = new[] { new TrackQueueItem(track) };
|
||||||
|
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
|
||||||
|
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync($"Couldn't read song information", _client);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
player.Vueue.TryDequeue(out var lavaTrack);
|
|
||||||
await player.PlayAsync(lavaTrack);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<LavaPlayer<LavaTrack>?> GetPlayer()
|
|
||||||
{
|
|
||||||
var voiceState = _context.User as IVoiceState;
|
|
||||||
|
|
||||||
if (voiceState?.VoiceChannel != null)
|
|
||||||
return await _lavaNode.JoinAsync(voiceState.VoiceChannel, _context.Channel as ITextChannel);
|
|
||||||
|
|
||||||
await _context.RespondAsync("You must be connected to a voice channel!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> SearchResponse(
|
|
||||||
SearchResponse searchResponse, LavaPlayer<LavaTrack> player,
|
|
||||||
string songName)
|
|
||||||
{
|
|
||||||
if (searchResponse.Status is SearchStatus.LoadFailed or SearchStatus.NoMatches) {
|
|
||||||
await _context.RespondAsync($"I wasn't able to find anything for `{songName}`.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(searchResponse.Playlist.Name)) {
|
|
||||||
player.Vueue.Enqueue(searchResponse.Tracks);
|
|
||||||
|
|
||||||
await _context.RespondAsync($"Enqueued {searchResponse.Tracks.Count} songs.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var track = searchResponse.Tracks.FirstOrDefault()!;
|
|
||||||
player.Vueue.Enqueue(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
var context = message.Message;
|
|
||||||
|
|
||||||
await _lavaNode.EnsureConnected();
|
if (player.CurrentItem is null)
|
||||||
|
{
|
||||||
if (!_lavaNode.TryGetPlayer(context.GetGuild(_client), out var player)) {
|
await context.SendMessageAsync("Nothing playing!", client).ConfigureAwait(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Lunaris2.Handler.GoodByeCommand;
|
using Lunaris2.Handler.MusicPlayer.DisconnectCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.JoinCommand;
|
using Lunaris2.Handler.MusicPlayer.PauseCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
using Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
using Lunaris2.Handler.MusicPlayer.ResumeCommand;
|
||||||
using Lunaris2.Handler.MusicPlayer.SkipCommand;
|
using Lunaris2.Handler.MusicPlayer.SkipCommand;
|
||||||
using Lunaris2.Notification;
|
using Lunaris2.Notification;
|
||||||
using Lunaris2.SlashCommand;
|
using Lunaris2.SlashCommand;
|
||||||
@@ -12,16 +13,18 @@ public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandle
|
|||||||
{
|
{
|
||||||
public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken)
|
public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
await notification.Message.DeferAsync();
|
||||||
|
|
||||||
switch (notification.Message.CommandName)
|
switch (notification.Message.CommandName)
|
||||||
{
|
{
|
||||||
case Command.Hello.Name:
|
case Command.Resume.Name:
|
||||||
await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken);
|
await mediator.Send(new ResumeCommand(notification.Message), cancellationToken);
|
||||||
break;
|
break;
|
||||||
case Command.Goodbye.Name:
|
case Command.Pause.Name:
|
||||||
await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken);
|
await mediator.Send(new PauseCommand(notification.Message), cancellationToken);
|
||||||
break;
|
break;
|
||||||
case Command.Join.Name:
|
case Command.Disconnect.Name:
|
||||||
await mediator.Send(new JoinCommand(notification.Message), cancellationToken);
|
await mediator.Send(new DisconnectCommand(notification.Message), cancellationToken);
|
||||||
break;
|
break;
|
||||||
case Command.Play.Name:
|
case Command.Play.Name:
|
||||||
await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
|
await mediator.Send(new PlayCommand(notification.Message), cancellationToken);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Lunaris2.Helper;
|
|
||||||
|
|
||||||
public static class Async
|
|
||||||
{
|
|
||||||
public static void Run(Func<Task> task)
|
|
||||||
{
|
|
||||||
_ = Task.Run(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,13 +9,17 @@
|
|||||||
</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" />
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFacto
|
|||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnMessageReceivedAsync(SocketMessage arg)
|
private Task OnMessageReceivedAsync(SocketMessage arg)
|
||||||
{
|
{
|
||||||
await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
|
_ = Task.Run(() => Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken), _cancellationToken);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg)
|
private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg)
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.Commands;
|
|
||||||
using Discord.Interactions;
|
using Discord.Interactions;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Lunaris2.Handler.ChatCommand;
|
using Lunaris2.Handler.ChatCommand;
|
||||||
|
using Lavalink4NET.Extensions;
|
||||||
using Lunaris2.Handler.MusicPlayer;
|
using Lunaris2.Handler.MusicPlayer;
|
||||||
using Lunaris2.Notification;
|
using Lunaris2.Notification;
|
||||||
|
using Lunaris2.Service;
|
||||||
using Lunaris2.SlashCommand;
|
using Lunaris2.SlashCommand;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Victoria;
|
|
||||||
using Victoria.Node;
|
using Victoria.Node;
|
||||||
using RunMode = Discord.Commands.RunMode;
|
|
||||||
|
|
||||||
namespace Lunaris2;
|
namespace Lunaris2;
|
||||||
|
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
private static LavaNode? _lavaNode;
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine(eventArgs.ExceptionObject);
|
||||||
|
};
|
||||||
CreateHostBuilder(args).Build().Run();
|
CreateHostBuilder(args).Build().Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,30 +35,31 @@ public class Program
|
|||||||
GatewayIntents = GatewayIntents.All
|
GatewayIntents = GatewayIntents.All
|
||||||
};
|
};
|
||||||
|
|
||||||
var commandServiceConfig = new CommandServiceConfig{ DefaultRunMode = RunMode.Async };
|
|
||||||
|
|
||||||
var client = new DiscordSocketClient(config);
|
var client = new DiscordSocketClient(config);
|
||||||
var commands = new CommandService(commandServiceConfig);
|
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(AppContext.BaseDirectory)
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
.AddJsonFile("appsettings.json")
|
.AddJsonFile("appsettings.json")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
services.AddSingleton(client)
|
services
|
||||||
.AddSingleton(commands)
|
.AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
|
||||||
.AddMediatR(configuration => configuration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
|
.AddLavalink()
|
||||||
.AddSingleton<DiscordEventListener>()
|
.ConfigureLavalink(options =>
|
||||||
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
|
|
||||||
.AddLavaNode(nodeConfiguration =>
|
|
||||||
{
|
{
|
||||||
nodeConfiguration.SelfDeaf = false;
|
options.BaseAddress = new Uri(
|
||||||
nodeConfiguration.Hostname = configuration["LavaLinkHostname"];
|
$"http://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}"
|
||||||
nodeConfiguration.Port = Convert.ToUInt16(configuration["LavaLinkPort"]);
|
);
|
||||||
nodeConfiguration.Authorization = configuration["LavaLinkPassword"];
|
options.WebSocketUri = new Uri($"ws://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}/v4/websocket");
|
||||||
|
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
|
||||||
|
options.Label = "Node";
|
||||||
})
|
})
|
||||||
.AddSingleton<LavaNode>()
|
.AddSingleton<LavaNode>()
|
||||||
.AddSingleton<MusicEmbed>()
|
.AddSingleton<MusicEmbed>()
|
||||||
.AddSingleton<ChatSettings>()
|
.AddSingleton<ChatSettings>()
|
||||||
|
.AddSingleton(client)
|
||||||
|
.AddSingleton<DiscordEventListener>()
|
||||||
|
.AddSingleton<VoiceChannelMonitorService>()
|
||||||
|
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
|
||||||
.Configure<ChatSettings>(configuration.GetSection("LLM"));
|
.Configure<ChatSettings>(configuration.GetSection("LLM"));
|
||||||
|
|
||||||
client.Ready += () => Client_Ready(client);
|
client.Ready += () => Client_Ready(client);
|
||||||
@@ -72,8 +75,6 @@ public class Program
|
|||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
|
|
||||||
_lavaNode = services.BuildServiceProvider().GetRequiredService<LavaNode>();
|
|
||||||
|
|
||||||
var listener = services
|
var listener = services
|
||||||
.BuildServiceProvider()
|
.BuildServiceProvider()
|
||||||
.GetRequiredService<DiscordEventListener>();
|
.GetRequiredService<DiscordEventListener>();
|
||||||
@@ -84,10 +85,12 @@ public class Program
|
|||||||
.GetResult();
|
.GetResult();
|
||||||
});
|
});
|
||||||
|
|
||||||
private static async Task Client_Ready(DiscordSocketClient client)
|
private static Task Client_Ready(DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
await _lavaNode.ConnectAsync();
|
|
||||||
client.RegisterCommands();
|
client.RegisterCommands();
|
||||||
|
|
||||||
|
new VoiceChannelMonitorService(client).StartMonitoring();
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task Log(LogMessage arg)
|
private static Task Log(LogMessage arg)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ flowchart TD
|
|||||||
EventListener[DiscordEventListener] --> A2[SlashCommandReceivedHandler]
|
EventListener[DiscordEventListener] --> A2[SlashCommandReceivedHandler]
|
||||||
|
|
||||||
A --> |Message| f{If bot is mentioned}
|
A --> |Message| f{If bot is mentioned}
|
||||||
f --> v[ChatHandler]
|
f --> |ChatCommand| v[ChatHandler]
|
||||||
v --> o[Ollama Server]
|
|
||||||
o --> v
|
|
||||||
|
|
||||||
A2[SlashCommandReceivedHandler] -->|Message| C{Send to correct command by
|
A2[SlashCommandReceivedHandler] -->|Message| C{Send to correct command by
|
||||||
looking at commandName}
|
looking at commandName}
|
||||||
@@ -25,6 +23,11 @@ Program registers an event listener ```DiscordEventListener``` which publish a m
|
|||||||
await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
|
await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|Name| Description |
|
||||||
|
|--|--|
|
||||||
|
| SlashCommandReceivedHandler | Handles commands using ``/`` from any Discord Guild/Server. |
|
||||||
|
| MessageReceivedHandler| Listens to **all** messages. |
|
||||||
|
|
||||||
## Handler integrations
|
## Handler integrations
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -33,5 +36,14 @@ flowchart TD
|
|||||||
F[HelloHandler] --> Disc[Discord Api]
|
F[HelloHandler] --> Disc[Discord Api]
|
||||||
G[GoodbyeHandler] --> Disc[Discord Api]
|
G[GoodbyeHandler] --> Disc[Discord Api]
|
||||||
v[ChatHandler] --> Disc[Discord Api]
|
v[ChatHandler] --> Disc[Discord Api]
|
||||||
|
v --> o[Ollama Server]
|
||||||
|
o --> v
|
||||||
E --> Lava[Lavalink]
|
E --> Lava[Lavalink]
|
||||||
```
|
```
|
||||||
|
|Name| Description |
|
||||||
|
|--|--|
|
||||||
|
| JoinHandler| Handles the logic for **just** joining a voice channel. |
|
||||||
|
| PlayHandler| Handles the logic for joining and playing music in a voice channel. |
|
||||||
|
| HelloHandler| Responds with Hello. (Dummy handler, will be removed)|
|
||||||
|
| GoodbyeHandler| Responds with Goodbye. (Dummy handler, will be removed)|
|
||||||
|
| ChatHandler| Handles the logic for LLM chat with user. |
|
||||||
|
|||||||
59
Bot/Service/VoiceChannelMonitorService.cs
Normal file
59
Bot/Service/VoiceChannelMonitorService.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace Lunaris2.Service;
|
||||||
|
|
||||||
|
public class VoiceChannelMonitorService
|
||||||
|
{
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly Dictionary<ulong, Timer> _timers = new();
|
||||||
|
|
||||||
|
public VoiceChannelMonitorService(DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartMonitoring()
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await CheckVoiceChannels();
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckVoiceChannels()
|
||||||
|
{
|
||||||
|
foreach (var guild in _client.Guilds)
|
||||||
|
{
|
||||||
|
var voiceChannel = guild.VoiceChannels.FirstOrDefault(vc => vc.ConnectedUsers.Count == 1);
|
||||||
|
if (voiceChannel != null)
|
||||||
|
{
|
||||||
|
if (!_timers.ContainsKey(voiceChannel.Id))
|
||||||
|
{
|
||||||
|
_timers[voiceChannel.Id] = new Timer(async _ => await LeaveChannel(voiceChannel), null, TimeSpan.FromMinutes(3), Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (voiceChannel == null || !_timers.ContainsKey(voiceChannel.Id))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _timers[voiceChannel.Id].DisposeAsync();
|
||||||
|
_timers.Remove(voiceChannel.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LeaveChannel(SocketVoiceChannel voiceChannel)
|
||||||
|
{
|
||||||
|
if (voiceChannel.ConnectedUsers.Count == 1 && voiceChannel.Users.Any(u => u.Id == _client.CurrentUser.Id))
|
||||||
|
{
|
||||||
|
await voiceChannel.DisconnectAsync();
|
||||||
|
await _timers[voiceChannel.Id].DisposeAsync();
|
||||||
|
_timers.Remove(voiceChannel.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,22 +9,10 @@ public static class Option
|
|||||||
|
|
||||||
public static class Command
|
public static class Command
|
||||||
{
|
{
|
||||||
public static class Hello
|
public static class Disconnect
|
||||||
{
|
{
|
||||||
public const string Name = "hello";
|
public const string Name = "disconnect";
|
||||||
public const string Description = "Say hello to the bot!";
|
public const string Description = "Disconnect from the voice channel!";
|
||||||
}
|
|
||||||
|
|
||||||
public static class Goodbye
|
|
||||||
{
|
|
||||||
public const string Name = "goodbye";
|
|
||||||
public const string Description = "Say goodbye to the bot!";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Join
|
|
||||||
{
|
|
||||||
public const string Name = "join";
|
|
||||||
public const string Description = "Join the voice channel!";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Skip
|
public static class Skip
|
||||||
@@ -33,10 +21,16 @@ public static class Command
|
|||||||
public const string Description = "Skip the current song!";
|
public const string Description = "Skip the current song!";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Stop
|
public static class Resume
|
||||||
{
|
{
|
||||||
public const string Name = "stop";
|
public const string Name = "resume";
|
||||||
public const string Description = "Stop the music!";
|
public const string Description = "Resume the music!";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Pause
|
||||||
|
{
|
||||||
|
public const string Name = "pause";
|
||||||
|
public const string Description = "Pause the music!";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Play
|
public static class Play
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ public class SlashCommandBuilder(
|
|||||||
|
|
||||||
private static async Task RemoveUnusedCommands(string[] commands, IEnumerable<SocketApplicationCommand> registeredCommands)
|
private static async Task RemoveUnusedCommands(string[] commands, IEnumerable<SocketApplicationCommand> registeredCommands)
|
||||||
{
|
{
|
||||||
// Remove commands from Discord(registeredCommands) that are not in the list of commands
|
|
||||||
foreach(var command in registeredCommands)
|
foreach(var command in registeredCommands)
|
||||||
{
|
{
|
||||||
if (commands.Contains(command.Name))
|
if (commands.Contains(command.Name))
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ public static class SlashCommandRegistration
|
|||||||
{
|
{
|
||||||
public static void RegisterCommands(this DiscordSocketClient client)
|
public static void RegisterCommands(this DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
RegisterCommand(client, Command.Hello.Name, Command.Hello.Description);
|
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description);
|
||||||
RegisterCommand(client, Command.Goodbye.Name, Command.Goodbye.Description);
|
RegisterCommand(client, Command.Pause.Name, Command.Pause.Description);
|
||||||
RegisterCommand(client, Command.Join.Name, Command.Join.Description);
|
RegisterCommand(client, Command.Disconnect.Name, Command.Disconnect.Description);
|
||||||
RegisterCommand(client, Command.Skip.Name, Command.Skip.Description);
|
RegisterCommand(client, Command.Skip.Name, Command.Skip.Description);
|
||||||
RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options);
|
RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options);
|
||||||
RegisterCommand(client, Command.Stop.Name, Command.Stop.Description);
|
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterCommand(
|
private static void RegisterCommand(
|
||||||
|
|||||||
@@ -6,12 +6,18 @@
|
|||||||
"Microsoft": "Information"
|
"Microsoft": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Token": "discordToken",
|
"Token": "TOKEN",
|
||||||
"LavaLinkPassword": "youshallnotpass",
|
"LavaLinkPassword": "youshallnotpass",
|
||||||
"LavaLinkHostname": "127.0.0.1",
|
"LavaLinkHostname": "127.0.0.1",
|
||||||
"LavaLinkPort": 2333
|
"LavaLinkPort": 2333,
|
||||||
"LLM": {
|
"LLM": {
|
||||||
"Url": "http://192.168.50.54:11434",
|
"Url": "http://localhost:7869",
|
||||||
"Model": "gemma"
|
"Model": "gemma",
|
||||||
|
"personalities": [
|
||||||
|
{
|
||||||
|
"name": "Lunaris",
|
||||||
|
"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:"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -7,6 +7,7 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
|
|||||||
- 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, and resume playback.
|
||||||
- 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.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -17,6 +18,11 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
|
|||||||
5. Make sure you got docker installed. And run the file ``start-services.sh``, make sure you got git-bash installed.
|
5. Make sure you got docker installed. And run the file ``start-services.sh``, make sure you got git-bash installed.
|
||||||
6. Now you can start the project and run the application.
|
6. Now you can start the project and run the application.
|
||||||
|
|
||||||
|
## LLM
|
||||||
|
Lunaris supports AI chat using a large language model, this is done by hosting the LLM locally, in this case Docker will set it up for you when you run the start-services script.
|
||||||
|
|
||||||
|
The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). Running LLM locally requires much resources from your system, minimum requirements is at least 8GB of ram. If your don't have enought ram, select a LLM model in the [appsettings file](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json#L15) that requires less of your system.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- `/play <song>`: Plays the specified song in the voice channel you're currently in.
|
- `/play <song>`: Plays the specified song in the voice channel you're currently in.
|
||||||
@@ -25,7 +31,3 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
|
|||||||
## 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.
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](https://choosealicense.com/licenses/mit/)
|
|
||||||
|
|||||||
@@ -2,17 +2,32 @@ server: # REST and WS server
|
|||||||
port: 2333
|
port: 2333
|
||||||
address: 0.0.0.0
|
address: 0.0.0.0
|
||||||
plugins:
|
plugins:
|
||||||
# name: # Name of the plugin
|
youtube:
|
||||||
# some_key: some_value # Some key-value pair for the plugin
|
enabled: true # Whether this source can be used.
|
||||||
# another_key: another_value
|
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used.
|
||||||
|
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
|
||||||
|
allowDirectPlaylistIds: true # Whether just playlist IDs can match. If false, only complete URLs will be loaded.
|
||||||
|
# The clients to use for track loading. See below for a list of valid clients.
|
||||||
|
# Clients are queried in the order they are given (so the first client is queried first and so on...)
|
||||||
|
clients:
|
||||||
|
- MUSIC
|
||||||
|
- ANDROID_TESTSUITE
|
||||||
|
- WEB
|
||||||
|
- TVHTML5EMBEDDED
|
||||||
|
# name: # Name of the plugin
|
||||||
|
# some_key: some_value # Some key-value pair for the plugin
|
||||||
|
# another_key: another_value
|
||||||
lavalink:
|
lavalink:
|
||||||
plugins:
|
plugins:
|
||||||
# - dependency: "group:artifact:version"
|
# - dependency: "group:artifact:version"
|
||||||
# repository: "repository"
|
# repository: "repository"
|
||||||
|
- dependency: "dev.lavalink.youtube:youtube-plugin:1.5.2"
|
||||||
|
snapshot: false # Set to true if you want to use a snapshot version.
|
||||||
|
|
||||||
server:
|
server:
|
||||||
password: "youshallnotpass"
|
password: "youshallnotpass"
|
||||||
sources:
|
sources:
|
||||||
youtube: true
|
youtube: false
|
||||||
bandcamp: true
|
bandcamp: true
|
||||||
soundcloud: true
|
soundcloud: true
|
||||||
twitch: true
|
twitch: true
|
||||||
@@ -30,9 +45,9 @@ lavalink:
|
|||||||
rotation: true
|
rotation: true
|
||||||
channelMix: true
|
channelMix: true
|
||||||
lowPass: true
|
lowPass: true
|
||||||
bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses.
|
bufferDurationMs: 1000 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Duration <= 0 to disable JDA-NAS. Minimum of 40ms, lower values may introduce pauses.
|
||||||
frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered
|
frameBufferDurationMs: 10000 # How many milliseconds of audio to keep buffered
|
||||||
opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
|
opusEncodingQuality: 5 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
|
||||||
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
|
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
|
||||||
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
|
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
|
||||||
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
|
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
lavalink:
|
lavalink:
|
||||||
# pin the image version to Lavalink v4
|
# pin the image version to Lavalink v4
|
||||||
image: ghcr.io/lavalink-devs/lavalink:3.7.11
|
image: ghcr.io/lavalink-devs/lavalink:4.0.7
|
||||||
container_name: lavalink
|
container_name: lavalink
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -24,7 +24,52 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# you only need this if you want to make your lavalink accessible from outside of containers
|
# you only need this if you want to make your lavalink accessible from outside of containers
|
||||||
- "2333:2333"
|
- "2333:2333"
|
||||||
|
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
ports:
|
||||||
|
- 7869:11434
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
- ./ollama/ollama:/root/.ollama
|
||||||
|
container_name: ollama
|
||||||
|
pull_policy: always
|
||||||
|
tty: true
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- OLLAMA_KEEP_ALIVE=24h
|
||||||
|
- OLLAMA_HOST=0.0.0.0
|
||||||
|
networks:
|
||||||
|
- ollama-docker
|
||||||
|
|
||||||
|
ollama-webui:
|
||||||
|
image: ghcr.io/open-webui/open-webui:main
|
||||||
|
container_name: ollama-webui
|
||||||
|
volumes:
|
||||||
|
- ./ollama/ollama-webui:/app/backend/data
|
||||||
|
depends_on:
|
||||||
|
- ollama
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
environment: # https://docs.openwebui.com/getting-started/env-configuration#default_models
|
||||||
|
- OLLAMA_BASE_URLS=http://host.docker.internal:7869 #comma separated ollama hosts
|
||||||
|
- ENV=dev
|
||||||
|
- WEBUI_AUTH=False
|
||||||
|
- WEBUI_NAME=valiantlynx AI
|
||||||
|
- WEBUI_URL=http://localhost:8080
|
||||||
|
- WEBUI_SECRET_KEY=t0p-s3cr3t
|
||||||
|
extra_hosts:
|
||||||
|
- host.docker.internal:host-gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ollama-docker
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama: {}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# create a lavalink network you can add other containers to, to give them access to Lavalink
|
# create a lavalink network you can add other containers to, to give them access to Lavalink
|
||||||
lavalink:
|
lavalink:
|
||||||
name: lavalink
|
name: lavalink
|
||||||
|
ollama-docker:
|
||||||
|
external: false
|
||||||
@@ -1 +1,3 @@
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
|
read -p "Press enter to continue"
|
||||||
|
|||||||
Reference in New Issue
Block a user