changing folder structure

This commit is contained in:
2023-08-10 16:01:39 -03:00
parent 73b0bac359
commit 3b2a4dc6d5
16 changed files with 19 additions and 16 deletions

View File

@@ -0,0 +1,14 @@
namespace Kasbot.Annotations
{
public class FlagAttribute : Attribute
{
public List<string> Names { get; set; }
public FlagAttribute() { }
public FlagAttribute(params string[] names)
{
Names = names.ToList();
}
}
}

View File

@@ -0,0 +1,18 @@
using Discord;
using Discord.Rest;
namespace Kasbot.Extensions
{
public static class RestMessageExtensions
{
public static async Task TryDeleteAsync(this RestMessage message, RequestOptions options = null)
{
try
{
await message.DeleteAsync(options);
}
catch { }
}
}
}

View File

@@ -0,0 +1,25 @@
using Discord;
using Discord.WebSocket;
namespace Kasbot.Extensions
{
public static class SocketMessageExtensions
{
private const int MessageDelay = 3_000; // in ms
public static async Task SendTemporaryMessageAsync(this ISocketMessageChannel channel, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
var message = await channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags);
await Task.Delay(MessageDelay);
await message.DeleteAsync();
}
public static async Task SendTemporaryMessageAsync(this IMessageChannel channel, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
{
var message = await channel.SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags);
await Task.Delay(MessageDelay);
await message.DeleteAsync();
}
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="legacy\**" />
<EmbeddedResource Remove="legacy\**" />
<None Remove="legacy\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="YoutubeExplode" Version="6.3.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using Discord;
using Discord.Audio;
using Kasbot.Services.Internal;
namespace Kasbot.Models
{
public class Connection
{
public IAudioClient AudioClient { get; set; }
public IVoiceChannel AudioChannel { get; set; }
public Stream? CurrentAudioStream { get; set; }
public Queue<Media> Queue { get; set; } = new Queue<Media>();
}
}

View File

@@ -0,0 +1,45 @@
using Kasbot.Annotations;
namespace Kasbot.Models
{
public class Flags
{
[Flag("-s", "-silent")]
public bool Silent { get; set; }
[Flag("-r", "-repeat")]
public bool Repeat { get; set; }
public Flags() { }
public Flags(string command)
{
this.Parse(command);
}
public string Parse(string command)
{
string result = command;
this.GetType().GetProperties().ToList().ForEach(prop =>
{
Attribute.GetCustomAttributes(prop).ToList().ForEach(attr =>
{
if (attr is FlagAttribute flag)
{
flag.Names.ForEach(name =>
{
if (command.Contains(name))
{
prop.SetValue(this, true);
result = result.Replace(name, string.Empty);
}
});
}
});
});
return result;
}
}
}

View File

@@ -0,0 +1,78 @@
using Discord;
using Discord.Commands;
using Kasbot.Models;
using Kasbot.Services;
using Kasbot.Services;
namespace Kasbot.Modules
{
public class CommandModule : ModuleBase<ShardedCommandContext>
{
public PictureService PictureService { get; set; }
public PlayerService PlayerService { get; set; }
[Command("cat")]
public async Task CatAsync()
{
var stream = await PictureService.GetCatPictureAsync();
stream.Seek(0, SeekOrigin.Begin);
await Context.Channel.SendFileAsync(stream, "cat.png");
}
[Command("play", RunMode = RunMode.Async)]
public async Task PlayAsync([Remainder] string text)
{
Console.WriteLine("Joining on " + Context.Guild.Name);
string youtubeUrl = text;
IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
if (channel is null)
{
throw new Exception("You must be connect in a voice channel!");
}
var flags = new Flags();
var withoutFlags = flags.Parse(text);
await PlayerService.Play(Context, withoutFlags.Trim(), flags);
}
[Command("join", RunMode = RunMode.Async)]
public async Task JoinAsync()
{
Console.WriteLine("Joining on " + Context.Guild.Name);
IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
if (channel is null)
{
throw new Exception("You must be connect in a voice channel!");
}
await PlayerService.Join(Context);
}
[Command("stop", RunMode = RunMode.Async)]
public async Task StopAsync()
{
await PlayerService.Stop(Context.Guild.Id);
}
[Command("skip", RunMode = RunMode.Async)]
public async Task SkipAsync()
{
await PlayerService.Skip(Context.Guild.Id);
}
[Command("leave", RunMode = RunMode.Async)]
public async Task LeaveAsync()
{
await PlayerService.Leave(Context.Guild.Id);
}
[Alias("r")]
[Command("repeat", RunMode = RunMode.Async)]
public async Task RepeatAsync()
{
await PlayerService.Repeat(Context.Guild.Id);
}
}
}

108
Kasbot.APP/Program.cs Normal file
View File

@@ -0,0 +1,108 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Kasbot.Services;
using Kasbot.Services.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Kasbot
{
class Program
{
private static string TOKEN = Environment.GetEnvironmentVariable("TOKEN");
private static int SHARDS = int.Parse(Environment.GetEnvironmentVariable("SHARDS") ?? "0");
static void Main(string[] args)
{
if (TOKEN == null)
{
throw new Exception("Discord Bot Token was not found.");
}
if (SHARDS == 0)
{
Console.WriteLine("Shards amount not found, defaulting to 1.");
SHARDS = 1;
}
new Program()
.MainAsync()
.GetAwaiter()
.GetResult();
}
public async Task MainAsync()
{
using (var services = ConfigureServices())
{
var client = services.GetRequiredService<DiscordShardedClient>();
client.Log += LogAsync;
client.LoggedIn += () => Client_LoggedIn(client);
client.ShardReady += (shard) => Client_Ready(shard);
services.GetRequiredService<CommandService>().Log += LogAsync;
await client.LoginAsync(TokenType.Bot, TOKEN);
await client.StartAsync();
await services.GetRequiredService<CommandHandlingService>().InitializeAsync();
await Task.Delay(Timeout.Infinite);
}
}
private async Task Client_Ready(DiscordSocketClient client)
{
var announceLoginGuild = ulong.Parse(Environment.GetEnvironmentVariable("ANNOUNCE_LOGIN_GUILD") ?? "0");
var announceLoginChannel = ulong.Parse(Environment.GetEnvironmentVariable("ANNOUNCE_LOGIN_CHANNEL") ?? "0");
if (announceLoginGuild == 0 || announceLoginChannel == 0)
{
return;
}
var channel = client.GetGuild(announceLoginGuild).GetTextChannel(announceLoginChannel);
if (channel == null)
{
Console.WriteLine("Announce channel not found.");
return;
}
await channel.SendMessageAsync("@everyone LIVE!");
}
private Task Client_LoggedIn(DiscordShardedClient client)
{
Console.WriteLine("Successfully logged in!");
return Task.CompletedTask;
}
private Task LogAsync(LogMessage log)
{
Console.WriteLine(log.ToString());
return Task.CompletedTask;
}
private ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton(new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent,
TotalShards = SHARDS
})
.AddSingleton<DiscordShardedClient>()
.AddSingleton<CommandService>()
.AddSingleton<YoutubeService>()
.AddSingleton<AudioService>()
.AddSingleton<PlayerService>()
.AddSingleton<CommandHandlingService>()
.AddSingleton<HttpClient>()
.AddSingleton<PictureService>()
.BuildServiceProvider();
}
}
}

View File

@@ -0,0 +1,64 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Kasbot.Extensions;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Kasbot.Services
{
public class CommandHandlingService
{
private readonly CommandService _commands;
private readonly DiscordShardedClient _discord;
private readonly IServiceProvider _services;
private readonly string CommandPrefix = "!";
public CommandHandlingService(IServiceProvider services)
{
_commands = services.GetRequiredService<CommandService>();
_discord = services.GetRequiredService<DiscordShardedClient>();
_services = services;
_commands.CommandExecuted += CommandExecutedAsync;
_discord.MessageReceived += MessageReceivedAsync;
CommandPrefix = Environment.GetEnvironmentVariable("COMMAND_PREFIX") ?? "!";
}
public async Task InitializeAsync()
{
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
}
public async Task MessageReceivedAsync(SocketMessage rawMessage)
{
if (!(rawMessage is SocketUserMessage message))
return;
if (message.Source != MessageSource.User)
return;
var argPos = 0;
//Check if the message sent has the specified prefix
if (!message.HasStringPrefix(CommandPrefix, ref argPos)) return;
var context = new ShardedCommandContext(_discord, message);
await _commands.ExecuteAsync(context, argPos, _services);
}
public async Task CommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
{
if (!command.IsSpecified)
return;
if (result.IsSuccess)
return;
await context.Channel.SendTemporaryMessageAsync($"Error: {result}");
}
}
}

View File

@@ -0,0 +1,79 @@
using Discord.Audio;
using System.Diagnostics;
namespace Kasbot.Services.Internal
{
public class AudioService
{
public AudioService() { }
public void StartAudioTask(Stream inputStream, IAudioClient outputAudioClient, Action<Stream> onStartAudio, Action<Task> onFinish)
{
var ffmpeg = CreateFFmpeg();
Task stdin = new Task(() =>
{
using (var output = inputStream)
{
try
{
output.CopyTo(ffmpeg.StandardInput.BaseStream);
ffmpeg.StandardInput.Close();
}
catch { }
finally
{
output.Flush();
}
}
});
Task stdout = new Task(() =>
{
using (var output = ffmpeg.StandardOutput.BaseStream)
using (var discord = outputAudioClient.CreatePCMStream(AudioApplication.Music))
{
try
{
onStartAudio.Invoke(ffmpeg.StandardOutput.BaseStream);
output.CopyTo(discord);
}
catch { }
finally
{
discord.Flush();
}
}
});
stdin.Start();
stdout.Start();
stdin.ContinueWith(onFinish);
stdout.ContinueWith(onFinish);
Task.WaitAll(stdin, stdout);
ffmpeg.Close();
}
private Process CreateFFmpeg()
{
var process = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-hide_banner -loglevel panic -i pipe:0 -ac 2 -f s16le -ar 48000 pipe:1",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true
});
if (process == null || process.HasExited)
{
throw new Exception("Sorry, ffmpeg killed itself in a tragic accident!");
}
return process;
}
}
}

View File

@@ -0,0 +1,144 @@
using Discord.WebSocket;
using YoutubeExplode.Videos;
using YoutubeExplode;
using Discord.Rest;
using YoutubeExplode.Videos.Streams;
using Kasbot.Models;
namespace Kasbot.Services.Internal
{
public class YoutubeService
{
public YoutubeService()
{
}
public async Task<MediaCollection> DownloadPlaylistMetadataFromYoutube(SocketUserMessage message, string search)
{
var collection = new MediaCollection();
var youtube = new YoutubeClient();
var playlistInfo = await youtube.Playlists.GetAsync(search);
await youtube.Playlists.GetVideosAsync(search).ForEachAsync(videoId =>
{
var media = new Media
{
Name = videoId.Title,
Length = videoId.Duration ?? new TimeSpan(0),
VideoId = videoId.Id,
Message = message,
Flags = new Flags()
};
collection.Medias.Add(media);
});
collection.CollectionName = playlistInfo.Title;
return collection;
}
public async Task<Media> DownloadMetadataFromYoutube(Media media)
{
var youtube = new YoutubeClient();
IVideo? videoId;
if (media.Search.StartsWith("http://") || media.Search.StartsWith("https://"))
videoId = await youtube.Videos.GetAsync(media.Search);
else
videoId = await youtube.Search.GetVideosAsync(media.Search).FirstOrDefaultAsync();
if (videoId == null)
{
return media;
}
media.Name = videoId.Title;
media.Length = videoId.Duration ?? new TimeSpan(0);
media.VideoId = videoId.Id;
return media;
}
public async Task<MemoryStream?> DownloadAudioFromYoutube(Media media)
{
if (media.VideoId == null)
{
return null;
}
var memoryStream = new MemoryStream();
var youtube = new YoutubeClient();
var streamInfoSet = await youtube.Videos.Streams.GetManifestAsync((VideoId)media.VideoId);
var streamInfo = streamInfoSet.GetAudioOnlyStreams().GetWithHighestBitrate();
var streamVideo = await youtube.Videos.Streams.GetAsync(streamInfo);
streamVideo.Position = 0;
streamVideo.CopyTo(memoryStream);
memoryStream.Position = 0;
return memoryStream;
}
public SearchType GetSearchType(string query)
{
if (string.IsNullOrEmpty(query))
return SearchType.None;
if (query.StartsWith("http://") || query.StartsWith("https://"))
{
if (query.Contains("playlist?list="))
return SearchType.PlaylistURL;
// need to add 'else if' for ChannelURL
return SearchType.VideoURL;
}
return SearchType.StringSearch;
}
}
public enum SearchType
{
None,
StringSearch,
VideoURL,
PlaylistURL,
ChannelURL
}
public class Media
{
public string Search { get; set; }
public string Name { get; set; }
public TimeSpan Length { get; set; }
public Flags Flags { get; set; }
public VideoId? VideoId { get; set; }
public RestUserMessage PlayMessage { get; set; }
public RestUserMessage? QueueMessage { get; set; }
private SocketUserMessage message;
public SocketUserMessage Message
{
get => message;
set
{
message = value;
Channel = value.Channel;
}
}
public ISocketMessageChannel Channel { get; private set; }
}
public class MediaCollection
{
public string CollectionName { get; set; }
public List<Media> Medias { get; set; } = new List<Media>();
}
}

View File

@@ -0,0 +1,20 @@
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace Kasbot.Services
{
public class PictureService
{
private readonly HttpClient _http;
public PictureService(HttpClient http)
=> _http = http;
public async Task<Stream> GetCatPictureAsync()
{
var resp = await _http.GetAsync("https://cataas.com/cat");
return await resp.Content.ReadAsStreamAsync();
}
}
}

View File

@@ -0,0 +1,249 @@
using Discord;
using Discord.Audio;
using Discord.Commands;
using Kasbot.Extensions;
using Kasbot.Models;
using Kasbot.Services.Internal;
namespace Kasbot.Services
{
public class PlayerService
{
public Dictionary<ulong, Connection> Clients { get; set; }
public YoutubeService YoutubeService { get; set; }
public AudioService AudioService { get; set; }
public PlayerService(YoutubeService youtubeService, AudioService audioService)
{
YoutubeService = youtubeService;
AudioService = audioService;
Clients = new Dictionary<ulong, Connection>();
}
private async Task<Connection> CreateConnection(ulong guildId, IVoiceChannel voiceChannel)
{
var conn = new Connection();
IAudioClient audioClient = await voiceChannel.ConnectAsync(selfDeaf: true);
conn.AudioClient = audioClient;
conn.AudioChannel = voiceChannel;
if (Clients.ContainsKey(guildId))
Clients.Remove(guildId);
Clients.Add(guildId, conn);
return conn;
}
public async Task Play(ShardedCommandContext Context, string arguments, Flags flags)
{
var media = new Media()
{
Message = Context.Message,
Search = arguments,
Flags = flags,
Name = "",
};
var guildId = Context.Guild.Id;
var userVoiceChannel = (Context.User as IVoiceState).VoiceChannel;
if (Clients.TryGetValue(guildId, out var conn))
{
if (conn.AudioChannel.Id != userVoiceChannel.Id)
{
await Stop(guildId);
conn = await CreateConnection(guildId, userVoiceChannel);
}
}
else
{
conn = await CreateConnection(guildId, userVoiceChannel);
}
await Enqueue(guildId, conn, media);
}
private async Task Enqueue(ulong guildId, Connection conn, Media media)
{
var startPlay = conn.Queue.Count == 0;
media.Search.Trim();
switch (YoutubeService.GetSearchType(media.Search))
{
case SearchType.StringSearch:
case SearchType.VideoURL:
media = await YoutubeService.DownloadMetadataFromYoutube(media);
if (media.VideoId == null)
{
await media.Channel.SendTemporaryMessageAsync($"No video found for \"{media.Search}\".");
return;
}
conn.Queue.Enqueue(media);
if (startPlay)
await PlayNext(guildId);
else
{
var message = $"Queued **{media.Name}** *({media.Length.TotalMinutes:00}:{media.Length.Seconds:00})*";
media.QueueMessage = await media.Channel.SendMessageAsync(message);
}
break;
case SearchType.ChannelURL:
case SearchType.PlaylistURL:
var collection = await YoutubeService.DownloadPlaylistMetadataFromYoutube(media.Message, media.Search);
collection.Medias.ForEach(m => conn.Queue.Enqueue(m));
await media.Channel.SendMessageAsync($"Queued **{collection.Medias.Count}** items from *{collection.CollectionName}* playlist.");
if (startPlay)
await PlayNext(guildId);
break;
case SearchType.None:
default:
break;
}
}
private async Task PlayNext(ulong guildId)
{
if (!Clients.ContainsKey(guildId) || Clients[guildId].Queue.Count == 0)
{
return;
}
var nextMedia = Clients[guildId].Queue.FirstOrDefault();
if (nextMedia == null)
{
await Stop(guildId);
return;
}
// since we can't verify if the bot was disconnected by a websocket error, we do this check which should do enough
if (Clients[guildId].AudioClient.ConnectionState == ConnectionState.Disconnected)
{
var voiceChannel = Clients[guildId].AudioChannel;
Clients.Remove(guildId);
await CreateConnection(guildId, voiceChannel);
}
var mp3Stream = await YoutubeService.DownloadAudioFromYoutube(nextMedia);
if (mp3Stream == null)
{
await Stop(guildId);
return;
}
var audioClient = Clients[guildId].AudioClient;
if (!nextMedia.Flags.Silent)
{
var message = $"⏯ Playing: **{nextMedia.Name}** *({nextMedia.Length.TotalMinutes:00}:{nextMedia.Length.Seconds:00})*";
nextMedia.PlayMessage = await nextMedia.Channel.SendMessageAsync(message);
}
if (nextMedia.QueueMessage != null)
{
await nextMedia.QueueMessage.TryDeleteAsync();
}
AudioService.StartAudioTask(mp3Stream, audioClient,
(outAudioStream) =>
{
Clients[guildId].CurrentAudioStream = outAudioStream;
},
async (ac) =>
{
if (ac.Exception != null)
{
await nextMedia.Channel.SendTemporaryMessageAsync("Error in stream: " + ac.Exception.ToString());
}
});
await nextMedia.PlayMessage.TryDeleteAsync();
if (Clients[guildId].Queue.Count > 0 &&
!Clients[guildId].Queue.First().Flags.Repeat)
Clients[guildId].Queue.Dequeue();
await PlayNext(guildId);
}
public Task Skip(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
throw new Exception("Bot is not connected!");
var media = Clients[guildId];
if (media.CurrentAudioStream == null)
throw new Exception("There is no audio playing!");
media.CurrentAudioStream.Close();
return Task.CompletedTask;
}
public async Task Stop(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
throw new Exception("Bot is not connected!");
var media = Clients[guildId];
foreach (var v in media.Queue.Skip(0))
{
await v.PlayMessage.TryDeleteAsync();
}
media.Queue.Clear();
if (media.CurrentAudioStream != null)
media.CurrentAudioStream.Close();
}
public async Task Leave(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
throw new Exception("Bot is not connected!");
await Stop(guildId);
var media = Clients[guildId];
if (media.AudioClient != null)
await media.AudioClient.StopAsync();
Clients.Remove(guildId);
}
public async Task Repeat(ulong guildId)
{
if (!Clients.ContainsKey(guildId))
throw new Exception("Bot is not connected!");
if (Clients[guildId].Queue.Count == 0)
throw new Exception("The queue is empty!");
var media = Clients[guildId].Queue.First();
media.Flags.Repeat = !media.Flags.Repeat;
await media.Channel.SendTemporaryMessageAsync(media.Flags.Repeat ? "Repeat turned on!" : "Repeat turned off!");
}
public async Task Join(ShardedCommandContext Context)
{
var guildId = Context.Guild.Id;
if (Clients.ContainsKey(guildId))
return;
await CreateConnection(guildId, (Context.User as IVoiceState).VoiceChannel);
}
}
}