mirror of
https://github.com/Myxelium/Lunaris2.0.git
synced 2026-08-22 13:51:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9730f32be0 | ||
|
|
1e2c10a7ea | ||
|
|
8dcd4b334d | ||
|
|
9bcebea6b0 | ||
|
|
d72676c7e0 | ||
|
|
b30d47e351 | ||
|
|
3ce0df7eaf | ||
|
|
e88e67f913 | ||
|
|
5053553182 | ||
|
|
327ccc9675 | ||
|
|
cbc99c2773 | ||
|
|
d56215f685 | ||
|
|
967bee923a | ||
|
|
f8e6854569 | ||
|
|
03150a3d04 | ||
|
|
32b6e09336 | ||
|
|
e3df4505fe | ||
|
|
3daf18e053 | ||
|
|
54c5c68ba6 | ||
|
|
e16ff9cfaf | ||
|
|
a1d20fd732 | ||
|
|
3d7655a902 | ||
|
|
8ddcf31da7 | ||
|
|
80a7c19b20 | ||
|
|
713715901b | ||
|
|
1a3a00f4ed | ||
|
|
0673653491 | ||
|
|
a650fd431e | ||
|
|
f6a763589e | ||
|
|
b7198bd01e | ||
|
|
f0678a4244 | ||
|
|
984d6b0af8 | ||
|
|
175f1f0d01 | ||
|
|
c8a1936f64 | ||
|
|
3f091ed7cc | ||
|
|
49872791b3 | ||
|
|
5a7b326dc2 | ||
|
|
0bc2a9be56 | ||
|
|
2d7ec0829e | ||
|
|
9b48536cd7 | ||
|
|
31c39e8893 | ||
|
|
521f808209 | ||
|
|
97667df53b | ||
|
|
95ab1281a4 | ||
|
|
f7b54ca80c | ||
|
|
3699a13cd9 | ||
|
|
13efaa7552 | ||
|
|
169b71c724 | ||
|
|
5b996ecd1d |
@@ -0,0 +1,67 @@
|
|||||||
|
name: .NET Core CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # required for github-action-get-previous-tag
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore ./Bot/Lunaris2.csproj
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build ./Bot/Lunaris2.csproj --no-restore -c Release -o ./out
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
|
||||||
|
|
||||||
|
- name: Zip the build
|
||||||
|
run: 7z a -tzip ./out/Lunaris.zip ./out/*
|
||||||
|
|
||||||
|
- name: Get previous tag
|
||||||
|
id: previoustag
|
||||||
|
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get next minor version
|
||||||
|
id: semver
|
||||||
|
uses: 'WyriHaximus/github-action-next-semvers@v1'
|
||||||
|
with:
|
||||||
|
version: ${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.semver.outputs.patch }}
|
||||||
|
release_name: Release ${{ steps.semver.outputs.patch }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Asset
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./out/Lunaris.zip
|
||||||
|
asset_name: Lunaris.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
obj
|
||||||
|
.idea
|
||||||
|
.vs
|
||||||
|
bin
|
||||||
|
appsettings.json
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Lunaris2.Notification;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
await BotMentioned(notification, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
|
||||||
|
{
|
||||||
|
// The bot was mentioned
|
||||||
|
const string pattern = "<.*?>";
|
||||||
|
const string replacement = "";
|
||||||
|
var regex = new Regex(pattern);
|
||||||
|
var messageContent = regex.Replace(notification.Message.Content, replacement);
|
||||||
|
|
||||||
|
await _mediatir.Send(new ChatCommand.ChatCommand(notification.Message, messageContent), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using Lavalink4NET.Players;
|
||||||
|
using Lavalink4NET.Players.Queued;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (message.GuildId == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Guild ID is null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.GetGuild(message.GuildId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IVoiceState GetVoiceState(this SocketSlashCommand message)
|
||||||
|
{
|
||||||
|
var voiceState = message.User as IVoiceState;
|
||||||
|
|
||||||
|
if (voiceState?.VoiceChannel == null)
|
||||||
|
{
|
||||||
|
throw new Exception("You must be connected to a voice channel!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return voiceState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RespondAsync(this SocketSlashCommand message, string content)
|
||||||
|
{
|
||||||
|
await message.RespondAsync(content, ephemeral: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static string GetOptionValueByName(this SocketSlashCommand command, string optionName)
|
||||||
|
{
|
||||||
|
return command.Data.Options.FirstOrDefault(option => option.Name == optionName)?.Value.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
|
|
||||||
|
public static class MessageModule
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<ulong, List<ulong>> GuildMessageIds = new();
|
||||||
|
|
||||||
|
public static async Task SendMessageAsync(this SocketSlashCommand context, string message, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var guildId = await StoreForRemoval(context, client);
|
||||||
|
|
||||||
|
var sentMessage = await context.FollowupAsync(message);
|
||||||
|
GuildMessageIds[guildId].Add(sentMessage.Id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SendMessageAsync(this SocketSlashCommand context, Embed message, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var guildId = await StoreForRemoval(context, client);
|
||||||
|
|
||||||
|
var sentMessage = await context.FollowupAsync(embed: message);
|
||||||
|
GuildMessageIds[guildId].Add(sentMessage.Id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RemoveMessages(this SocketSlashCommand context, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
var guildId = context.GetGuild(client).Id;
|
||||||
|
|
||||||
|
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
||||||
|
{
|
||||||
|
if (value.Count <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var messageId in value)
|
||||||
|
{
|
||||||
|
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
||||||
|
if (messageToDelete != null)
|
||||||
|
await messageToDelete.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ulong> StoreForRemoval(SocketSlashCommand context, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
var guildId = context.GetGuild(client).Id;
|
||||||
|
|
||||||
|
if (GuildMessageIds.TryGetValue(guildId, out var value))
|
||||||
|
{
|
||||||
|
if (value.Count <= 0)
|
||||||
|
return guildId;
|
||||||
|
|
||||||
|
foreach (var messageId in value)
|
||||||
|
{
|
||||||
|
var messageToDelete = await context.Channel.GetMessageAsync(messageId);
|
||||||
|
if (messageToDelete != null)
|
||||||
|
await messageToDelete.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GuildMessageIds.Add(guildId, new List<ulong>());
|
||||||
|
}
|
||||||
|
|
||||||
|
return guildId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET.Tracks;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer;
|
||||||
|
|
||||||
|
public class MusicEmbed
|
||||||
|
{
|
||||||
|
private Embed SendMusicEmbed(
|
||||||
|
string imageUrl,
|
||||||
|
string title,
|
||||||
|
string length,
|
||||||
|
string artist,
|
||||||
|
string queuedBy)
|
||||||
|
{
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.WithAuthor("Lunaris", "https://media.tenor.com/GqAwMt01UXgAAAAi/cd.gif")
|
||||||
|
.WithTitle(title)
|
||||||
|
.WithDescription($"Length: {length}\nArtist: {artist}\nQueued by: {queuedBy}")
|
||||||
|
.WithColor(Color.Magenta)
|
||||||
|
.WithThumbnailUrl(imageUrl)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NowPlayingEmbed(
|
||||||
|
LavalinkTrack player,
|
||||||
|
SocketSlashCommand context,
|
||||||
|
DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
var artwork = player.ArtworkUri;
|
||||||
|
var embed = SendMusicEmbed(
|
||||||
|
artwork.ToString(),
|
||||||
|
player.Title,
|
||||||
|
player.Duration.ToString(),
|
||||||
|
player.Author,
|
||||||
|
context.User.Username);
|
||||||
|
|
||||||
|
await context.SendMessageAsync(embed, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using Lunaris2.SlashCommand;
|
||||||
|
using MediatR;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using Lavalink4NET.Events.Players;
|
||||||
|
using Lavalink4NET.Players.Queued;
|
||||||
|
using Lavalink4NET.Rest.Entities.Tracks;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.PlayCommand;
|
||||||
|
|
||||||
|
public record PlayCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class PlayHandler : IRequestHandler<PlayCommand>
|
||||||
|
{
|
||||||
|
private readonly MusicEmbed _musicEmbed;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IAudioService _audioService;
|
||||||
|
private SocketSlashCommand _context;
|
||||||
|
private const int MaxTrackDuration = 30;
|
||||||
|
|
||||||
|
public PlayHandler(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
MusicEmbed musicEmbed,
|
||||||
|
IAudioService audioService)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Handle(PlayCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
new Thread(PlayMusic).Start();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
async void PlayMusic()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _audioService.StartAsync(cancellationToken);
|
||||||
|
|
||||||
|
var context = command.Message;
|
||||||
|
_context = context;
|
||||||
|
|
||||||
|
if ((context.User as SocketGuildUser)?.VoiceChannel == null)
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("You must be in a voice channel to use this command.", _client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchQuery = context.GetOptionValueByName(Option.Input);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(searchQuery))
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("Please provide search terms.", _client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SendMessageAsync("📻 Searching...", _client);
|
||||||
|
|
||||||
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.CurrentTrack?.Duration.TotalMinutes > MaxTrackDuration)
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync($"🔈 Sorry the track is longer than { MaxTrackDuration } minutes, to save resources this limit is active for now.", _client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.CurrentTrack is null)
|
||||||
|
{
|
||||||
|
await player.PlayAsync(track, cancellationToken: cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _musicEmbed.NowPlayingEmbed(track, context, _client);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var queueTracks = new[] { new TrackQueueItem(track) };
|
||||||
|
await player.Queue.AddRangeAsync(queueTracks, cancellationToken);
|
||||||
|
await context.SendMessageAsync($"🔈 Added to queue: {track.Title}", _client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
throw new Exception("Error occured in the Play handler!", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
PlayHandler --> EnsureConnected
|
||||||
|
EnsureConnected --> GetPlayer
|
||||||
|
GetPlayer --> SearchAsync
|
||||||
|
SearchAsync --> SearchResponse
|
||||||
|
SearchResponse --> PlayTrack
|
||||||
|
PlayTrack --> NowPlayingEmbed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steps in the code
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
|--|--|
|
||||||
|
| PlayHandler | Holds the logic for playing songs |
|
||||||
|
| GetPlayer | Joins voice channel, produces chat resposne |
|
||||||
|
| EnsureConnected | Makes sure the client is connected |
|
||||||
|
| SearchAsync | Searches for songs information |
|
||||||
|
| SearchResponse | Handling possible errors from the response of SearchAsync |
|
||||||
|
| PlayTrack | Plays the song |
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
| 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. |
|
||||||
|
| `_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, 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. |
|
||||||
|
| `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. |
|
||||||
|
| `songName` | `string` | A string that represents the name or URL of a song to play. Used to search for and queue tracks. |
|
||||||
|
| `searchResponse` | `SearchResponse` | An instance of the `SearchResponse` class, representing the result of a search for tracks. Used to get the tracks that were found and queue them in the player. |
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using Lavalink4NET;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Handler.MusicPlayer.SkipCommand;
|
||||||
|
|
||||||
|
public record SkipCommand(SocketSlashCommand Message) : IRequest;
|
||||||
|
|
||||||
|
public class SkipHandler(DiscordSocketClient client, IAudioService audioService) : IRequestHandler<SkipCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(SkipCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var context = command.Message;
|
||||||
|
var player = await audioService.GetPlayerAsync(client, context, connectToVoiceChannel: true);
|
||||||
|
|
||||||
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (player.CurrentItem is null)
|
||||||
|
{
|
||||||
|
await context.SendMessageAsync("Nothing playing!", client).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await player.SkipAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var track = player.CurrentItem;
|
||||||
|
|
||||||
|
if (track is not null)
|
||||||
|
await context.SendMessageAsync($"Skipped. Now playing: {track.Track!.Title}", client).ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
await context.SendMessageAsync("Skipped. Stopped playing because the queue is now empty.", client).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UserSecretsId>ec2f340f-a44c-4869-ab79-a12ba9459d80</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Discord.Net" Version="3.15.3" />
|
||||||
|
<PackageReference Include="Discord.Net.Commands" Version="3.15.3" />
|
||||||
|
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
|
||||||
|
<PackageReference Include="Discord.Net.Interactions" Version="3.15.3" />
|
||||||
|
<PackageReference Include="Discord.Net.Rest" Version="3.15.3" />
|
||||||
|
<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.Binder" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" 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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Lunaris2.Notification;
|
||||||
|
|
||||||
|
public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope)
|
||||||
|
{
|
||||||
|
private readonly CancellationToken _cancellationToken = new CancellationTokenSource().Token;
|
||||||
|
|
||||||
|
private IMediator Mediator
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var scope = serviceScope.CreateScope();
|
||||||
|
return scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
client.SlashCommandExecuted += OnSlashCommandRecievedAsync;
|
||||||
|
client.MessageReceived += OnMessageReceivedAsync;
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnMessageReceivedAsync(SocketMessage arg)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() => Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken), _cancellationToken);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg)
|
||||||
|
{
|
||||||
|
await Mediator.Publish(new SlashCommandReceivedNotification(arg), _cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Notification;
|
||||||
|
|
||||||
|
public class MessageReceivedNotification(SocketMessage message) : INotification
|
||||||
|
{
|
||||||
|
public SocketMessage Message { get; } = message ?? throw new ArgumentNullException(nameof(message));
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Lunaris2.Notification;
|
||||||
|
|
||||||
|
public class SlashCommandReceivedNotification(SocketSlashCommand message) : INotification
|
||||||
|
{
|
||||||
|
public SocketSlashCommand Message { get; } = message ?? throw new ArgumentNullException(nameof(message));
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Lunaris2.Handler.ChatCommand;
|
||||||
|
using Lavalink4NET.Extensions;
|
||||||
|
using Lunaris2.Handler.MusicPlayer;
|
||||||
|
using Lunaris2.Notification;
|
||||||
|
using Lunaris2.Service;
|
||||||
|
using Lunaris2.SlashCommand;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Victoria.Node;
|
||||||
|
|
||||||
|
namespace Lunaris2;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine(eventArgs.ExceptionObject);
|
||||||
|
};
|
||||||
|
CreateHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureServices((_, services) =>
|
||||||
|
{
|
||||||
|
var config = new DiscordSocketConfig
|
||||||
|
{
|
||||||
|
GatewayIntents = GatewayIntents.All
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = new DiscordSocketClient(config);
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddMediatR(mediatRServiceConfiguration => mediatRServiceConfiguration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
|
||||||
|
.AddLavalink()
|
||||||
|
.ConfigureLavalink(options =>
|
||||||
|
{
|
||||||
|
options.BaseAddress = new Uri(
|
||||||
|
$"http://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}"
|
||||||
|
);
|
||||||
|
options.WebSocketUri = new Uri($"ws://{configuration["LavaLinkHostname"]}:{configuration["LavaLinkPort"]}/v4/websocket");
|
||||||
|
options.Passphrase = configuration["LavaLinkPassword"] ?? "youshallnotpass";
|
||||||
|
options.Label = "Node";
|
||||||
|
})
|
||||||
|
.AddSingleton<LavaNode>()
|
||||||
|
.AddSingleton<MusicEmbed>()
|
||||||
|
.AddSingleton<ChatSettings>()
|
||||||
|
.AddSingleton(client)
|
||||||
|
.AddSingleton<DiscordEventListener>()
|
||||||
|
.AddSingleton<VoiceChannelMonitorService>()
|
||||||
|
.AddSingleton(service => new InteractionService(service.GetRequiredService<DiscordSocketClient>()))
|
||||||
|
.Configure<ChatSettings>(configuration.GetSection("LLM"));
|
||||||
|
|
||||||
|
client.Ready += () => Client_Ready(client);
|
||||||
|
client.Log += Log;
|
||||||
|
|
||||||
|
client
|
||||||
|
.LoginAsync(TokenType.Bot, configuration["Token"])
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
client
|
||||||
|
.StartAsync()
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
var listener = services
|
||||||
|
.BuildServiceProvider()
|
||||||
|
.GetRequiredService<DiscordEventListener>();
|
||||||
|
|
||||||
|
listener
|
||||||
|
.StartAsync()
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
private static Task Client_Ready(DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
client.RegisterCommands();
|
||||||
|
|
||||||
|
new VoiceChannelMonitorService(client).StartMonitoring();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task Log(LogMessage arg)
|
||||||
|
{
|
||||||
|
Console.WriteLine(arg);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## How commands from Discord gets executed
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Program[Program] -->|Register| EventListener
|
||||||
|
EventListener[DiscordEventListener] --> A[MessageReceivedHandler]
|
||||||
|
|
||||||
|
EventListener[DiscordEventListener] --> A2[SlashCommandReceivedHandler]
|
||||||
|
|
||||||
|
A --> |Message| f{If bot is mentioned}
|
||||||
|
f --> |ChatCommand| v[ChatHandler]
|
||||||
|
|
||||||
|
A2[SlashCommandReceivedHandler] -->|Message| C{Send to correct command by
|
||||||
|
looking at commandName}
|
||||||
|
|
||||||
|
C -->|JoinCommand| D[JoinHandler]
|
||||||
|
C -->|PlayCommand| E[PlayHandler]
|
||||||
|
C -->|HelloCommand| F[HelloHandler]
|
||||||
|
C -->|GoodbyeCommand| G[GoodbyeHandler]
|
||||||
|
```
|
||||||
|
Program registers an event listener ```DiscordEventListener``` which publish a message :
|
||||||
|
|
||||||
|
```c#
|
||||||
|
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
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
D[JoinHandler] --> Disc[Discord Api]
|
||||||
|
E[PlayHandler] --> Disc[Discord Api]
|
||||||
|
F[HelloHandler] --> Disc[Discord Api]
|
||||||
|
G[GoodbyeHandler] --> Disc[Discord Api]
|
||||||
|
v[ChatHandler] --> Disc[Discord Api]
|
||||||
|
v --> o[Ollama Server]
|
||||||
|
o --> v
|
||||||
|
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. |
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Lunaris2.SlashCommand;
|
||||||
|
|
||||||
|
public static class Option
|
||||||
|
{
|
||||||
|
public const string Input = "input";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Command
|
||||||
|
{
|
||||||
|
public static class Disconnect
|
||||||
|
{
|
||||||
|
public const string Name = "disconnect";
|
||||||
|
public const string Description = "Disconnect from the voice channel!";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Skip
|
||||||
|
{
|
||||||
|
public const string Name = "skip";
|
||||||
|
public const string Description = "Skip the current song!";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Resume
|
||||||
|
{
|
||||||
|
public const string Name = "resume";
|
||||||
|
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 const string Name = "play";
|
||||||
|
public const string Description = "Play a song!";
|
||||||
|
|
||||||
|
public static readonly List<SlashCommandOptionBuilder>? Options = new()
|
||||||
|
{
|
||||||
|
new SlashCommandOptionBuilder
|
||||||
|
{
|
||||||
|
Name = "input",
|
||||||
|
Description = "The song you want to play",
|
||||||
|
Type = ApplicationCommandOptionType.String,
|
||||||
|
IsRequired = true
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] GetAllCommands()
|
||||||
|
{
|
||||||
|
return typeof(Command)
|
||||||
|
.GetNestedTypes()
|
||||||
|
.Select(command => command.GetField("Name")?.GetValue(null)?.ToString())
|
||||||
|
.ToArray()!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.Net;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Lunaris2.SlashCommand;
|
||||||
|
|
||||||
|
public class SlashCommandBuilder(
|
||||||
|
string commandName,
|
||||||
|
string commandDescription,
|
||||||
|
List<SlashCommandOptionBuilder>? commandOptions = null)
|
||||||
|
{
|
||||||
|
private string CommandName { get; set; } = commandName;
|
||||||
|
private string CommandDescription { get; set; } = commandDescription;
|
||||||
|
private List<SlashCommandOptionBuilder>? CommandOptions { get; set; } = commandOptions;
|
||||||
|
|
||||||
|
public async Task CreateSlashCommand(DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
var registeredCommands = await client.GetGlobalApplicationCommandsAsync();
|
||||||
|
await RemoveUnusedCommands(Command.GetAllCommands(), registeredCommands);
|
||||||
|
|
||||||
|
if (await CommandExists(registeredCommands))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var globalCommand = new Discord.SlashCommandBuilder();
|
||||||
|
globalCommand.WithName(CommandName);
|
||||||
|
globalCommand.WithDescription(CommandDescription);
|
||||||
|
|
||||||
|
CommandOptions?.ForEach(option => globalCommand.AddOption(option));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.CreateGlobalApplicationCommandAsync(globalCommand.Build());
|
||||||
|
Console.WriteLine($"Command {CommandName} has been registered.");
|
||||||
|
}
|
||||||
|
catch(HttpException exception)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
|
||||||
|
Console.WriteLine(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RemoveUnusedCommands(string[] commands, IEnumerable<SocketApplicationCommand> registeredCommands)
|
||||||
|
{
|
||||||
|
foreach(var command in registeredCommands)
|
||||||
|
{
|
||||||
|
if (commands.Contains(command.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await command.DeleteAsync();
|
||||||
|
Console.WriteLine($"Command {command.Name} has been removed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> CommandExists(IEnumerable<SocketApplicationCommand> registeredCommands)
|
||||||
|
{
|
||||||
|
if (!registeredCommands.Any(command => command.Name == CommandName && command.Description == CommandDescription))
|
||||||
|
return Task.FromResult(false);
|
||||||
|
|
||||||
|
Console.WriteLine($"Command {CommandName} already exists.");
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace Lunaris2.SlashCommand;
|
||||||
|
|
||||||
|
public static class SlashCommandRegistration
|
||||||
|
{
|
||||||
|
public static void RegisterCommands(this DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description);
|
||||||
|
RegisterCommand(client, Command.Pause.Name, Command.Pause.Description);
|
||||||
|
RegisterCommand(client, Command.Disconnect.Name, Command.Disconnect.Description);
|
||||||
|
RegisterCommand(client, Command.Skip.Name, Command.Skip.Description);
|
||||||
|
RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options);
|
||||||
|
RegisterCommand(client, Command.Resume.Name, Command.Resume.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterCommand(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
string commandName,
|
||||||
|
string commandDescription,
|
||||||
|
List<SlashCommandOptionBuilder>? commandOptions = null)
|
||||||
|
{
|
||||||
|
var command = new SlashCommandBuilder(commandName, commandDescription, commandOptions);
|
||||||
|
_ = command.CreateSlashCommand(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Token": "TOKEN",
|
||||||
|
"LavaLinkPassword": "youshallnotpass",
|
||||||
|
"LavaLinkHostname": "127.0.0.1",
|
||||||
|
"LavaLinkPort": 2333,
|
||||||
|
"LLM": {
|
||||||
|
"Url": "http://localhost:7869",
|
||||||
|
"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:"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.9.34701.34
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lunaris2", "Bot/Lunaris2.csproj", "{3A16FD4C-EE50-4B05-9A75-0DDCD93AA615}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{3A16FD4C-EE50-4B05-9A75-0DDCD93AA615}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3A16FD4C-EE50-4B05-9A75-0DDCD93AA615}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3A16FD4C-EE50-4B05-9A75-0DDCD93AA615}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3A16FD4C-EE50-4B05-9A75-0DDCD93AA615}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {4E738D71-9DB0-456E-9BF2-0CE3FED1DE44}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -1,2 +1,51 @@
|
|||||||
# Lunaris2.0
|
# Lunaris2 - Discord Music Bot
|
||||||
WIP
|
|
||||||
|
Lunaris2 is a Discord bot designed to play music in your server's voice channels. It's built using C# and the Discord.Net library, and it uses the LavaLink music client for audio streaming.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Play music from YouTube directly in your Discord server.
|
||||||
|
- Skip tracks, pause, and resume playback.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
1. Clone the repo.
|
||||||
|
2. Extract.
|
||||||
|
3. If there isn't already a appsettings.json file in there, create one.
|
||||||
|
4. Set the discord bot token. How the file should look (without token): [appsettings.json](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json)]
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
*NOTE: you need to download the model from the Ollama ui, the model name which is preselected in the code is called ``gemma``.*
|
||||||
|
|
||||||
|
## PM2 Setup
|
||||||
|
- Install PM2 and configure it following their setup guide
|
||||||
|
#### Lavalink
|
||||||
|
* Download Lavalink 4.X.X (.jar)
|
||||||
|
* Install Java 17
|
||||||
|
|
||||||
|
If using Linux run following command to start Lavalink with PM2:
|
||||||
|
``pm2 start "sudo java -Xmx1G -jar Lavalink.jar" --name Lavalink4.0.7``
|
||||||
|
|
||||||
|
For me I have Lavalink.jar downloaded in ``/opt`` folder from Linux root. By running Lavalink using PM2, you can monitor it and manage it from a page in your browser instead of having to access the server terminal.
|
||||||
|
#### Lunaris
|
||||||
|
* Install dotnet
|
||||||
|
|
||||||
|
Register the Lunaris bot with PM2:
|
||||||
|
``pm2 start "dotnet Lunaris2.dll"``
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- `/play <song>`: Plays the specified song in the voice channel you're currently in.
|
||||||
|
- `/skip`: Skips the currently playing song.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
server: # REST and WS server
|
||||||
|
port: 2333
|
||||||
|
address: 0.0.0.0
|
||||||
|
plugins:
|
||||||
|
youtube:
|
||||||
|
enabled: true # Whether this source can be used.
|
||||||
|
allowSearch: true # Whether "ytsearch:" and "ytmsearch:" can be used.
|
||||||
|
allowDirectVideoIds: true # Whether just video IDs can match. If false, only complete URLs will be loaded.
|
||||||
|
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:
|
||||||
|
plugins:
|
||||||
|
# - dependency: "group:artifact:version"
|
||||||
|
# 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:
|
||||||
|
password: "youshallnotpass"
|
||||||
|
sources:
|
||||||
|
youtube: false
|
||||||
|
bandcamp: true
|
||||||
|
soundcloud: true
|
||||||
|
twitch: true
|
||||||
|
vimeo: true
|
||||||
|
http: true
|
||||||
|
local: false
|
||||||
|
filters: # All filters are enabled by default
|
||||||
|
volume: true
|
||||||
|
equalizer: true
|
||||||
|
karaoke: true
|
||||||
|
timescale: true
|
||||||
|
tremolo: true
|
||||||
|
vibrato: true
|
||||||
|
distortion: true
|
||||||
|
rotation: true
|
||||||
|
channelMix: true
|
||||||
|
lowPass: true
|
||||||
|
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: 10000 # How many milliseconds of audio to keep buffered
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
|
||||||
|
playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds
|
||||||
|
youtubeSearchEnabled: true
|
||||||
|
soundcloudSearchEnabled: true
|
||||||
|
gc-warnings: true
|
||||||
|
#ratelimit:
|
||||||
|
#ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
|
||||||
|
#excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
|
||||||
|
#strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
|
||||||
|
#searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
|
||||||
|
#retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
|
||||||
|
#youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without.
|
||||||
|
#email: "" # Email of Google account
|
||||||
|
#password: "" # Password of Google account
|
||||||
|
#httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
|
||||||
|
#proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
|
||||||
|
#proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
|
||||||
|
#proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
|
||||||
|
#proxyPassword: "" # Password for basic authentication
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
prometheus:
|
||||||
|
enabled: false
|
||||||
|
endpoint: /metrics
|
||||||
|
|
||||||
|
sentry:
|
||||||
|
dsn: ""
|
||||||
|
environment: ""
|
||||||
|
# tags:
|
||||||
|
# some_key: some_value
|
||||||
|
# another_key: another_value
|
||||||
|
|
||||||
|
logging:
|
||||||
|
file:
|
||||||
|
path: ./logs/
|
||||||
|
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
lavalink: INFO
|
||||||
|
|
||||||
|
request:
|
||||||
|
enabled: true
|
||||||
|
includeClientInfo: true
|
||||||
|
includeHeaders: false
|
||||||
|
includeQueryString: true
|
||||||
|
includePayload: true
|
||||||
|
maxPayloadLength: 10000
|
||||||
|
|
||||||
|
|
||||||
|
logback:
|
||||||
|
rollingpolicy:
|
||||||
|
max-file-size: 1GB
|
||||||
|
max-history: 30
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
services:
|
||||||
|
lavalink:
|
||||||
|
# pin the image version to Lavalink v4
|
||||||
|
image: ghcr.io/lavalink-devs/lavalink:4.0.7
|
||||||
|
container_name: lavalink
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# set Java options here
|
||||||
|
- _JAVA_OPTIONS=-Xmx6G
|
||||||
|
# set lavalink server port
|
||||||
|
- SERVER_PORT=2333
|
||||||
|
# set password for lavalink
|
||||||
|
- LAVALINK_SERVER_PASSWORD=youshallnotpass
|
||||||
|
volumes:
|
||||||
|
# mount application.yml from the same directory or use environment variables
|
||||||
|
- ./application.yml:/opt/Lavalink/application.yml
|
||||||
|
# persist plugins between restarts, make sure to set the correct permissions (user: 322, group: 322)
|
||||||
|
- ./plugins/:/opt/Lavalink/plugins/
|
||||||
|
networks:
|
||||||
|
- lavalink
|
||||||
|
expose:
|
||||||
|
# lavalink exposes port 2333 to connect to for other containers (this is for documentation purposes only)
|
||||||
|
- 2333
|
||||||
|
ports:
|
||||||
|
# you only need this if you want to make your lavalink accessible from outside of containers
|
||||||
|
- "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:
|
||||||
|
# create a lavalink network you can add other containers to, to give them access to Lavalink
|
||||||
|
lavalink:
|
||||||
|
name: lavalink
|
||||||
|
ollama-docker:
|
||||||
|
external: false
|
||||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
read -p "Press enter to continue"
|
||||||
Reference in New Issue
Block a user