Compare commits

..
49 Commits
Author SHA1 Message Date
Myx 9730f32be0 Small fix 2024-08-13 03:13:02 +02:00
1e2c10a7ea Require user to be in chat to queue (#5)
Co-authored-by: Myx <info@azaaxin.com>
2024-08-12 19:16:11 +02:00
8dcd4b334d Fix deadlock and auto leave voice channel after 3 min (#4)
* Fix for thread being busy handling discord commands blocking audioplayer to timeout
* Auto leave if alone in voice channel after 3 min

Co-authored-by: Myx <info@azaaxin.com>
2024-08-12 02:02:10 +02:00
9bcebea6b0 Lavalink4net (#3)
* Update readme.md

Test

Migrate from Victoria

* Small fix

---------

Co-authored-by: Myx <info@azaaxin.com>
2024-08-11 16:06:52 +02:00
d72676c7e0 Improve chat (#2)
Co-authored-by: Myx <info@azaaxin.com>
2024-08-11 01:18:29 +02:00
myxeliumandGitHub b30d47e351 Add LLM info into main Readme 2024-06-19 12:33:19 +02:00
myxeliumandGitHub 3ce0df7eaf Added Ollama and Ollama Web UI 2024-06-19 12:21:05 +02:00
myxeliumandGitHub e88e67f913 Fix broken appsettings file
Invalid json
2024-06-19 12:12:05 +02:00
myxeliumandGitHub 5053553182 Update dotnet.yml 2024-06-02 19:12:45 +02:00
myxeliumandGitHub 327ccc9675 Update dotnet.yml 2024-06-02 18:57:56 +02:00
myxeliumandGitHub cbc99c2773 Update dotnet.yml 2024-06-02 18:55:14 +02:00
myxeliumandGitHub d56215f685 Update README.md 2024-06-02 18:53:02 +02:00
myxeliumandGitHub 967bee923a Update dotnet.yml 2024-06-02 18:51:38 +02:00
myxeliumandGitHub f8e6854569 Create readme.md 2024-06-02 00:04:14 +02:00
myxeliumandGitHub 03150a3d04 Update README.md 2024-06-01 23:53:15 +02:00
myxeliumandGitHub 32b6e09336 Update README.md 2024-06-01 23:42:17 +02:00
myxeliumandGitHub e3df4505fe Update README.md 2024-06-01 23:37:47 +02:00
myxeliumandGitHub 3daf18e053 Update README.md 2024-06-01 23:36:07 +02:00
myxeliumandGitHub 54c5c68ba6 Update README.md 2024-06-01 23:35:40 +02:00
myxeliumandGitHub e16ff9cfaf Update README.md 2024-06-01 23:35:16 +02:00
a1d20fd732 Add chat functionality (#1)
* Working chatbot

* Clean

* Working LLM chatbot

---------

Co-authored-by: Myx <info@azaaxin.com>
2024-06-01 23:22:47 +02:00
myxeliumandGitHub 3d7655a902 Update readme.md 2024-04-15 00:00:38 +02:00
myxeliumandGitHub 8ddcf31da7 Update readme.md 2024-04-14 23:59:03 +02:00
myxeliumandGitHub 80a7c19b20 Update readme.md 2024-04-14 23:58:10 +02:00
myxeliumandGitHub 713715901b Create readme.md 2024-04-14 23:54:26 +02:00
myxeliumandGitHub 1a3a00f4ed Update README.md 2024-04-14 23:33:44 +02:00
Myx 0673653491 Fix broken lavalink version 2024-04-14 23:02:51 +02:00
myxeliumandGitHub a650fd431e Create appsettings.json 2024-04-14 23:01:58 +02:00
myxeliumandGitHub f6a763589e Update dotnet.yml 2024-04-14 22:22:11 +02:00
myxeliumandGitHub b7198bd01e Update dotnet.yml 2024-04-14 22:19:45 +02:00
myxeliumandGitHub f0678a4244 Update dotnet.yml 2024-04-14 22:14:32 +02:00
myxeliumandGitHub 984d6b0af8 Update dotnet.yml 2024-04-14 22:08:46 +02:00
myxeliumandGitHub 175f1f0d01 Update dotnet.yml 2024-04-14 22:05:38 +02:00
myxeliumandGitHub c8a1936f64 Update README.md 2024-04-14 21:57:14 +02:00
myxeliumandGitHub 3f091ed7cc Update dotnet.yml 2024-04-14 21:54:44 +02:00
myxeliumandGitHub 49872791b3 Update dotnet.yml 2024-04-14 21:53:45 +02:00
myxeliumandGitHub 5a7b326dc2 Create dotnet.yml 2024-04-14 21:48:26 +02:00
Myx 0bc2a9be56 Add Embed 2024-04-14 21:43:25 +02:00
Myx 2d7ec0829e Add Join and Play handlers, Expand Command Builder functionality 2024-04-14 17:29:49 +02:00
myxeliumandGitHub 9b48536cd7 Update README.md 2024-04-14 16:57:18 +02:00
myxeliumandGitHub 31c39e8893 Update README.md 2024-04-14 16:37:57 +02:00
myxeliumandGitHub 521f808209 Add mermaid diagram 2024-04-14 16:35:57 +02:00
myxeliumandGitHub 97667df53b Create README.md 2024-04-14 16:11:43 +02:00
Myx 95ab1281a4 Add Join and Play handlers, Expand Command Builder functionality 2024-04-14 16:06:18 +02:00
Myx f7b54ca80c Restructure 2024-04-13 20:04:51 +02:00
Myx 3699a13cd9 Slightly update program.cs 2024-04-13 20:00:53 +02:00
myxeliumandGitHub 13efaa7552 Create README.md 2024-04-11 04:27:53 +02:00
Myx 169b71c724 Ignore appsetttings 2024-04-11 04:24:17 +02:00
Myx 5b996ecd1d Basic bot 2024-04-11 04:22:49 +02:00
33 changed files with 1436 additions and 2 deletions
+67
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
obj
.idea
.vs
bin
appsettings.json
+68
View File
@@ -0,0 +1,68 @@
using System.Text;
using Discord.WebSocket;
using MediatR;
using Microsoft.Extensions.Options;
using OllamaSharp;
namespace Lunaris2.Handler.ChatCommand
{
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
public class ChatHandler : IRequestHandler<ChatCommand>
{
private readonly OllamaApiClient _ollama;
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
private readonly ChatSettings _chatSettings;
public ChatHandler(IOptions<ChatSettings> chatSettings)
{
_chatSettings = chatSettings.Value;
var uri = new Uri(chatSettings.Value.Url);
_ollama = new OllamaApiClient(uri)
{
SelectedModel = chatSettings.Value.Model
};
}
public async Task Handle(ChatCommand command, CancellationToken cancellationToken)
{
var channelId = command.Message.Channel.Id;
_chatContexts.TryAdd(channelId, null);
var userMessage = command.FilteredMessage;
var randomPersonality = _chatSettings.Personalities[new Random().Next(_chatSettings.Personalities.Count)];
userMessage = $"{randomPersonality.Instruction} {userMessage}";
using var setTyping = command.Message.Channel.EnterTypingState();
if (string.IsNullOrWhiteSpace(userMessage))
{
await command.Message.Channel.SendMessageAsync("Am I expected to read your mind?");
setTyping.Dispose();
return;
}
var response = await GenerateResponse(userMessage, channelId, cancellationToken);
await command.Message.Channel.SendMessageAsync(response);
setTyping.Dispose();
}
private async Task<string> GenerateResponse(string userMessage, ulong channelId, CancellationToken cancellationToken)
{
var response = new StringBuilder();
if (_chatContexts[channelId] == null)
{
_chatContexts[channelId] = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? ""));
}
await _chatContexts[channelId].Send(userMessage, cancellationToken);
return response.ToString();
}
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace Lunaris2.Handler.ChatCommand;
public class ChatSettings
{
public string Url { get; set; }
public string Model { get; set; }
public List<Personality> Personalities { get; set; }
}
public class Personality
{
public string Name { get; set; }
public string Instruction { get; set; }
}
+8
View 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)
+37
View File
@@ -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);
}
}
+76
View File
@@ -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;
}
}
+87
View File
@@ -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;
}
}
+40
View File
@@ -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;
}
}
}
+36
View File
@@ -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>
+38
View File
@@ -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
View File
@@ -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;
}
}
+49
View File
@@ -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. |
+59
View 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);
}
}
}
+60
View File
@@ -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()!;
}
}
+63
View File
@@ -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);
}
}
+23
View File
@@ -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:"
}
]
}
}
+25
View File
@@ -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
+51 -2
View File
@@ -1,2 +1,51 @@
# Lunaris2.0
WIP
# Lunaris2 - Discord Music Bot
Lunaris2 is a Discord bot designed to play music in your server's voice channels. It's built using C# and the Discord.Net library, and it uses the LavaLink music client for audio streaming.
## 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
View File
@@ -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
+75
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
docker compose up -d
read -p "Press enter to continue"