Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
obj/
11 changes: 11 additions & 0 deletions .idea/.idea.Acrysel/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.idea.Acrysel/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .idea/.idea.Acrysel/.idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/.idea.Acrysel/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.Acrysel/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Acrysel.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Acrysel", "Acrysel\Acrysel.csproj", "{46372200-1B21-4A30-98BA-66FED682D388}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{46372200-1B21-4A30-98BA-66FED682D388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46372200-1B21-4A30-98BA-66FED682D388}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46372200-1B21-4A30-98BA-66FED682D388}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46372200-1B21-4A30-98BA-66FED682D388}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions Acrysel.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">Required</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">Required</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">Required</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue">Required</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Acrysel/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
17 changes: 17 additions & 0 deletions Acrysel/Acrysel.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<Folder Include="Controllers"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0-eap3"/>
<PackageReference Include="Remora.Discord" Version="2022.17.0"/>
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions Acrysel/Commands/TestCommandGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.ComponentModel;
using System.Drawing;
using System.Threading.Tasks;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Messages;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Results;

namespace Acrysel.Commands;

public class TestCommandGroup : CommandGroup
{
private readonly ICommandContext _context;
private readonly FeedbackService _feedbackService;

public TestCommandGroup(ICommandContext context, FeedbackService feedbackService)
{
_context = context;
_feedbackService = feedbackService;
}

[Command("hello")]
[Description("Say hello!")]
[Ephemeral]
public async Task<IResult> SayHelloAsync([Description("Your name.")] string name)
{
var message = $"Hello, {name}! How are you?";

var feedbackMessage = new FeedbackMessage(message, Color.Firebrick);

var reply = await _feedbackService.SendContextualMessageAsync(feedbackMessage);

return reply.IsSuccess
? Result.FromSuccess()
: Result.FromError(reply);
}
}
82 changes: 82 additions & 0 deletions Acrysel/Commands/YoutubeCommandGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Acrysel.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Feedback.Messages;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Results;

namespace Acrysel.Commands;

public class YoutubeCommandGroup : CommandGroup
{
private readonly ICommandContext _context;
private readonly FeedbackService _feedbackService;
private readonly IYoutubeApiClient _youtubeApiClient;

public YoutubeCommandGroup(IYoutubeApiClient youtubeApiClient, FeedbackService feedbackService,
ICommandContext context)
{
_youtubeApiClient = youtubeApiClient;
_feedbackService = feedbackService;
_context = context;
}

[Command("subscribe")]
[Description("subscribe to a channel")]
[Ephemeral]
public async Task<IResult> SubscribeAsync([Description("The channel you would like to search for.")] string query)
{
var searchResult = await _youtubeApiClient.SearchForChannelAsync(query);

if (!searchResult.IsSuccess)
{
return Result.FromError(searchResult.Error);
}

var embed = new Embed(Description: "Select which channel to subscribe to below.");

var options = new FeedbackMessageOptions(MessageComponents: new IMessageComponent[]
{
new ActionRowComponent(new[]
{
new SelectMenuComponent("ChannelSelector",
searchResult
.Entity
.Select(result =>
{
var createdAt = $" (Created At: {result.CreatedAt})";

var description = $"{result.Description}";

if (description.Length + createdAt.Length >= 100)
{
description = description[..(99 - createdAt.Length)];
}

description += createdAt;

var length = description.Length;

return new SelectOption(result.ChannelTitle, result.Id, description);
}).ToArray())
})
}
);

var embedResult = await _feedbackService.SendContextualEmbedAsync(embed, options);

if (!embedResult.IsSuccess)
{
return Result.FromError(embedResult.Error);
}

return Result.FromSuccess();
}
}
27 changes: 27 additions & 0 deletions Acrysel/Extensions/TreeRegistrationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Linq;
using System.Reflection;
using Remora.Commands.DependencyInjection;
using Remora.Commands.Groups;

namespace Acrysel.Extensions;

public static class TreeRegistrationBuilderExtensions
{
public static TreeRegistrationBuilder AddCommandsFromAssembly(this TreeRegistrationBuilder builder)
{
var allCommands = Assembly.GetExecutingAssembly()
.ExportedTypes
.Where(type => type.IsClass && type.IsAssignableTo(typeof(CommandGroup)))
.ToArray();

if (allCommands.Length > 0)
{
foreach (var command in allCommands)
{
builder.WithCommandGroup(command);
}
}

return builder;
}
}
108 changes: 108 additions & 0 deletions Acrysel/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Threading.Tasks;
using Acrysel.Extensions;
using Acrysel.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Remora.Commands.Extensions;
using Remora.Discord.API;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Commands.Services;
using Remora.Discord.Hosting.Extensions;
using Remora.Rest.Core;

var app = CreateApplication(args);

await UpdateSlashCommandsAsync(app);

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

WebApplication CreateApplication(string[] arguments)
{
var builder = WebApplication.CreateBuilder(arguments);

builder.Services.AddControllers();
builder.Services.AddSingleton<IYoutubeApiClient, YoutubeApiClient>();

var host = builder.Configuration.GetValue<string?>("HOST") ?? "localhost";
var port = builder.Configuration.GetValue<uint?>("PORT") ?? 5001;

builder.WebHost.UseUrls($"http://{host}:{port}");

builder.Host.AddDiscordService(services =>
{
var config = services.GetRequiredService<IConfiguration>();

var token = config.GetValue<string?>("DISCORD_TOKEN");

if (token is null)
{
throw new ArgumentException("Missing DISCORD_TOKEN in configuration.");
}

return token;
})
.ConfigureServices((_, services) =>
{
services
.AddDiscordCommands(true)
.AddCommandTree()
.AddCommandsFromAssembly();
})
.ConfigureLogging(loggingConfig =>
{
loggingConfig
.AddConsole()
.AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
.AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning);
});

return builder.Build();
}

async Task UpdateSlashCommandsAsync(WebApplication application)
{
var services = application.Services;

var configuration = services.GetRequiredService<IConfiguration>();
var slashService = services.GetRequiredService<SlashService>();
var logger = services.GetRequiredService<ILogger<Program>>();

var debugServer = default(Snowflake?);

#if DEBUG
var debugServerString = configuration.GetValue<string?>("DEBUG_SERVER");

if (debugServerString is null)
{
logger.LogWarning("No debug server specified when running in development mode");
}
else
{
if (DiscordSnowflake.TryParse(debugServerString, out debugServer))
{
logger.LogInformation("Debug server set to {DebugServer}", debugServer);
}
else
{
logger.LogWarning("Invalid snowflake for debug server specified: {DebugServer}", debugServer);
}
}
#endif

var updateSlashCommandsResult = await slashService.UpdateSlashCommandsAsync(debugServer);

if (!updateSlashCommandsResult.IsSuccess)
{
logger.LogError("Failed to update slash commands: {UpdateSlashCommandsError}",
updateSlashCommandsResult.Inner!.Error);
}
}
31 changes: 31 additions & 0 deletions Acrysel/Services/Entities/YoutubeChannelDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Acrysel.Services.Entities;

[PublicAPI]
public class YoutubeChannelDescriptor
{
[JsonPropertyName("channelId")] public string? Id { get; set; }

[JsonPropertyName("channelTitle")] public string? ChannelTitle { get; set; }

[JsonPropertyName("description")] public string? Description { get; set; }

[JsonPropertyName("publishedAt")] public DateTime CreatedAt { get; set; }

[JsonPropertyName("thumbnails")] public Thumbnails? Thumbnails { get; set; }
}

[PublicAPI]
public class Thumbnails
{
[JsonPropertyName("high")] public High? High { get; set; }
}

[PublicAPI]
public class High
{
[JsonPropertyName("url")] public Uri? Url { get; set; }
}
13 changes: 13 additions & 0 deletions Acrysel/Services/Entities/YoutubeChannelSearchResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace Acrysel.Services.Entities;

public class YoutubeChannelSearchResponse
{
[JsonPropertyName("items")] public Item[] Items { get; set; }

public class Item
{
[JsonPropertyName("snippet")] public YoutubeChannelDescriptor Descriptor { get; set; }
}
}
Loading