diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml new file mode 100644 index 0000000..7b8e4d8 --- /dev/null +++ b/.github/FUNDING.yaml @@ -0,0 +1 @@ +github: [Poisnfang] diff --git a/Commands/BasicCommandsModule.cs b/Commands/BasicCommandsModule.cs index cda7366..3d27946 100644 --- a/Commands/BasicCommandsModule.cs +++ b/Commands/BasicCommandsModule.cs @@ -1,11 +1,6 @@ -using DSharpPlus; -using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.Entities; -using DSharpPlus.Interactivity; using DSharpPlus.Interactivity.Extensions; -using System; -using System.Threading.Tasks; namespace PoisnCopy.Commands; diff --git a/Commands/CopyChannelCommand.cs b/Commands/CopyChannelCommand.cs index 6f68144..2c388d1 100644 --- a/Commands/CopyChannelCommand.cs +++ b/Commands/CopyChannelCommand.cs @@ -1,13 +1,11 @@ -using DSharpPlus; +using CsvHelper; +using DSharpPlus; using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; using DSharpPlus.Entities; -using DSharpPlus.Interactivity; using DSharpPlus.Interactivity.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Globalization; +using System.IO; using static DSharpPlus.Entities.DiscordEmbedBuilder; namespace PoisnCopy.Commands; @@ -78,6 +76,9 @@ public async Task CopyChannel(CommandContext ctx) await ctx.RespondAsync( $"Copy command: `pc.loadchannel {selectedChannel.Value.GuildId} {selectedChannel.Value.Id}`" ); + await ctx.RespondAsync( + $"Export command: `pc.exportchannel {selectedChannel.Value.GuildId} {selectedChannel.Value.Id}`" + ); } [Command("loadchannel")] @@ -182,4 +183,85 @@ await ctx.Channel.SendMessageAsync( await ctx.RespondAsync($"{newChan.Name} copy complete!"); } -} \ No newline at end of file + + [Command("exportchannel")] + [Description("Export a copied channel")] + public async Task ExportChannel(CommandContext ctx, ulong guildId, ulong channelId) + { + var guild = await ctx.Client.GetGuildAsync(guildId); + var selectedChannel = guild.GetChannel(channelId); + + await ctx.Channel.SendMessageAsync("Starting export..."); + + await ctx.Channel.SendMessageAsync("Collecting messages..."); + + var messag = await selectedChannel.GetMessagesAsync(); + + var messCopy = messag.ToList(); + var more = await selectedChannel.GetMessagesAsync(100); + + while (more.Count > 0) + { + messCopy.AddRange(more); + more = await selectedChannel.GetMessagesBeforeAsync(more.LastOrDefault().Id, 100); + } + + await ctx.Channel.SendMessageAsync("Organizing messages..."); + + messCopy.Reverse(); + + var messageExports = new List(); + + await ctx.Channel.SendMessageAsync( + $"Exporting {messCopy.Count} messages... (this could take awhile)" + ); + + foreach (var mes in messCopy) + { + if (!string.IsNullOrEmpty(mes.Content)) + { + var textMessage = new MessageExport + { + AuthorName = mes.Author.Username, + IconUrl = mes.Author.AvatarUrl, + MessageConent = mes.Content, + Timestamp = mes.Timestamp.ToString("o") + }; + messageExports.Add(textMessage); + } + + if (mes.Attachments.Count > 0) + { + foreach (var att in mes.Attachments) + { + var imageMessage = new MessageExport + { + AuthorName = mes.Author.Username, + IconUrl = mes.Author.AvatarUrl, + MessageConent = att.Url, + Timestamp = mes.Timestamp.ToString("o") + }; + messageExports.Add(imageMessage); + } + } + } + + try + { + using var memStream = new MemoryStream(); + using var writer = new StreamWriter(memStream); + using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); + csv.WriteRecords(messageExports); + await writer.FlushAsync(); + memStream.Position = 0; + var fileMessage = new DiscordMessageBuilder() { Content = "Messages exported" }; + fileMessage.AddFile($"{selectedChannel.Name}-export.csv", memStream, true); + + await ctx.Channel.SendMessageAsync(fileMessage); + } + catch (Exception e) + { + await ctx.Channel.SendMessageAsync(e.Message); + } + } +} diff --git a/Images/snapshot-bmc-button.png b/Images/snapshot-bmc-button.png deleted file mode 100644 index 71ae4b9..0000000 Binary files a/Images/snapshot-bmc-button.png and /dev/null differ diff --git a/MessageExport.cs b/MessageExport.cs new file mode 100644 index 0000000..d466d17 --- /dev/null +++ b/MessageExport.cs @@ -0,0 +1,9 @@ +namespace PoisnCopy; + +internal class MessageExport +{ + public string AuthorName { get; set; } + public string IconUrl { get; set; } + public string MessageConent { get; set; } + public string Timestamp { get; set; } +} \ No newline at end of file diff --git a/PoisnCopy.csproj b/PoisnCopy.csproj index d7f086e..3429bfd 100644 --- a/PoisnCopy.csproj +++ b/PoisnCopy.csproj @@ -8,6 +8,7 @@ + diff --git a/Program.cs b/Program.cs index 2688c54..42dd428 100644 --- a/Program.cs +++ b/Program.cs @@ -1,141 +1,134 @@ -using System; -using System.Linq; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus; +using DSharpPlus; using DSharpPlus.CommandsNext; using DSharpPlus.Interactivity; -using Microsoft.Extensions.Configuration; -using DSharpPlus.Interactivity.Extensions; using DSharpPlus.Interactivity.Enums; -using System.Collections.Generic; +using DSharpPlus.Interactivity.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace PoisnCopy +namespace PoisnCopy; + +internal class Program { - internal class Program - { - /* This is the cancellation token we'll use to end the bot if needed(used for most async stuff). */ - private CancellationTokenSource _cts { get; set; } + /* This is the cancellation token we'll use to end the bot if needed(used for most async stuff). */ + private CancellationTokenSource _cts { get; set; } - /* We'll load the app config into this when we create it a little later. */ - private IConfigurationRoot _config; + /* We'll load the app config into this when we create it a little later. */ + private IConfigurationRoot _config; - /* These are the discord library's main classes */ - private DiscordClient _discord; - private CommandsNextExtension _commands; - private InteractivityExtension _interactivity; + /* These are the discord library's main classes */ + private DiscordClient _discord; + private CommandsNextExtension _commands; + private InteractivityExtension _interactivity; - /* Use the async main to create an instance of the class and await it(async main is only available in C# 7.1 onwards). */ + /* Use the async main to create an instance of the class and await it(async main is only available in C# 7.1 onwards). */ - private static async Task Main(string[] args) => await new Program().InitBot(args); + private static async Task Main(string[] args) => await new Program().InitBot(args); - private async Task InitBot(string[] args) + private async Task InitBot(string[] args) + { + try { - try - { - Console.WriteLine("[info] Welcome to my bot!"); - _cts = new CancellationTokenSource(); - - // Load the config file(we'll create this shortly) - Console.WriteLine("[info] Loading config file.."); - _config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("config.json", optional: false, reloadOnChange: true) - .Build(); - - // Create the DSharpPlus client - Console.WriteLine("[info] Creating discord client.."); - _discord = new DiscordClient( - new DiscordConfiguration - { - Token = _config.GetValue("discord:token"), - TokenType = TokenType.Bot, - Intents = DiscordIntents.AllUnprivileged | DiscordIntents.MessageContents - } - ); - - // Create the interactivity module(I'll show you how to use this later on) - _interactivity = _discord.UseInteractivity( - new InteractivityConfiguration() - { - PaginationBehaviour = PaginationBehaviour.WrapAround, // What to do when a pagination request times out - PaginationDeletion = PaginationDeletion.DeleteMessage, // How long to wait before timing out - Timeout = TimeSpan.FromSeconds(30) // Default time to wait for interactive commands like waiting for a message or a reaction - } - ); - - // Build dependancies and then create the commands module. - var services = BuildServices(); - _commands = _discord.UseCommandsNext( - new CommandsNextConfiguration - { - StringPrefixes = new List - { - _config.GetValue("discord:CommandPrefix") - }, // Load the command prefix(what comes before the command, eg "!" or "/") from our config file - Services = services, - EnableDms = false - } - ); - - Console.WriteLine("[info] Loading command modules.."); - - var type = typeof(IModule); // Get the type of our interface - var types = AppDomain.CurrentDomain - .GetAssemblies() // Get the assemblies associated with our project - .SelectMany(s => s.GetTypes()) // Get all the types - .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); // Filter to find any type that can be assigned to an IModule - - var typeList = types as Type[] ?? types.ToArray(); // Convert to an array - foreach (var t in typeList) + Console.WriteLine("[info] Welcome to my bot!"); + _cts = new CancellationTokenSource(); + + // Load the config file(we'll create this shortly) + Console.WriteLine("[info] Loading config file.."); + _config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("config.json", optional: false, reloadOnChange: true) + .Build(); + + // Create the DSharpPlus client + Console.WriteLine("[info] Creating discord client.."); + _discord = new DiscordClient( + new DiscordConfiguration { - _commands.RegisterCommands(t); + Token = _config.GetValue("discord:token"), + TokenType = TokenType.Bot, + Intents = DiscordIntents.AllUnprivileged | DiscordIntents.MessageContents } + ); - Console.WriteLine($"[info] Loaded {typeList.Count()} modules."); - await RunAsync(args); - } - catch (Exception ex) - { - // This will catch any exceptions that occur during the operation/setup of your bot. + // Create the interactivity module(I'll show you how to use this later on) + _interactivity = _discord.UseInteractivity( + new InteractivityConfiguration() + { + PaginationBehaviour = PaginationBehaviour.WrapAround, // What to do when a pagination request times out + PaginationDeletion = PaginationDeletion.DeleteMessage, // How long to wait before timing out + Timeout = TimeSpan.FromSeconds(30) // Default time to wait for interactive commands like waiting for a message or a reaction + } + ); + + // Build dependancies and then create the commands module. + var services = BuildServices(); + _commands = _discord.UseCommandsNext( + new CommandsNextConfiguration + { + StringPrefixes = new List + { + _config.GetValue("discord:CommandPrefix") + }, // Load the command prefix(what comes before the command, eg "!" or "/") from our config file + Services = services, + EnableDms = false + } + ); + + Console.WriteLine("[info] Loading command modules.."); - // Feel free to replace this with what ever logging solution you'd like to use. - // I may do a guide later on the basic logger I implemented in my most recent bot. - Console.Error.WriteLine(ex.ToString()); + var type = typeof(IModule); // Get the type of our interface + var types = AppDomain.CurrentDomain + .GetAssemblies() // Get the assemblies associated with our project + .SelectMany(s => s.GetTypes()) // Get all the types + .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); // Filter to find any type that can be assigned to an IModule + + var typeList = types as Type[] ?? types.ToArray(); // Convert to an array + foreach (var t in typeList) + { + _commands.RegisterCommands(t); } - } - private async Task RunAsync(string[] args) + Console.WriteLine($"[info] Loaded {typeList.Count()} modules."); + await RunAsync(args); + } + catch (Exception ex) { - // Connect to discord's service - Console.WriteLine("Connecting.."); - await _discord.ConnectAsync(); - Console.WriteLine("Connected!"); - var connections = _discord.Guilds.Count; - Console.WriteLine($"I am running on {connections} servers"); - - // Keep the bot running until the cancellation token requests we stop - while (!_cts.IsCancellationRequested) - await Task.Delay(TimeSpan.FromMinutes(1)); + // This will catch any exceptions that occur during the operation/setup of your bot. + + // Feel free to replace this with what ever logging solution you'd like to use. + // I may do a guide later on the basic logger I implemented in my most recent bot. + Console.Error.WriteLine(ex.ToString()); } + } + + private async Task RunAsync(string[] args) + { + // Connect to discord's service + Console.WriteLine("Connecting.."); + await _discord.ConnectAsync(); + Console.WriteLine("Connected!"); + var connections = _discord.Guilds.Count; + Console.WriteLine($"I am running on {connections} servers"); + + // Keep the bot running until the cancellation token requests we stop + while (!_cts.IsCancellationRequested) + await Task.Delay(TimeSpan.FromMinutes(1)); + } - /* - DSharpPlus has dependancy injection for commands, this builds a list of dependancies. - We can then access these in our command modules. - */ + /* + DSharpPlus has dependancy injection for commands, this builds a list of dependancies. + We can then access these in our command modules. + */ - private ServiceProvider BuildServices() - { - var deps = new ServiceCollection(); + private ServiceProvider BuildServices() + { + var deps = new ServiceCollection(); - deps.AddSingleton(_interactivity) // Add interactivity - .AddSingleton(_cts) // Add the cancellation token - .AddSingleton(_config) // Add our config - .AddSingleton(_discord); // Add the discord client + deps.AddSingleton(_interactivity) // Add interactivity + .AddSingleton(_cts) // Add the cancellation token + .AddSingleton(_config) // Add our config + .AddSingleton(_discord); // Add the discord client - return deps.BuildServiceProvider(); - } + return deps.BuildServiceProvider(); } } \ No newline at end of file diff --git a/README.md b/README.md index db29ede..a572931 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # ALERT -PoisnCopy is now on 100 servers which is the capacity based on [this post from Discord](https://support.discord.com/hc/en-us/articles/4410940809111). I do not have any plans to Verify my bot. this decision is not an easy one as I know this bot is very useful to many people as Discord does not offer this service themselves. I frankly do not have time to spend getting this bot into compliance. Should demand change for this, I might reconsider. -Therefore, _**if you try to add the bot to your servers it won't work.**_ Feel free to contact me directly with question: posinfang@poisnfang.com -### Show Some Support -Buy Me A Coffee +PoisnCopy is now on 100 servers which is the capacity based on [this post from Discord](https://support.discord.com/hc/en-us/articles/4410940809111). I hope to verify this bot at some point, but it might be take a while. + +Therefore, _**if you try to add the bot to your servers it won't work.**_ Contact me directly with questions: posinfang@poisnfang.com + +**_I can assist in setting this bot up for personal use. Please reach out if you need help!_** + +### Show Some Support - _Sponsor this project on Github_ + # PoisnCopy + Discord channel copy bot Add the bot to BOTH of your servers using this link. (NOTE: You must be the OWNER (Not just an admin) of BOTH servers) @@ -18,7 +23,7 @@ The bot will list the channels in the server that you are typing in. You must en **Make sure your bot can see which channel you are typing in (check left hand side)** ![image](https://user-images.githubusercontent.com/60050783/107395699-49d3a300-6aba-11eb-8b1c-d4e4b41cd6f3.png) -*Note: The `pc.copychannel` command is just to list the IDs, once you have them, you can just use the `pc.loadchannel`command. +\*Note: The `pc.copychannel` command is just to list the IDs, once you have them, you can just use the `pc.loadchannel`command. i.e.