diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..79098dc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+db.dat
+config.json
+
+# Common IntelliJ Platform excludes
+
+# User specific
+**/.idea/**/workspace.xml
+**/.idea/**/tasks.xml
+**/.idea/shelf/*
+**/.idea/dictionaries
+**/.idea/httpRequests/
+
+# Sensitive or high-churn files
+**/.idea/**/dataSources/
+**/.idea/**/dataSources.ids
+**/.idea/**/dataSources.xml
+**/.idea/**/dataSources.local.xml
+**/.idea/**/sqlDataSources.xml
+**/.idea/**/dynamic.xml
+
+# Rider
+# Rider auto-generates .iml files, and contentModel.xml
+**/.idea/**/*.iml
+**/.idea/**/contentModel.xml
+**/.idea/**/modules.xml
+
+*.suo
+*.user
+.vs/
+[Bb]in/
+[Oo]bj/
+_UpgradeReport_Files/
+[Pp]ackages/
+Logs/
+
+Thumbs.db
+Desktop.ini
+.DS_Store
+
+bin/
+obj/
\ No newline at end of file
diff --git a/.idea/.idea.QuoteBot/.idea/.gitignore b/.idea/.idea.QuoteBot/.idea/.gitignore
new file mode 100644
index 0000000..229aca8
--- /dev/null
+++ b/.idea/.idea.QuoteBot/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/.idea.QuoteBot.iml
+/modules.xml
+/projectSettingsUpdater.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.QuoteBot/.idea/encodings.xml b/.idea/.idea.QuoteBot/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.QuoteBot/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.QuoteBot/.idea/indexLayout.xml b/.idea/.idea.QuoteBot/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.QuoteBot/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Commands/ConfigureQuotes.cs b/Commands/ConfigureQuotes.cs
new file mode 100644
index 0000000..a92612a
--- /dev/null
+++ b/Commands/ConfigureQuotes.cs
@@ -0,0 +1,35 @@
+using Discord;
+using Discord.WebSocket;
+using SimpleDiscordNet.Commands;
+
+namespace QuoteBot.Commands;
+
+public class ConfigureQuotes {
+ private static readonly string[] ValidForms = ["embed", "fake-msg", "bot-msg"];
+
+ [SlashCommand("configure-quotes", "Change quote settings", GuildPermission.ManageGuild)]
+ [SlashCommandArgument("channel", "The quotes channel to send quotes to", true, ApplicationCommandOptionType.Channel)]
+ [SlashCommandArgument("quote-form", "How to display quotes: 'embed', 'fake-msg' or 'bot-msg'", true, ApplicationCommandOptionType.String)]
+ public async Task Execute(SocketSlashCommand cmd, DiscordSocketClient client) {
+ IGuildChannel channel = cmd.GetArgument("channel")!;
+ string form = cmd.GetArgument("quote-form")!;
+
+ if (!ValidForms.Contains(form)) {
+ await cmd.RespondWithEmbedAsync("Configure Quotes", "Invalid quote form, must be 'embed', 'fake-msg' or 'bot-msg'.", ResponseType.Error, ephemeral: false);
+ return;
+ }
+
+ if (cmd.Channel.GetChannelType() == ChannelType.DM) {
+ await cmd.RespondWithEmbedAsync("Configure Quotes", "You can't do this in your DMs.", ResponseType.Error, ephemeral: false);
+ return;
+ }
+
+ if (channel is not ITextChannel textChannel) {
+ await cmd.RespondWithEmbedAsync("Configure Quotes", "Channel must be a text channel.", ResponseType.Error, ephemeral: true);
+ return;
+ }
+
+ Program.Storage.SetQuoteSettings(cmd.GuildId!.Value, textChannel.Id, form);
+ await cmd.RespondWithEmbedAsync("Configure Quotes", "Quotes have been configured in this server.", ResponseType.Success, ephemeral: true);
+ }
+}
\ No newline at end of file
diff --git a/Commands/QuoteCommand.cs b/Commands/QuoteCommand.cs
new file mode 100644
index 0000000..40c3cea
--- /dev/null
+++ b/Commands/QuoteCommand.cs
@@ -0,0 +1,58 @@
+using Discord;
+using Discord.WebSocket;
+using GeneralPurposeLib;
+using QuoteBot.Data;
+using SimpleDiscordNet.Commands;
+
+namespace QuoteBot.Commands;
+
+public class QuoteCommand {
+
+ [SlashCommand("quote-user", "Quote something someone said and attribute quote to user")]
+ [SlashCommandArgument("person", "The person you are quoting", true, ApplicationCommandOptionType.User)]
+ [SlashCommandArgument("message", "What they said", true, ApplicationCommandOptionType.String)]
+ public Task WithUser(SocketSlashCommand cmd, DiscordSocketClient client) {
+ IUser quotee = cmd.GetArgument("person")!;
+ string message = cmd.GetArgument("message")!;
+ return Execute(cmd, client, message, quotee: quotee);
+ }
+
+ [SlashCommand("quote", "Quote something someone said")]
+ [SlashCommandArgument("person", "The person you are quoting", true, ApplicationCommandOptionType.String)]
+ [SlashCommandArgument("message", "What they said", true, ApplicationCommandOptionType.String)]
+ public Task WithString(SocketSlashCommand cmd, DiscordSocketClient client) {
+ string quotee = cmd.GetArgument("person")!;
+ string message = cmd.GetArgument("message")!;
+ return Execute(cmd, client, message, quoteeName: quotee);
+ }
+
+ public async Task Execute(SocketSlashCommand cmd, DiscordSocketClient client, string msg, string? quoteeName = null, IUser? quotee = null) {
+ if (cmd.Channel.GetChannelType() == ChannelType.DM) {
+ await cmd.RespondWithEmbedAsync("Quote", "You can't do this in your DMs.", ResponseType.Error, ephemeral: false);
+ return;
+ }
+
+ ITextChannel? quotesChannel = await cmd.GetQuotesChannel(client);
+
+ if (quotesChannel == null) {
+ await cmd.RespondWithEmbedAsync("Error",
+ "There is no quotes channel configured, ask a staff member to configure it.", ResponseType.Error,
+ ephemeral: true);
+ return;
+ }
+
+ await cmd.DeferAsync();
+
+ await Quoting.QuoteMessage(client,
+ cmd.GuildId!.Value,
+ quoteeName ?? quotee!.Username,
+ cmd.User.Username,
+ cmd.User.Id,
+ quoteeName ?? quotee!.Id.ToString(),
+ msg,
+ quotee);
+
+ await cmd.ModifyWithEmbedAsync("Quotes", $"Quote has been created in {quotesChannel.Mention}.",
+ ResponseType.Success);
+ }
+}
\ No newline at end of file
diff --git a/Data/Quote.cs b/Data/Quote.cs
new file mode 100644
index 0000000..b063d00
--- /dev/null
+++ b/Data/Quote.cs
@@ -0,0 +1,3 @@
+namespace QuoteBot.Data;
+
+public record Quote(ulong Quoter, string Quotee, ulong Guild, ulong MessageId, string Text, ulong? QuotedMessageChannel = null, ulong? QuotedMessageId = null);
\ No newline at end of file
diff --git a/Data/Storage/IStorageService.cs b/Data/Storage/IStorageService.cs
new file mode 100644
index 0000000..209dfec
--- /dev/null
+++ b/Data/Storage/IStorageService.cs
@@ -0,0 +1,11 @@
+namespace QuoteBot.Data.Storage;
+
+public interface IStorageService {
+ void Init();
+ void Deinit();
+
+ void CreateQuote(Quote quote);
+ void SetQuoteSettings(ulong guild, ulong channel, string form);
+ ulong? GetQuoteChannel(ulong guild);
+ string? GetQuoteForm(ulong guild);
+}
diff --git a/Data/Storage/SqliteStorageService.cs b/Data/Storage/SqliteStorageService.cs
new file mode 100644
index 0000000..2e9f87a
--- /dev/null
+++ b/Data/Storage/SqliteStorageService.cs
@@ -0,0 +1,80 @@
+using System.Data.SQLite;
+
+namespace QuoteBot.Data.Storage;
+
+public class SqliteStorageService : IStorageService {
+ private const string ConnectionString = "Data Source=db.dat";
+ private SQLiteConnection _connection = null!;
+
+ public void Init() {
+ _connection = new SQLiteConnection(ConnectionString);
+ _connection.Open();
+ CreateTables();
+ }
+
+ private void CreateTables() {
+ SQLiteCommand cmd = new(@"
+CREATE TABLE IF NOT EXISTS quotes (
+ quoter VARCHAR(64),
+ quotee VARCHAR(64),
+ guild VARCHAR(64),
+ message_id VARCHAR(64),
+ quote TEXT,
+ quoted_message_channel VARCHAR(64),
+ quoted_message_id VARCHAR(64)
+);
+
+CREATE TABLE IF NOT EXISTS guild_configs (
+ guild VARCHAR(64) PRIMARY KEY,
+ channel_id VARCHAR(64),
+ form VARCHAR(16)
+)
+", _connection);
+ cmd.ExecuteNonQuery();
+ }
+
+ public void Deinit() {
+ _connection.Dispose();
+ }
+
+ public void CreateQuote(Quote quote) {
+ using SQLiteCommand cmd = new("INSERT INTO quotes (quoter, quotee, guild, message_id, quote, quoted_message_channel, quoted_message_id) " +
+ "VALUES (@quoter, @quotee, @guild, @message_id, @quote, @quoted_message_channel, @quoted_message_id);", _connection);
+ cmd.Parameters.AddWithValue("quoter", quote.Quoter.ToString());
+ cmd.Parameters.AddWithValue("quotee", quote.Quotee);
+ cmd.Parameters.AddWithValue("guild", quote.Guild.ToString());
+ cmd.Parameters.AddWithValue("message_id", quote.MessageId.ToString());
+ cmd.Parameters.AddWithValue("quote", quote.Text);
+ cmd.Parameters.AddWithValue("quoted_message_channel", quote.QuotedMessageChannel.ToString());
+ cmd.Parameters.AddWithValue("quoted_message_id", quote.QuotedMessageId.ToString());
+ cmd.ExecuteNonQuery();
+ }
+
+ public void SetQuoteSettings(ulong guild, ulong channel, string form) {
+ using SQLiteCommand cmd = new("INSERT OR REPLACE INTO guild_configs (guild, channel_id, form) VALUES (@guild, @channel, @form);", _connection);
+ cmd.Parameters.AddWithValue("guild", guild.ToString());
+ cmd.Parameters.AddWithValue("channel", channel.ToString());
+ cmd.Parameters.AddWithValue("form", form);
+ cmd.ExecuteNonQuery();
+ }
+
+ public ulong? GetQuoteChannel(ulong guild) {
+ using SQLiteCommand cmd = new("SELECT channel_id FROM guild_configs WHERE guild = @guild;", _connection);
+ cmd.Parameters.AddWithValue("guild", guild.ToString());
+ using SQLiteDataReader reader = cmd.ExecuteReader();
+ if (!reader.Read()) {
+ return null;
+ }
+ return ulong.Parse(reader.GetString(0));
+ }
+
+ public string? GetQuoteForm(ulong guild) {
+ using SQLiteCommand cmd = new("SELECT form FROM guild_configs WHERE guild = @guild;", _connection);
+ cmd.Parameters.AddWithValue("guild", guild.ToString());
+ using SQLiteDataReader reader = cmd.ExecuteReader();
+ if (!reader.Read()) {
+ return null;
+ }
+ return reader.GetString(0);
+ }
+}
\ No newline at end of file
diff --git a/DefaultConfig.cs b/DefaultConfig.cs
new file mode 100644
index 0000000..f174ac2
--- /dev/null
+++ b/DefaultConfig.cs
@@ -0,0 +1,9 @@
+using GeneralPurposeLib;
+
+namespace QuoteBot;
+
+public static class DefaultConfig {
+ public static Dictionary Values = new() {
+ { "token", "xxxxxxxxxxxxxxx" }
+ };
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..7cd2890
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,37 @@
+using GeneralPurposeLib;
+using QuoteBot.Data.Storage;
+using SimpleDiscordNet;
+
+namespace QuoteBot;
+
+public static class Program {
+ public static IStorageService Storage = null!;
+
+ public static async Task Main(string[] args) {
+ Logger.Init(LogLevel.Debug);
+ Config config = new(DefaultConfig.Values);
+
+ Storage = new SqliteStorageService();
+ Storage.Init();
+
+ SimpleDiscordBot bot = new(config["token"]);
+
+ bot.Log += message => {
+ Logger.Info(message.Message);
+ if (message.Exception != null) {
+ Logger.Info(message.Exception);
+ }
+ return Task.CompletedTask;
+ };
+
+ bot.Client.Ready += () => {
+ bot.UpdateCommands();
+ bot.Client.SetCustomStatusAsync("Watching for funny quotes");
+ Logger.Info("Bot ready");
+ return Task.CompletedTask;
+ };
+ await bot.StartBot();
+ Logger.Info("Bot started");
+ await bot.WaitAsync();
+ }
+}
\ No newline at end of file
diff --git a/QuoteBot.csproj b/QuoteBot.csproj
new file mode 100644
index 0000000..e58d448
--- /dev/null
+++ b/QuoteBot.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+ SimpleDiscordNet.dll
+
+
+
+
diff --git a/QuoteBot.sln b/QuoteBot.sln
new file mode 100644
index 0000000..063dc59
--- /dev/null
+++ b/QuoteBot.sln
@@ -0,0 +1,16 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuoteBot", "QuoteBot.csproj", "{132496D3-908A-4419-BB25-A08E11448FDE}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {132496D3-908A-4419-BB25-A08E11448FDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {132496D3-908A-4419-BB25-A08E11448FDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {132496D3-908A-4419-BB25-A08E11448FDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {132496D3-908A-4419-BB25-A08E11448FDE}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/Quoting.cs b/Quoting.cs
new file mode 100644
index 0000000..ef2d532
--- /dev/null
+++ b/Quoting.cs
@@ -0,0 +1,65 @@
+using Discord;
+using Discord.Webhook;
+using Discord.WebSocket;
+using QuoteBot.Data;
+
+namespace QuoteBot;
+
+public class Quoting {
+
+ public static async Task QuoteMessage(DiscordSocketClient client,
+ ulong guild,
+ string quoteeName,
+ string quoterName,
+ ulong quoterId,
+ string quoteeData,
+ string msg,
+ IUser? quotee = null,
+ ulong? quotedMsgChannel = null,
+ ulong? quotedMsgId = null) {
+ ITextChannel? quotesChannel = await client.GetQuotesChannel(guild);
+ if (quotesChannel == null) {
+ throw new Exception("Quotes channel isn't defined");
+ }
+
+ string form = Program.Storage.GetQuoteForm(guild)!; // Should be defined if channel is
+
+ IMessage createdMsg;
+ switch (form) {
+ case "embed": {
+ EmbedBuilder embedBuilder = new();
+ embedBuilder.WithTitle(quoteeName);
+ embedBuilder.WithDescription($"\"{msg}\"");
+ embedBuilder.WithFooter("Quoted by " + quoterName);
+ embedBuilder.WithColor(Color.Green);
+
+ createdMsg = await quotesChannel.SendMessageAsync(embed: embedBuilder.Build());
+ break;
+ }
+
+ case "fake-msg": {
+ IWebhook? webhook = (await quotesChannel.GetWebhooksAsync()).FirstOrDefault() ?? await quotesChannel.CreateWebhookAsync("Quotes");
+
+ DiscordWebhookClient webhookClient = new(webhook);
+ ulong newMsgId = await webhookClient.SendMessageAsync(text: msg,
+ username: quoteeName,
+ avatarUrl: quotee?.GetAvatarUrl());
+
+ createdMsg = await quotesChannel.GetMessageAsync(newMsgId);
+ break;
+ }
+
+ case "bot-msg": {
+ createdMsg = await quotesChannel.SendMessageAsync($"{quoteeName}: \"{msg}\"");
+ break;
+ }
+
+ default:
+ throw new Exception("Invalid quote form in db.");
+ }
+
+
+ Quote quote = new(quoterId, quoteeData, guild, createdMsg.Id, msg, quotedMsgChannel, quotedMsgId);
+ Program.Storage.CreateQuote(quote);
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..540b9e7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# Quote Bot
+Quote people in Discord.
\ No newline at end of file
diff --git a/ReplyQuoteListener.cs b/ReplyQuoteListener.cs
new file mode 100644
index 0000000..d3ec733
--- /dev/null
+++ b/ReplyQuoteListener.cs
@@ -0,0 +1,39 @@
+using Discord;
+using Discord.WebSocket;
+using SimpleDiscordNet.MessageReceived;
+
+namespace QuoteBot;
+
+public class ReplyQuoteListener {
+
+ [MessageListener]
+ public async Task MessageListen(SocketMessage msg, DiscordSocketClient client) {
+ if (msg.Channel is not IGuildChannel channel) {
+ return;
+ }
+
+ if (msg.MentionedUsers.All(u => u.Id != client.CurrentUser.Id)) {
+ return;
+ }
+
+ if (msg.Reference == null || !msg.Reference.MessageId.IsSpecified) {
+ await msg.Channel.SendMessageAsync("Reply to a message and ping me to quote it.",
+ messageReference: msg.ToReference());
+ return;
+ }
+
+ IMessage quotedMsg = await msg.Channel.GetMessageAsync(msg.Reference.MessageId.Value);
+ await Quoting.QuoteMessage(client,
+ channel.GuildId,
+ quotedMsg.Author.Username,
+ msg.Author.Username,
+ msg.Author.Id,
+ quotedMsg.Author.Id.ToString(),
+ quotedMsg.Content,
+ quotedMsg.Author,
+ quotedMsg.Channel.Id,
+ quotedMsg.Id);
+
+ await msg.AddReactionAsync(Emoji.Parse(":white_check_mark:"));
+ }
+}
\ No newline at end of file
diff --git a/SimpleDiscordNet.dll b/SimpleDiscordNet.dll
new file mode 100644
index 0000000..1872619
Binary files /dev/null and b/SimpleDiscordNet.dll differ
diff --git a/Utils.cs b/Utils.cs
new file mode 100644
index 0000000..1b5499d
--- /dev/null
+++ b/Utils.cs
@@ -0,0 +1,31 @@
+using Discord;
+using Discord.WebSocket;
+
+namespace QuoteBot;
+
+public static class Utils {
+
+ public static async Task GetQuotesChannel(this SocketSlashCommand cmd, DiscordSocketClient client) {
+ ulong? channelId = Program.Storage.GetQuoteChannel(cmd.GuildId!.Value);
+ if (channelId == null) {
+ return null;
+ }
+
+ IChannel channel = await client.GetChannelAsync(channelId.Value);
+ return channel as ITextChannel;
+ }
+
+ public static async Task GetQuotesChannel(this DiscordSocketClient client, ulong guild) {
+ ulong? channelId = Program.Storage.GetQuoteChannel(guild);
+ if (channelId == null) {
+ return null;
+ }
+
+ IChannel channel = await client.GetChannelAsync(channelId.Value);
+ return channel as ITextChannel;
+ }
+
+ public static MessageReference ToReference(this SocketMessage msg) {
+ return new MessageReference(msg.Id);
+ }
+}
\ No newline at end of file