Skip to content

Commit

Permalink
feat: Librarian.Says("Hello World")
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverbooth committed Sep 30, 2023
1 parent 7d11867 commit 864c480
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 5 deletions.
43 changes: 43 additions & 0 deletions Librarian/Commands/BookmarkCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Discord;
using Discord.Interactions;
using Librarian.Services;

namespace Librarian.Commands;

/// <summary>
/// Represents a class which implements the <c>Bookmark</c> context menu.
/// </summary>
internal sealed class BookmarkCommand : InteractionModuleBase<SocketInteractionContext>
{
private readonly BookmarkService _bookmarkService;

/// <summary>
/// Initializes a new instance of the <see cref="BookmarkCommand" /> class.
/// </summary>
/// <param name="bookmarkService">The bookmark service.</param>
public BookmarkCommand(BookmarkService bookmarkService)
{
_bookmarkService = bookmarkService;
}

[MessageCommand("Bookmark")]
[RequireContext(ContextType.Guild)]
public async Task BookmarkAsync(IMessage message)
{
if (message is not IUserMessage userMessage)
{
return;
}

var member = (IGuildUser)Context.User;
IUserMessage? bookmark = await _bookmarkService.CreateBookmarkAsync(member, userMessage).ConfigureAwait(false);
if (bookmark is null)
{
await RespondAsync("Bookmark failed. Please make sure you have DMs enabled.", ephemeral: true).ConfigureAwait(false);
return;
}

var response = $"Bookmark created. [Check your DMs]({bookmark.GetJumpUrl()}) to view your bookmarks.";
await RespondAsync(response, ephemeral: true).ConfigureAwait(false);
}
}
57 changes: 57 additions & 0 deletions Librarian/Commands/InfoCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Text;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Humanizer;
using Librarian.Services;

namespace Librarian.Commands;

/// <summary>
/// Represents a class which implements the <c>info</c> command.
/// </summary>
internal sealed class InfoCommand : InteractionModuleBase<SocketInteractionContext>
{
private readonly BotService _botService;
private readonly DiscordSocketClient _discordClient;

/// <summary>
/// Initializes a new instance of the <see cref="InfoCommand" /> class.
/// </summary>
/// <param name="botService">The bot service.</param>
/// <param name="discordClient"></param>
public InfoCommand(BotService botService, DiscordSocketClient discordClient)
{
_botService = botService;
_discordClient = discordClient;
}

[SlashCommand("info", "Displays information about the bot.")]
[RequireContext(ContextType.Guild)]
public async Task InfoAsync()
{
SocketGuildUser member = Context.Guild.GetUser(_discordClient.CurrentUser.Id);
string botVerison = _botService.Version;

SocketRole? highestRole = member.Roles.Where(r => r.Color != Color.Default).MaxBy(r => r.Position);

var embed = new EmbedBuilder();
embed.WithAuthor(member);
embed.WithColor(highestRole?.Color ?? Color.Default);
embed.WithThumbnailUrl(member.GetAvatarUrl());
embed.WithTitle($"Librarian v{botVerison}");
embed.AddField("Ping", $"{_discordClient.Latency} ms", true);
embed.AddField("Uptime", (DateTimeOffset.UtcNow - _botService.StartedAt).Humanize(), true);
embed.AddField("View Source", "[View on GitHub](https://github.com/BrackeysBot/Librarian)", true);

var builder = new StringBuilder();
builder.AppendLine($"Librarian: {botVerison}");
builder.AppendLine($"Discord.Net: {_botService.DiscordNetVersion}");
builder.AppendLine($"CLR: {Environment.Version.ToString(3)}");
builder.AppendLine($"Host: {Environment.OSVersion}");

embed.AddField("Version", $"```\n{builder}\n```");

await RespondAsync(embed: embed.Build(), ephemeral: true).ConfigureAwait(false);
}
}
37 changes: 34 additions & 3 deletions Librarian/Librarian.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,43 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>

<PropertyGroup Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' == ''">
<Version>$(VersionPrefix)-$(VersionSuffix)</Version>
<AssemblyVersion>$(VersionPrefix).0</AssemblyVersion>
<FileVersion>$(VersionPrefix).0</FileVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">
<Version>$(VersionPrefix)-$(VersionSuffix).$(BuildNumber)</Version>
<AssemblyVersion>$(VersionPrefix).$(BuildNumber)</AssemblyVersion>
<FileVersion>$(VersionPrefix).$(BuildNumber)</FileVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(VersionSuffix)' == ''">
<Version>$(VersionPrefix)</Version>
<AssemblyVersion>$(VersionPrefix).0</AssemblyVersion>
<FileVersion>$(VersionPrefix).0</FileVersion>
</PropertyGroup>

<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>

<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<PackageReference Include="Discord.Net" Version="3.12.0"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1"/>
<PackageReference Include="Serilog" Version="3.0.1"/>
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="X10D" Version="3.3.1"/>
<PackageReference Include="X10D.Hosting" Version="3.3.1"/>
</ItemGroup>

</Project>
42 changes: 40 additions & 2 deletions Librarian/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
// See https://aka.ms/new-console-template for more information
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Librarian.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using X10D.Hosting.DependencyInjection;

Console.WriteLine("Hello, World!");
Directory.CreateDirectory("data");

Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/latest.log", rollingInterval: RollingInterval.Day)
#if DEBUG
.MinimumLevel.Debug()
#endif
.CreateLogger();


HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonFile("data/config.json", true, true);
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();

builder.Services.AddSingleton<DiscordSocketClient>();
builder.Services.AddSingleton<InteractionService>();
builder.Services.AddSingleton(new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMessages | GatewayIntents.MessageContent
});

builder.Services.AddHostedSingleton<BookmarkService>();
builder.Services.AddHostedSingleton<BookmarkEmoteService>();

builder.Services.AddHostedSingleton<BotService>();

IHost app = builder.Build();
await app.RunAsync();
51 changes: 51 additions & 0 deletions Librarian/Services/BookmarkEmoteService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Hosting;

namespace Librarian.Services;

/// <summary>
/// Represents a service which listens for the bookmark emote.
/// </summary>
internal sealed class BookmarkEmoteService : BackgroundService
{
private readonly DiscordSocketClient _discordClient;
private readonly BookmarkService _bookmarkService;

/// <summary>
/// Initializes a new instance of the <see cref="BookmarkDeletionService" /> class.
/// </summary>
/// <param name="discordClient">The Discord client.</param>
/// <param name="bookmarkService">The bookmark service.</param>
public BookmarkEmoteService(DiscordSocketClient discordClient, BookmarkService bookmarkService)
{
_discordClient = discordClient;
_bookmarkService = bookmarkService;
}

/// <inheritdoc />
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_discordClient.ReactionAdded += OnBookmarkAdded;
return Task.CompletedTask;
}

private async Task OnBookmarkAdded(Cacheable<IUserMessage, ulong> message,
Cacheable<IMessageChannel, ulong> channel,
SocketReaction reaction)
{
var theChannel = (ITextChannel)(channel.HasValue ? channel.Value : reaction.Channel);
var theMessage = (IUserMessage)(message.HasValue
? message.Value
: await theChannel.GetMessageAsync(message.Id).ConfigureAwait(false)
);

if (theMessage.Channel is not ITextChannel) return; // ignore DMs
if (reaction.Emote.Name != "🔖") return;

IUser user = reaction.User.Value;

await theMessage.RemoveReactionAsync(reaction.Emote, user).ConfigureAwait(false);
await _bookmarkService.CreateBookmarkAsync((IGuildUser)user, theMessage).ConfigureAwait(false);
}
}
90 changes: 90 additions & 0 deletions Librarian/Services/BookmarkService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Hosting;

namespace Librarian.Services;

internal sealed class BookmarkService : BackgroundService
{
private readonly DiscordSocketClient _discordClient;

/// <summary>
/// Initializes a new instance of the <see cref="BookmarkService" /> class.
/// </summary>
/// <param name="discordClient">The Discord client.</param>
public BookmarkService(DiscordSocketClient discordClient)
{
_discordClient = discordClient;
}

/// <summary>
/// Creates a bookmark for the specified message.
/// </summary>
/// <param name="user">The user who bookmarked the message.</param>
/// <param name="message">The message to bookmark.</param>
/// <returns>The bookmark sent to the user.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="user" /> or <paramref name="message" /> is <see langword="null" />.
/// </exception>
public async Task<IUserMessage?> CreateBookmarkAsync(IGuildUser user, IUserMessage message)
{
if (user is null) throw new ArgumentNullException(nameof(user));
if (message is null) throw new ArgumentNullException(nameof(message));

IDMChannel dmChannel;
try
{
dmChannel = await user.CreateDMChannelAsync();
}
catch
{
return null;
}

IGuild guild = user.Guild;

var embed = new EmbedBuilder();
embed.WithTitle("🔖 Bookmarked Message");
embed.WithDescription($"You bookmarked a message in **{guild.Name}**.");
embed.WithColor(Color.Green);
embed.WithTimestamp(DateTimeOffset.UtcNow);
embed.WithThumbnailUrl(guild.IconUrl);
embed.AddField("Author", message.Author.Mention, true);
embed.AddField("Sent", $"<t:{message.Timestamp.ToUnixTimeSeconds()}:R>", true);
embed.AddField("Channel", MentionUtils.MentionChannel(message.Channel.Id), true);

var jumpButton = new ButtonBuilder();
jumpButton.WithLabel("Jump to Message");
jumpButton.WithStyle(ButtonStyle.Link);
jumpButton.WithUrl(message.GetJumpUrl());

var deleteButton = new ButtonBuilder();
deleteButton.WithLabel("Delete Bookmark");
deleteButton.WithStyle(ButtonStyle.Danger);
deleteButton.WithCustomId("delete_bookmark");
deleteButton.WithEmote(new Emoji("🗑️"));

var components = new ComponentBuilder();
components.WithButton(jumpButton);
components.WithButton(deleteButton);

return await dmChannel.SendMessageAsync(embed: embed.Build(), components: components.Build()).ConfigureAwait(false);
}

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_discordClient.ButtonExecuted += OnDeleteBookmark;
return Task.CompletedTask;
}

private async Task OnDeleteBookmark(SocketMessageComponent component)
{
if (component.Data.CustomId != "delete_bookmark")
{
return;
}

await component.Message.DeleteAsync().ConfigureAwait(false);
await component.RespondAsync("Bookmark deleted!", ephemeral: true).ConfigureAwait(false);
}
}
Loading

0 comments on commit 864c480

Please sign in to comment.