├── Voltaire ├── Models │ ├── BannedIdentifier.cs │ └── Guild.cs ├── Controllers │ ├── Settings │ │ ├── Refresh.cs │ │ ├── ClearBans.cs │ │ ├── ClearAllowedRole.cs │ │ ├── SetEmbeds.cs │ │ ├── SetDirectMessageAccess.cs │ │ ├── SetUseUserIdentifiers.cs │ │ ├── SetAllowedRole.cs │ │ ├── ListBans.cs │ │ ├── SetAdminRole.cs │ │ ├── UnBanIdentifier.cs │ │ ├── GenereteGuildUserIdentifierSeed.cs │ │ └── BanIdentifier .cs │ ├── Helpers │ │ ├── UserJoined.cs │ │ ├── UserHasRole.cs │ │ ├── FindOrCreateGuild.cs │ │ ├── IncrementAndCheckMessageLimit.cs │ │ ├── JoinedGuild.cs │ │ └── EnsureActiveSubscription.cs │ ├── Subscriptions │ │ ├── Cancel.cs │ │ └── Pro.cs │ ├── Reactions │ │ └── React.cs │ └── Messages │ │ ├── SendReply.cs │ │ ├── PrefixHelper.cs │ │ ├── SendDirectMessage.cs │ │ ├── SendToGuild.cs │ │ └── Send.cs ├── Views │ ├── Info │ │ ├── JoinedGuild.cs │ │ ├── UserJoined.cs │ │ ├── Prompt.cs │ │ ├── Pro.cs │ │ ├── MultipleGuildSendResponse.cs │ │ ├── SlashAdmin.cs │ │ ├── SlashHelp.cs │ │ ├── UpgradeNotification.cs │ │ ├── Admin.cs │ │ └── Help.cs │ ├── Modals │ │ └── MessagePrompt.cs │ └── Message.cs ├── Modules │ ├── Reactions.cs │ ├── MessageCommand.cs │ ├── SubscriptionInteractions.cs │ ├── Subscription.cs │ ├── InteractionsBase.cs │ ├── InfoInteractions.cs │ ├── ReactionInteractions.cs │ ├── Info.cs │ ├── PromptInteractions.cs │ ├── Messages.cs │ ├── MessageInteractions.cs │ ├── Admin.cs │ └── AdminInteractions.cs ├── Migrations │ ├── 20190319045013_AddAdminRole.cs │ ├── 20181005012009_AddAllowedRole.cs │ ├── 20190312035652_SubscriptionId.cs │ ├── 20201025184258_AddEmbedOption.cs │ ├── 20180903043025_Added UserIdentfie.cs │ ├── 20180908141038_GuildUserIdentifierSeed.cs │ ├── 20191006234916_MessageLimit.cs │ ├── 20180726032554_InitialCreate.cs │ ├── 20180726032554_InitialCreate.Designer.cs │ ├── 20180903043025_Added UserIdentfie.Designer.cs │ ├── 20180908141038_GuildUserIdentifierSeed.Designer.cs │ ├── 20190220044024_BannedUsers.cs │ ├── 20181005012009_AddAllowedRole.Designer.cs │ ├── 20190220044024_BannedUsers.Designer.cs │ ├── 20190312035652_SubscriptionId.Designer.cs │ ├── 20190319045013_AddAdminRole.Designer.cs │ ├── DataBaseModelSnapshot.cs │ ├── 20191006234916_MessageLimit.Designer.cs │ └── 20201025184258_AddEmbedOption.Designer.cs ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── LoadConfig.cs ├── Database.cs ├── Utility │ ├── UnifiedContext.cs │ └── Encryption.cs ├── Preconditions │ ├── Administrator.cs │ └── AdministratorInteraction.cs ├── Voltaire.csproj └── Program.cs ├── LICENSE.md ├── Voltaire.sln ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── .gitattributes └── .gitignore /Voltaire/Models/BannedIdentifier.cs: -------------------------------------------------------------------------------- 1 | namespace Voltaire.Models 2 | { 3 | public class BannedIdentifier 4 | { 5 | public int ID { get; set; } 6 | public string Identifier { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/Refresh.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Voltaire.Controllers.Messages; 3 | 4 | namespace Voltaire.Controllers.Settings 5 | { 6 | class Refresh 7 | { 8 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 9 | { 10 | Helpers.JoinedGuild.Refresh(context.Guild); 11 | 12 | await Send.SendMessageToContext(context, "User list has been refreshed."); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/JoinedGuild.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Voltaire.Views.Info 9 | { 10 | public static class JoinedGuild 11 | { 12 | 13 | public static Tuple Response() 14 | { 15 | return new Tuple("Thanks for adding Voltaire to your server! Try: `/volt-help` to get rolling.", null); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Voltaire/Views/Modals/MessagePrompt.cs: -------------------------------------------------------------------------------- 1 | using Discord.Interactions; 2 | using Discord; 3 | 4 | namespace Voltaire.Views.Modals 5 | { 6 | public class MessagePrompt : IModal 7 | { 8 | // note, this title is currently always overwritten 9 | public string Title => "Sending anonymous message"; 10 | // Strings with the ModalTextInput attribute will automatically become components. 11 | [InputLabel("Message")] 12 | [ModalTextInput("message", style: TextInputStyle.Paragraph)] 13 | public string message { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/UserJoined.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Voltaire.Views.Info 9 | { 10 | public static class UserJoined 11 | { 12 | 13 | public static Tuple Response() 14 | { 15 | return new Tuple("Looks like you just joined a server using Voltaire. Voltaire allows you to send messages to the server anonymously. Try typing: `help` to get rolling.", null); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Helpers/UserJoined.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | using System.Threading.Tasks; 3 | 4 | 5 | namespace Voltaire.Controllers.Helpers 6 | { 7 | public static class UserJoined 8 | { 9 | public static async Task SendJoinedMessage(SocketGuildUser user) 10 | { 11 | if (user.IsBot) 12 | return; 13 | var view = Views.Info.UserJoined.Response(); 14 | var channel = await user.CreateDMChannelAsync(); 15 | await channel.SendMessageAsync(text: view.Item1, embed: view.Item2); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/ClearBans.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Voltaire.Controllers.Helpers; 3 | using Voltaire.Controllers.Messages; 4 | 5 | namespace Voltaire.Controllers.Settings 6 | { 7 | class ClearBans 8 | { 9 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 10 | { 11 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 12 | db.RemoveRange(guild.BannedIdentifiers); 13 | await db.SaveChangesAsync(); 14 | await Send.SendMessageToContext(context, "Bans cleared"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/ClearAllowedRole.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Voltaire.Controllers.Messages; 3 | using Voltaire.Controllers.Helpers; 4 | 5 | namespace Voltaire.Controllers.Settings 6 | { 7 | class ClearAllowedRole 8 | { 9 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 10 | { 11 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 12 | guild.AllowedRole = null; 13 | await db.SaveChangesAsync(); 14 | await Send.SendMessageToContext(context, $"All users can now use Voltaire."); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/SetEmbeds.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using System; 3 | using Voltaire.Controllers.Messages; 4 | using System.Threading.Tasks; 5 | using Voltaire.Controllers.Helpers; 6 | 7 | namespace Voltaire.Controllers.Settings 8 | { 9 | class SetEmbeds 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, Boolean setting, DataBase db) 12 | { 13 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 14 | guild.UseEmbed = setting; 15 | await db.SaveChangesAsync(); 16 | await Send.SendMessageToContext(context, $"'Embeds' set to {setting}"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/SetDirectMessageAccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Voltaire.Controllers.Messages; 3 | using System.Threading.Tasks; 4 | using Voltaire.Controllers.Helpers; 5 | 6 | namespace Voltaire.Controllers.Settings 7 | { 8 | class SetDirectMessageAccess 9 | { 10 | public static async Task PerformAsync(UnifiedContext context, Boolean setting, DataBase db) 11 | { 12 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 13 | guild.AllowDirectMessage = setting; 14 | await db.SaveChangesAsync(); 15 | await Send.SendMessageToContext(context, $"'Allow DM' set to {setting}"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/SetUseUserIdentifiers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Voltaire.Controllers.Messages; 3 | using System.Threading.Tasks; 4 | using Voltaire.Controllers.Helpers; 5 | 6 | namespace Voltaire.Controllers.Settings 7 | { 8 | class SetUseUserIdentifiers 9 | { 10 | public static async Task PerformAsync(UnifiedContext context, Boolean setting, DataBase db) 11 | { 12 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 13 | guild.UseUserIdentifiers = setting; 14 | await db.SaveChangesAsync(); 15 | await Send.SendMessageToContext(context, $"'User Identifiers' set to {setting}"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Voltaire/Modules/Reactions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Discord.Commands; 3 | 4 | namespace Voltaire.Modules 5 | { 6 | public class Reaction : ModuleBase 7 | { 8 | private DataBase _database; 9 | 10 | public Reaction(DataBase database) 11 | { 12 | _database = database; 13 | } 14 | 15 | [Command("react", RunMode = RunMode.Async)] 16 | [Summary("add a reaction to a message")] 17 | public async Task React(ulong messageId, string emoji) 18 | { 19 | await Controllers.Reactions.React.PerformAsync(new CommandBasedContext(Context), messageId, emoji, _database); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20190319045013_AddAdminRole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Voltaire.Migrations 4 | { 5 | public partial class AddAdminRole : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "AdminRole", 11 | table: "Guilds", 12 | nullable: true); 13 | } 14 | 15 | protected override void Down(MigrationBuilder migrationBuilder) 16 | { 17 | migrationBuilder.DropColumn( 18 | name: "AdminRole", 19 | table: "Guilds"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20181005012009_AddAllowedRole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Voltaire.Migrations 4 | { 5 | public partial class AddAllowedRole : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "AllowedRole", 11 | table: "Guilds", 12 | nullable: true); 13 | } 14 | 15 | protected override void Down(MigrationBuilder migrationBuilder) 16 | { 17 | migrationBuilder.DropColumn( 18 | name: "AllowedRole", 19 | table: "Guilds"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Voltaire/Modules/MessageCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Discord.Interactions; 3 | using System.Linq; 4 | using Discord.WebSocket; 5 | using System; 6 | using Discord; 7 | 8 | namespace Voltaire.Modules 9 | { 10 | public class MessageCommand : InteractionsBase 11 | { 12 | public MessageCommand(DataBase database): base(database) {} 13 | 14 | [MessageCommand("Create Thread Anonymously")] 15 | public async Task MessageCommandHandler(IMessage msg) 16 | { 17 | var channel = msg.Channel as Discord.ITextChannel; 18 | await channel.CreateThreadAsync("voltaire thread", message: msg); 19 | await RespondAsync("Thread Created!", ephemeral: true); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Voltaire/Migrations/20190312035652_SubscriptionId.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Voltaire.Migrations 4 | { 5 | public partial class SubscriptionId : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "SubscriptionId", 11 | table: "Guilds", 12 | nullable: true); 13 | } 14 | 15 | protected override void Down(MigrationBuilder migrationBuilder) 16 | { 17 | migrationBuilder.DropColumn( 18 | name: "SubscriptionId", 19 | table: "Guilds"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/SetAllowedRole.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | using Voltaire.Controllers.Messages; 3 | using System.Threading.Tasks; 4 | using Voltaire.Controllers.Helpers; 5 | 6 | namespace Voltaire.Controllers.Settings 7 | { 8 | class SetAllowedRole 9 | { 10 | public static async Task PerformAsync(UnifiedContext context, SocketRole role, DataBase db) 11 | { 12 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 13 | guild.AllowedRole = role.Id.ToString(); 14 | await db.SaveChangesAsync(); 15 | await Send.SendMessageToContext(context, $"{role.Name} is now the only role that can use Voltaire on this server"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Voltaire/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | FileSystem 9 | Release 10 | netcoreapp3.1 11 | C:\Users\noelm\Documents\Visual Studio 2017\Projects\VoltaireDist 12 | 13 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20201025184258_AddEmbedOption.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Voltaire.Migrations 4 | { 5 | public partial class AddEmbedOption : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "UseEmbed", 11 | table: "Guilds", 12 | nullable: false, 13 | defaultValue: false); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropColumn( 19 | name: "UseEmbed", 20 | table: "Guilds"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Voltaire/LoadConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | using System.IO; 4 | 5 | namespace Voltaire 6 | { 7 | public sealed class LoadConfig 8 | { 9 | 10 | private static readonly Lazy lazy = 11 | new Lazy(() => new LoadConfig()); 12 | 13 | public static LoadConfig Instance { get { return lazy.Value; } } 14 | 15 | public IConfiguration config; 16 | 17 | private LoadConfig() 18 | { 19 | var builder = new ConfigurationBuilder() 20 | .SetBasePath(Directory.GetCurrentDirectory()) 21 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); 22 | 23 | config = builder.Build(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20180903043025_Added UserIdentfie.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Voltaire.Migrations 4 | { 5 | public partial class AddedUserIdentfie : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "UseUserIdentifiers", 11 | table: "Guilds", 12 | nullable: false, 13 | defaultValue: false); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropColumn( 19 | name: "UseUserIdentifiers", 20 | table: "Guilds"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20180908141038_GuildUserIdentifierSeed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Voltaire.Migrations 4 | { 5 | public partial class GuildUserIdentifierSeed : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "UserIdentifierSeed", 11 | table: "Guilds", 12 | nullable: false, 13 | defaultValue: 0); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropColumn( 19 | name: "UserIdentifierSeed", 20 | table: "Guilds"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Helpers/UserHasRole.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.WebSocket; 3 | using System.Linq; 4 | using Voltaire.Models; 5 | 6 | namespace Voltaire.Controllers.Helpers 7 | { 8 | class UserHasRole 9 | { 10 | public static bool Perform(SocketGuild reciver, IUser sender, Guild receivingRecord) 11 | { 12 | if (receivingRecord == null || receivingRecord.AllowedRole == null) 13 | { 14 | return true; 15 | } 16 | var role = reciver.Roles.FirstOrDefault(x => x.Id.ToString() == receivingRecord.AllowedRole); 17 | if (role == null) 18 | { 19 | return false; 20 | } 21 | 22 | return role.Members.Any(x => x.Id == sender.Id); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Helpers/FindOrCreateGuild.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.Linq; 4 | using Voltaire.Models; 5 | using System.Threading.Tasks; 6 | using System; 7 | 8 | namespace Voltaire.Controllers.Helpers 9 | { 10 | class FindOrCreateGuild 11 | { 12 | public static async Task Perform(IGuild guild, DataBase db) 13 | { 14 | var dbGuild = await db.Guilds.Include(x => x.BannedIdentifiers).FirstOrDefaultAsync(u => u.DiscordId == guild.Id.ToString()); 15 | if (dbGuild == null) 16 | { 17 | dbGuild = new Guild { DiscordId = guild.Id.ToString() }; 18 | await db.Guilds.AddAsync(dbGuild); 19 | await db.SaveChangesAsync(); 20 | } 21 | return dbGuild; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/ListBans.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Voltaire.Controllers.Helpers; 5 | using Voltaire.Controllers.Messages; 6 | 7 | namespace Voltaire.Controllers.Settings 8 | { 9 | class ListBans 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 12 | { 13 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 14 | 15 | var bannedIdentifiers = guild.BannedIdentifiers.Select(x => x.Identifier).ToArray(); 16 | 17 | if (bannedIdentifiers.Count() == 0) 18 | { 19 | await Send.SendMessageToContext(context, $"No users are currently banned."); 20 | return; 21 | } 22 | 23 | await Send.SendMessageToContext(context, $"**Banned Users:**\n{String.Join("\n", bannedIdentifiers)}"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/Prompt.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | using Voltaire.Models; 4 | 5 | namespace Voltaire.Views.Info 6 | { 7 | public static class Prompt 8 | { 9 | 10 | public static Tuple Response(string prompt, Discord.WebSocket.SocketChannel channel, Discord.Interactions.ShardedInteractionContext context) 11 | { 12 | var text = prompt ?? $"Use the button below to send an anonymous message to \n <#{channel.Id}>"; 13 | 14 | var embed = new EmbedBuilder 15 | { 16 | Author = new EmbedAuthorBuilder 17 | { 18 | Name = "Voltaire Anonymous Messaging" 19 | }, 20 | Description = text, 21 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 22 | Color = new Color(111, 111, 111), 23 | }; 24 | return new Tuple("", embed.Build()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Voltaire/Models/Guild.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Voltaire.Models 5 | { 6 | public class Guild 7 | { 8 | public Guild() 9 | { 10 | this.BannedIdentifiers = new HashSet(); 11 | } 12 | 13 | public int ID { get; set; } 14 | public string DiscordId { get; set; } 15 | public string AllowedRole { get; set; } 16 | public string AdminRole { get; set; } 17 | public bool AllowDirectMessage { get; set; } = true; 18 | public bool UseUserIdentifiers { get; set; } = false; 19 | public bool UseEmbed { get; set; } = false; 20 | public int UserIdentifierSeed { get; set; } = 0; 21 | public virtual ICollection BannedIdentifiers { get; set; } 22 | public string SubscriptionId { get; set; } 23 | public int MessagesSentThisMonth { get; set; } = 0; 24 | public DateTime TrackingMonth { get; set; } = DateTime.Now; 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/SetAdminRole.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | using System.Threading.Tasks; 3 | using Voltaire.Controllers.Helpers; 4 | using Voltaire.Controllers.Messages; 5 | 6 | 7 | namespace Voltaire.Controllers.Settings 8 | { 9 | class SetAdminRole 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, SocketRole role, DataBase db) 12 | { 13 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 14 | 15 | if (!EnsureActiveSubscription.Perform(guild, db)) 16 | { 17 | await Send.SendMessageToContext(context, "You need an active Voltaire Pro subscription to set an admin role. To get started, use `/pro`"); 18 | return; 19 | } 20 | 21 | guild.AdminRole = role.Id.ToString(); 22 | await db.SaveChangesAsync(); 23 | await Send.SendMessageToContext(context, $"{role.Name} can now configure Voltaire and ban users on this server."); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Voltaire/Views/Message.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Voltaire.Views 9 | { 10 | public static class Message 11 | { 12 | 13 | public static Tuple Response(string username, string message) 14 | { 15 | EmbedBuilder embed = BuildEmbed(message); 16 | 17 | if (!string.IsNullOrEmpty(username)) 18 | { 19 | embed.Author = new EmbedAuthorBuilder 20 | { 21 | Name = username 22 | }; 23 | } 24 | 25 | return new Tuple("", embed.Build()); 26 | } 27 | 28 | private static EmbedBuilder BuildEmbed(string message) 29 | { 30 | return new EmbedBuilder 31 | { 32 | Description = message, 33 | Color = new Color(111, 111, 111) 34 | }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Helpers/IncrementAndCheckMessageLimit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Voltaire.Models; 4 | 5 | namespace Voltaire.Controllers.Helpers 6 | { 7 | class IncrementAndCheckMessageLimit 8 | { 9 | public static async Task Perform(Guild guild, DataBase db) 10 | { 11 | // see if we need to reset the counter 12 | CheckMonth(guild); 13 | 14 | // increment counter by one 15 | guild.MessagesSentThisMonth += 1; 16 | await db.SaveChangesAsync(); 17 | 18 | return guild.MessagesSentThisMonth <= 50 || EnsureActiveSubscription.Perform(guild, db); 19 | } 20 | 21 | public static void CheckMonth(Guild guild) 22 | { 23 | if (guild.TrackingMonth.Month != DateTime.Now.Month || guild.TrackingMonth.Year != DateTime.Now.Year) 24 | { 25 | guild.TrackingMonth = DateTime.Now; 26 | guild.MessagesSentThisMonth = 0; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Voltaire/Database.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Design; 3 | using Microsoft.Extensions.Configuration; 4 | using System.IO; 5 | using System; 6 | 7 | namespace Voltaire 8 | { 9 | public class DataBase : DbContext 10 | { 11 | public DbSet Guilds { get; set; } 12 | public DbSet BannedIdentifiers { get; set; } 13 | 14 | public DataBase(DbContextOptions options) : base(options) {} 15 | 16 | } 17 | 18 | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory 19 | { 20 | public DataBase CreateDbContext(string[] args) 21 | { 22 | IConfiguration configuration = LoadConfig.Instance.config; 23 | 24 | var optionsBuilder = new DbContextOptionsBuilder(); 25 | optionsBuilder.UseSqlServer($@"{configuration.GetConnectionString("sql")}"); 26 | 27 | return new DataBase(optionsBuilder.Options); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Voltaire/Modules/SubscriptionInteractions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Discord.Interactions; 3 | using Discord; 4 | 5 | namespace Voltaire.Modules 6 | { 7 | public class SubscriptionInteractions : InteractionsBase 8 | { 9 | 10 | public SubscriptionInteractions(DataBase database): base(database) {} 11 | 12 | 13 | [SlashCommand("pro", "generate a link to upgrade to voltaire pro or get current subscription info")] 14 | [RequireUserPermission(GuildPermission.Administrator)] 15 | public async Task Pro() 16 | { 17 | await Controllers.Subscriptions.Pro.PerformAsync(new InteractionBasedContext(Context, PublicResponder), _database); 18 | } 19 | 20 | [SlashCommand("pro-cancel", "cancel your voltaire pro subscription")] 21 | [RequireUserPermission(GuildPermission.Administrator)] 22 | public async Task Cancel() 23 | { 24 | await Controllers.Subscriptions.Cancel.PerformAsync(new InteractionBasedContext(Context, PublicResponder), _database); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/Pro.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | using Voltaire.Models; 4 | 5 | namespace Voltaire.Views.Info 6 | { 7 | public static class Pro 8 | { 9 | 10 | public static Tuple Response(string url, Guild guild, DataBase db) 11 | { 12 | 13 | var embed = new EmbedBuilder 14 | { 15 | Author = new EmbedAuthorBuilder 16 | { 17 | Name = "Upgrade To Voltaire Pro With the Folling URL:" 18 | }, 19 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 20 | Description = $"{url}", 21 | Color = new Color(111, 111, 111) 22 | }; 23 | 24 | Controllers.Helpers.IncrementAndCheckMessageLimit.CheckMonth(guild); 25 | 26 | embed.AddField("Messages Sent This Month:", $"**{guild.MessagesSentThisMonth}**/50 (note: any messages sent above the limit have been surpressed)"); 27 | 28 | return new Tuple("", embed.Build()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Voltaire/Modules/Subscription.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Discord.Commands; 3 | using Discord; 4 | 5 | namespace Voltaire.Modules 6 | { 7 | public class Subscription : ModuleBase 8 | { 9 | private DataBase _database; 10 | 11 | public Subscription(DataBase database) 12 | { 13 | _database = database; 14 | } 15 | 16 | [Command("pro", RunMode = RunMode.Async)] 17 | [Summary("Upgrade your subscription")] 18 | [RequireUserPermission(GuildPermission.Administrator)] 19 | public async Task Pro() 20 | { 21 | await Controllers.Subscriptions.Pro.PerformAsync(new CommandBasedContext(Context), _database); 22 | } 23 | 24 | [Command("cancel", RunMode = RunMode.Async)] 25 | [Summary("Cancel your subscription")] 26 | [RequireUserPermission(GuildPermission.Administrator)] 27 | public async Task Cancel() 28 | { 29 | await Controllers.Subscriptions.Cancel.PerformAsync(new CommandBasedContext(Context), _database); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Noel Minchow 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Voltaire.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Voltaire", "Voltaire\Voltaire.csproj", "{1A52F479-288C-4998-8336-B196CD5321A9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {1A52F479-288C-4998-8336-B196CD5321A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {1A52F479-288C-4998-8336-B196CD5321A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {1A52F479-288C-4998-8336-B196CD5321A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {1A52F479-288C-4998-8336-B196CD5321A9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {D9B9151B-03A1-4B7D-A580-4A49C8522B4E} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/Voltaire/bin/Debug/net6.0/Voltaire.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/Voltaire", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /Voltaire/Migrations/20191006234916_MessageLimit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Voltaire.Migrations 5 | { 6 | public partial class MessageLimit : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.AddColumn( 11 | name: "MessagesSentThisMonth", 12 | table: "Guilds", 13 | nullable: false, 14 | defaultValue: 0); 15 | 16 | migrationBuilder.AddColumn( 17 | name: "TrackingMonth", 18 | table: "Guilds", 19 | nullable: false, 20 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); 21 | } 22 | 23 | protected override void Down(MigrationBuilder migrationBuilder) 24 | { 25 | migrationBuilder.DropColumn( 26 | name: "MessagesSentThisMonth", 27 | table: "Guilds"); 28 | 29 | migrationBuilder.DropColumn( 30 | name: "TrackingMonth", 31 | table: "Guilds"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Voltaire/Modules/InteractionsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Discord.Interactions; 3 | using System.Threading.Tasks; 4 | 5 | namespace Voltaire.Modules 6 | { 7 | public abstract class InteractionsBase : InteractionModuleBase 8 | { 9 | protected DataBase _database; 10 | protected Func Responder; 11 | protected Func PublicResponder; 12 | 13 | public InteractionsBase(DataBase database) 14 | { 15 | _database = database; 16 | Responder = (response, embed) => 17 | { 18 | if (embed != null) { 19 | return RespondAsync(response, new[] { embed }, ephemeral: true); 20 | } 21 | return RespondAsync(response, ephemeral: true); 22 | }; 23 | 24 | PublicResponder = (response, embed) => 25 | { 26 | if (embed != null) { 27 | return RespondAsync(response, new[] { embed }, ephemeral: false); 28 | } 29 | return RespondAsync(response, ephemeral: false); 30 | }; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Subscriptions/Cancel.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Voltaire.Controllers.Helpers; 3 | using Voltaire.Controllers.Messages; 4 | using Stripe; 5 | 6 | 7 | namespace Voltaire.Controllers.Subscriptions 8 | { 9 | class Cancel 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 12 | { 13 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 14 | if(EnsureActiveSubscription.Perform(guild, db)) 15 | { 16 | var service = new SubscriptionService(); 17 | var options = new SubscriptionCancelOptions { 18 | Prorate = false 19 | }; 20 | service.Cancel(guild.SubscriptionId, options); 21 | 22 | await Send.SendMessageToContext(context, "Your subscription has been canceled. Use `/pro` to re-upgrade at any time!"); 23 | } 24 | else 25 | { 26 | await Send.SendMessageToContext(context, "You do not currently have an active Voltaire Pro subscription. To create one, use the" + 27 | " `/pro` command."); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20180726032554_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Voltaire.Migrations 5 | { 6 | public partial class InitialCreate : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Guilds", 12 | columns: table => new 13 | { 14 | ID = table.Column(nullable: false) 15 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 16 | DiscordId = table.Column(nullable: true), 17 | AllowDirectMessage = table.Column(nullable: false) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_Guilds", x => x.ID); 22 | }); 23 | } 24 | 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.DropTable( 28 | name: "Guilds"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/UnBanIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Voltaire.Controllers.Helpers; 4 | using Voltaire.Controllers.Messages; 5 | 6 | namespace Voltaire.Controllers.Settings 7 | { 8 | class UnBanIdentifier 9 | { 10 | public static async Task PerformAsync(UnifiedContext context, string identifier, DataBase db) 11 | { 12 | if (!BanIdentifier.ValidIdentifier(identifier)) 13 | { 14 | await Send.SendErrorWithDeleteReaction(context, "Please use the 4 digit number following the identifier to unban users."); 15 | return; 16 | } 17 | 18 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 19 | 20 | var identifiers = guild.BannedIdentifiers.Where(x => x.Identifier == identifier); 21 | if (identifiers.Count() == 0) 22 | { 23 | await Send.SendErrorWithDeleteReaction(context, "That user is not currently banned."); 24 | return; 25 | } 26 | 27 | db.BannedIdentifiers.RemoveRange(identifiers.ToList()); 28 | 29 | await db.SaveChangesAsync(); 30 | await Send.SendMessageToContext(context, $"{identifier} is now unbanned"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Voltaire/Modules/InfoInteractions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Discord.Interactions; 4 | using Discord; 5 | 6 | namespace Voltaire.Modules 7 | { 8 | public class InfoInteractions : InteractionsBase 9 | { 10 | 11 | public InfoInteractions(DataBase database): base(database) {} 12 | 13 | 14 | [SlashCommand("volt-help", "get an overview of Voltaire's commands and functionality")] 15 | public async Task VoltHelp( 16 | [Summary("private", "show the help dialogue privately")] Boolean? ephemeral = null 17 | ) 18 | { 19 | var view = Views.Info.SlashHelp.Response(new InteractionBasedContext(Context, PublicResponder)); 20 | await RespondAsync(view.Item1, embed: view.Item2, ephemeral: ephemeral == true); 21 | } 22 | 23 | [SlashCommand("volt-faq", "display the bot's faq link")] 24 | public async Task Faq() 25 | { 26 | await RespondAsync("View the FAQ here: https://discordapp.com/channels/426894892262752256/581280324340940820/612849796025155585"); 27 | } 28 | 29 | [SlashCommand("volt-link", "display the bot's invite link")] 30 | public async Task Link() 31 | { 32 | await RespondAsync(""); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Voltaire/Utility/UnifiedContext.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.Interactions; 4 | using Discord.WebSocket; 5 | using System.Threading.Tasks; 6 | using System; 7 | 8 | 9 | namespace Voltaire 10 | { 11 | public abstract class UnifiedContext 12 | { 13 | 14 | public DiscordShardedClient Client { get; set; } 15 | public SocketGuild Guild { get; set; } 16 | public IMessageChannel Channel { get; set; } 17 | public IUser User { get; set; } 18 | } 19 | 20 | 21 | public class CommandBasedContext : UnifiedContext 22 | { 23 | 24 | public CommandBasedContext(ShardedCommandContext context) 25 | { 26 | Client = context.Client; 27 | Guild = context.Guild; 28 | Channel = context.Channel; 29 | User = context.User; 30 | Message = context.Message; 31 | } 32 | 33 | public IUserMessage Message { get; set; } 34 | 35 | } 36 | 37 | public class InteractionBasedContext: UnifiedContext 38 | { 39 | public InteractionBasedContext(ShardedInteractionContext context, Func responder) 40 | { 41 | Client = context.Client; 42 | Guild = context.Guild; 43 | Channel = context.Channel; 44 | User = context.User; 45 | Responder = responder; 46 | } 47 | 48 | public Func Responder { get; set; } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Voltaire/Voltaire.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/Voltaire/Voltaire.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/Voltaire/Voltaire.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /Voltaire/Views/Info/MultipleGuildSendResponse.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace Voltaire.Views.Info 10 | { 11 | public static class MultipleGuildSendResponse 12 | { 13 | 14 | public static Tuple Response(IEnumerable guilds, string message) 15 | { 16 | 17 | var embed = new EmbedBuilder 18 | { 19 | Author = new EmbedAuthorBuilder 20 | { 21 | Name = "send_server" 22 | }, 23 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 24 | Description = "It looks like you belong to multiple servers where Voltaire is installed. Please specify your server using the following command: `send_server (server_name) (channel_name) (message)`\n\n" + 25 | "**Servers:**", 26 | Color = new Color(111, 111, 111) 27 | }; 28 | 29 | guilds.Select(x => { 30 | embed.AddField(x.Name, $"ex: `!volt send_server \"{x.Name}\" {x.Channels.FirstOrDefault().Name} {message}`"); 31 | return true; 32 | }).ToList(); 33 | 34 | return new Tuple("", embed.Build()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Voltaire/Preconditions/Administrator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System.Threading.Tasks; 6 | using System.Linq; 7 | using Voltaire.Controllers.Helpers; 8 | using Discord; 9 | 10 | namespace Voltaire.Preconditions 11 | { 12 | class Administrator : PreconditionAttribute 13 | { 14 | public async override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 15 | { 16 | // possible to pass the context in? 17 | var db = services.GetService(); 18 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 19 | 20 | if (guild.AdminRole != null) 21 | { 22 | var role = context.Guild.Roles.FirstOrDefault(x => x.Id.ToString() == guild.AdminRole); 23 | if (role != null) 24 | { 25 | var p = (SocketRole)role; 26 | if(p.Members.Any(x => x.Id == context.User.Id)) 27 | { 28 | return PreconditionResult.FromSuccess(); 29 | } 30 | } 31 | } 32 | 33 | return await new RequireUserPermissionAttribute(GuildPermission.Administrator).CheckPermissionsAsync(context, command, services); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Voltaire/Modules/ReactionInteractions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Discord.Interactions; 3 | using System; 4 | 5 | 6 | namespace Voltaire.Modules 7 | { 8 | public class ReactionInteractions : InteractionsBase 9 | { 10 | public ReactionInteractions(DataBase database): base(database) {} 11 | 12 | // todo: make this a "message command" 13 | // https://discordnet.dev/guides/int_framework/intro.html#message-commands 14 | [SlashCommand("react", "add an anonymous reaction to a message")] 15 | public async Task React( 16 | [Summary("message-ID", "The discord ID of the message you'd like to react to")] string messageIdString, 17 | [Summary("emoji", "the emoji reaction you'd like to send")] string emoji 18 | ) 19 | { 20 | ulong messageId; 21 | try { 22 | messageId = UInt64.Parse(messageIdString); 23 | } catch { 24 | await RespondAsync("Problem parsing message Id.", ephemeral: true); 25 | return; 26 | } 27 | try { 28 | await Controllers.Reactions.React.PerformAsync(new InteractionBasedContext(Context, Responder), messageId, emoji, _database); 29 | } catch { 30 | await RespondAsync("Problem sending this reaction. If this message persists, please let us know in the support server (https://discord.gg/xyzMyJH)!", ephemeral: true); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Voltaire/Voltaire.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Always 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Helpers/JoinedGuild.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | using Microsoft.Extensions.Configuration; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | 8 | namespace Voltaire.Controllers.Helpers 9 | { 10 | public static class JoinedGuild 11 | { 12 | 13 | public static void Refresh(SocketGuild guild) 14 | { 15 | Task.Run(async () => { 16 | Console.WriteLine("downloading user list"); 17 | await guild.DownloadUsersAsync(); 18 | Console.WriteLine("user list downloaded"); 19 | }); 20 | } 21 | 22 | public static Func Joined(DataBase db) 23 | { 24 | Func convert = async delegate (SocketGuild guild) 25 | { 26 | IConfiguration configuration = LoadConfig.Instance.config; 27 | 28 | Refresh(guild); 29 | 30 | await FindOrCreateGuild.Perform(guild, db); 31 | 32 | var view = Views.Info.JoinedGuild.Response(); 33 | await guild.TextChannels.First().SendMessageAsync(text: view.Item1, embed: view.Item2); 34 | 35 | // AuthDiscordBotListApi DblApi = new AuthDiscordBotListApi(425833927517798420, token); 36 | // var me = await DblApi.GetMeAsync(); 37 | // await me.UpdateStatsAsync(db.Guilds.Count()); 38 | }; 39 | 40 | return convert; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Voltaire/Modules/Info.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using System.Threading.Tasks; 3 | using System; 4 | 5 | namespace Voltaire.Modules 6 | { 7 | public class Info : ModuleBase 8 | { 9 | [Command("help", RunMode = RunMode.Async)] 10 | [Summary("get command overview")] 11 | public async Task GetInfo() 12 | { 13 | var view = Views.Info.Help.Response(Context); 14 | await Context.Channel.SendMessageAsync(view.Item1, embed: view.Item2); 15 | } 16 | 17 | [Command("admin", RunMode = RunMode.Async)] 18 | [Summary("get admin command overview")] 19 | public async Task AdminInfo() 20 | { 21 | var view = Views.Info.Admin.Response(new CommandBasedContext(Context)); 22 | await Context.Channel.SendMessageAsync(view.Item1, embed: view.Item2); 23 | } 24 | 25 | [Command("link", RunMode = RunMode.Async)] 26 | [Summary("display invite link")] 27 | public async Task InviteLink() 28 | { 29 | await Context.Channel.SendMessageAsync(""); 30 | } 31 | 32 | [Command("faq", RunMode = RunMode.Async)] 33 | [Summary("display faq link")] 34 | public async Task Faq() 35 | { 36 | await Context.Channel.SendMessageAsync("View the FAQ here: https://discordapp.com/channels/426894892262752256/581280324340940820/612849796025155585"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20180726032554_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Voltaire; 8 | 9 | namespace Voltaire.Migrations 10 | { 11 | [DbContext(typeof(DataBase))] 12 | [Migration("20180726032554_InitialCreate")] 13 | partial class InitialCreate 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 21 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 22 | 23 | modelBuilder.Entity("Voltaire.Models.Guild", b => 24 | { 25 | b.Property("ID") 26 | .ValueGeneratedOnAdd() 27 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 28 | 29 | b.Property("AllowDirectMessage"); 30 | 31 | b.Property("DiscordId"); 32 | 33 | b.HasKey("ID"); 34 | 35 | b.ToTable("Guilds"); 36 | }); 37 | #pragma warning restore 612, 618 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/GenereteGuildUserIdentifierSeed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Voltaire.Controllers.Helpers; 5 | using Voltaire.Controllers.Messages; 6 | using Voltaire.Models; 7 | 8 | namespace Voltaire.Controllers.Settings 9 | { 10 | class GenerateGuildUserIdentifierSeed 11 | { 12 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 13 | { 14 | try 15 | { 16 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 17 | 18 | // toList to force enumeration before we shuffle identifier 19 | var bannedUsers = context.Guild.Users.Where(x => PrefixHelper.UserBlocked(x.Id, guild)).ToList(); 20 | 21 | guild.UserIdentifierSeed = new Random().Next(int.MinValue, int.MaxValue); 22 | 23 | var items = bannedUsers.Select(x => PrefixHelper.GetIdentifierString(x.Id, guild)).Select(x => new BannedIdentifier { Identifier = x }); 24 | 25 | db.RemoveRange(guild.BannedIdentifiers); 26 | 27 | items.Select((x) => { 28 | guild.BannedIdentifiers.Add(x); 29 | return true; 30 | }).ToList(); 31 | 32 | await db.SaveChangesAsync(); 33 | 34 | await Send.SendMessageToContext(context, "User identifiers have been randomized."); 35 | } 36 | catch (Exception ex) 37 | { 38 | Console.WriteLine("error rotating"); 39 | Console.WriteLine(ex.ToString()); 40 | } 41 | 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20180903043025_Added UserIdentfie.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Voltaire; 8 | 9 | namespace Voltaire.Migrations 10 | { 11 | [DbContext(typeof(DataBase))] 12 | [Migration("20180903043025_Added UserIdentfie")] 13 | partial class AddedUserIdentfie 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 21 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 22 | 23 | modelBuilder.Entity("Voltaire.Models.Guild", b => 24 | { 25 | b.Property("ID") 26 | .ValueGeneratedOnAdd() 27 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 28 | 29 | b.Property("AllowDirectMessage"); 30 | 31 | b.Property("DiscordId"); 32 | 33 | b.Property("UseUserIdentifiers"); 34 | 35 | b.HasKey("ID"); 36 | 37 | b.ToTable("Guilds"); 38 | }); 39 | #pragma warning restore 612, 618 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Reactions/React.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using System.Linq; 3 | using System; 4 | using System.Threading.Tasks; 5 | using Voltaire.Controllers.Messages; 6 | 7 | namespace Voltaire.Controllers.Reactions 8 | { 9 | class React 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, ulong messageId, string emoji, DataBase db) 12 | { 13 | var guildList = Send.GuildList(context); 14 | 15 | Discord.IMessage message; 16 | try { 17 | message = await context.Channel.GetMessageAsync(messageId); 18 | } catch { 19 | await Send.SendErrorWithDeleteReaction(context, "message not found"); 20 | return; 21 | } 22 | 23 | // test for simple emoji (😃) 24 | try { 25 | var d = new Discord.Emoji(emoji); 26 | await message.AddReactionAsync(d); 27 | await Send.SendSentEmoteIfCommand(context); 28 | return; 29 | } catch (Discord.Net.HttpException) {} 30 | 31 | // look for custom discord emotes 32 | var emote = guildList.SelectMany(x => x.Emotes).FirstOrDefault(x => $":{x.Name}:".IndexOf( 33 | emoji, StringComparison.OrdinalIgnoreCase) != -1); 34 | 35 | if (emote != null) { 36 | await message.AddReactionAsync(emote); 37 | await Send.SendSentEmoteIfCommand(context); 38 | } else { 39 | await Send.SendErrorWithDeleteReaction(context, "Emoji not found. To send a custom emote, use the emote's name."); 40 | } 41 | return; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Settings/BanIdentifier .cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Voltaire.Controllers.Helpers; 4 | using Voltaire.Controllers.Messages; 5 | using Voltaire.Models; 6 | 7 | namespace Voltaire.Controllers.Settings 8 | { 9 | class BanIdentifier 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, string identifier, DataBase db) 12 | { 13 | if (!ValidIdentifier(identifier)) 14 | { 15 | await Send.SendErrorWithDeleteReaction(context, "Please use the 4 digit number following the identifier to ban users."); 16 | return; 17 | } 18 | 19 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 20 | 21 | if(!EnsureActiveSubscription.Perform(guild,db)) 22 | { 23 | await Send.SendErrorWithDeleteReaction(context,"You need an active Voltaire Pro subscription to ban users. To get started, use `/pro`"); 24 | return; 25 | } 26 | 27 | if (guild.BannedIdentifiers.Any(x => x.Identifier == identifier)) 28 | { 29 | await Send.SendErrorWithDeleteReaction(context,"That identifier is already banned!"); 30 | return; 31 | } 32 | 33 | guild.BannedIdentifiers.Add(new BannedIdentifier { Identifier = identifier }); 34 | await db.SaveChangesAsync(); 35 | await Send.SendMessageToContext(context, $"{identifier} is now banned"); 36 | } 37 | 38 | public static bool ValidIdentifier(string identifier) 39 | { 40 | return identifier.Length == 4 && int.TryParse(identifier, out int n); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Voltaire/Preconditions/AdministratorInteraction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Discord.WebSocket; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Threading.Tasks; 5 | using System.Linq; 6 | using Voltaire.Controllers.Helpers; 7 | using Discord; 8 | using Discord.Interactions; 9 | 10 | namespace Voltaire.Preconditions 11 | { class AdministratorInteraction : Discord.Interactions.PreconditionAttribute 12 | { 13 | public async override Task CheckRequirementsAsync(IInteractionContext context, Discord.Interactions.ICommandInfo commandInfo, IServiceProvider services) 14 | { 15 | if (context.Guild == null) { 16 | return PreconditionResult.FromError("This command can only be executed from within server channels."); 17 | } 18 | 19 | // possible to pass the context in? 20 | var db = services.GetService(); 21 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 22 | 23 | if (guild.AdminRole != null) 24 | { 25 | var role = context.Guild.Roles.FirstOrDefault(x => x.Id.ToString() == guild.AdminRole); 26 | if (role != null) 27 | { 28 | var p = (SocketRole)role; 29 | if(p.Members.Any(x => x.Id == context.User.Id)) 30 | { 31 | return PreconditionResult.FromSuccess(); 32 | } 33 | } 34 | } 35 | Console.WriteLine("returning default"); 36 | return await new RequireUserPermissionAttribute(GuildPermission.Administrator).CheckRequirementsAsync(context, commandInfo, services); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Voltaire/Controllers/Subscriptions/Pro.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Voltaire.Controllers.Helpers; 3 | using Voltaire.Controllers.Messages; 4 | using Stripe; 5 | 6 | 7 | namespace Voltaire.Controllers.Subscriptions 8 | { 9 | class Pro 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, DataBase db) 12 | { 13 | var guild = await FindOrCreateGuild.Perform(context.Guild, db); 14 | if(EnsureActiveSubscription.Perform(guild, db)) 15 | { 16 | var service = new SubscriptionService(); 17 | var subscription = service.Get(guild.SubscriptionId); 18 | var amount = subscription.Plan.Amount; 19 | var date = subscription.CurrentPeriodEnd; 20 | 21 | var message = $"Your current subscription will renew {date.Value.ToLongDateString()}.\n" + 22 | $"To cancel your subscription, use the `/pro-cancel` command."; 23 | 24 | await Send.SendMessageToContext(context, message); 25 | } 26 | else 27 | { 28 | var size = context.Guild.MemberCount <= 200 ? "s" : "l"; 29 | var url = $"https://nminchow.github.io/VoltaireWeb/upgrade?serverId={context.Guild.Id.ToString()}&type={size}"; 30 | var view = Views.Info.Pro.Response(url, guild, db); 31 | try { 32 | await Send.SendMessageToContext(context, view.Item1, embed: view.Item2); 33 | } catch (Discord.Net.HttpException e) { 34 | await Send.SendMessageToContext(context, $"Use this URL to upgrade to Volatire Pro: {url}"); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20180908141038_GuildUserIdentifierSeed.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Voltaire; 8 | 9 | namespace Voltaire.Migrations 10 | { 11 | [DbContext(typeof(DataBase))] 12 | [Migration("20180908141038_GuildUserIdentifierSeed")] 13 | partial class GuildUserIdentifierSeed 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 21 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 22 | 23 | modelBuilder.Entity("Voltaire.Models.Guild", b => 24 | { 25 | b.Property("ID") 26 | .ValueGeneratedOnAdd() 27 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 28 | 29 | b.Property("AllowDirectMessage"); 30 | 31 | b.Property("DiscordId"); 32 | 33 | b.Property("UseUserIdentifiers"); 34 | 35 | b.Property("UserIdentifierSeed"); 36 | 37 | b.HasKey("ID"); 38 | 39 | b.ToTable("Guilds"); 40 | }); 41 | #pragma warning restore 612, 618 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20190220044024_BannedUsers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Voltaire.Migrations 5 | { 6 | public partial class BannedUsers : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "BannedIdentifiers", 12 | columns: table => new 13 | { 14 | ID = table.Column(nullable: false) 15 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 16 | Identifier = table.Column(nullable: true), 17 | GuildID = table.Column(nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_BannedIdentifiers", x => x.ID); 22 | table.ForeignKey( 23 | name: "FK_BannedIdentifiers_Guilds_GuildID", 24 | column: x => x.GuildID, 25 | principalTable: "Guilds", 26 | principalColumn: "ID", 27 | onDelete: ReferentialAction.Restrict); 28 | }); 29 | 30 | migrationBuilder.CreateIndex( 31 | name: "IX_BannedIdentifiers_GuildID", 32 | table: "BannedIdentifiers", 33 | column: "GuildID"); 34 | } 35 | 36 | protected override void Down(MigrationBuilder migrationBuilder) 37 | { 38 | migrationBuilder.DropTable( 39 | name: "BannedIdentifiers"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20181005012009_AddAllowedRole.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Voltaire; 8 | 9 | namespace Voltaire.Migrations 10 | { 11 | [DbContext(typeof(DataBase))] 12 | [Migration("20181005012009_AddAllowedRole")] 13 | partial class AddAllowedRole 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 21 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 22 | 23 | modelBuilder.Entity("Voltaire.Models.Guild", b => 24 | { 25 | b.Property("ID") 26 | .ValueGeneratedOnAdd() 27 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 28 | 29 | b.Property("AllowDirectMessage"); 30 | 31 | b.Property("AllowedRole"); 32 | 33 | b.Property("DiscordId"); 34 | 35 | b.Property("UseUserIdentifiers"); 36 | 37 | b.Property("UserIdentifierSeed"); 38 | 39 | b.HasKey("ID"); 40 | 41 | b.ToTable("Guilds"); 42 | }); 43 | #pragma warning restore 612, 618 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voltaire 2 | 3 | **Voltaire** is a discord bot that allows you to send messages anonymously. It supports sending messages to server channels as well as sending DMs to members of the server it has been added to. Voltaire has several admin settings (which can be viewed with `/volt-admin help`) to allow admins to best fit the bot to their use case. When Voltaire is added to your server, type `/volt-help` to get a list of commands. 4 | 5 | [Add Voltaire to Your Server](https://discordapp.com/oauth2/authorize?client_id=425833927517798420&permissions=2147998784&scope=bot%20applications.commands) 6 | 7 | [Official Voltaire Discord](https://discord.gg/xyzMyJH) 8 | 9 | ## Built With 10 | 11 | * [Discord.net](https://github.com/RogueException/Discord.Net) - Bot Framework 12 | 13 | ## Contributing 14 | 15 | Pull requests are welcome! 16 | 17 | #### Development setup 18 | 19 | To get running locally: 20 | 1. Create a [discord bot user](https://discordapp.com/developers/applications/) 21 | 2. Set up a sql database 22 | 3. Create a appsettings.json file within the project's "Voltaire" directory (see example below) 23 | 4. Run migrations 24 | 5. Be excellent to eachother 25 | 26 | ``` 27 | // appsettings.json 28 | { 29 | "discordAppToken": "F5OCongcjYOMXmEgrTmGDFy1Te5CUZy5ignm2DLoUUwJ1QsbfqEeOpyWBhe", 30 | // the emoji the bot will use when a message is sent 31 | "sent_emoji": "<:message_sent:491776018970050570>", 32 | // a 256 bit key used to generate response codes and usernames 33 | "encryptionKey": "PSVJQRk9QTEpNVU1DWUZCRVFGV1VVT0ZOV1RRU1NaWQ=", 34 | "ConnectionStrings": { 35 | "sql": "Server=(localdb)\\mssqllocaldb;Database=Voltaire;Trusted_Connection=True;" 36 | } 37 | } 38 | ``` 39 | 40 | ## License 41 | 42 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 43 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Helpers/EnsureActiveSubscription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Voltaire.Models; 5 | using Stripe; 6 | 7 | namespace Voltaire.Controllers.Helpers 8 | { 9 | class EnsureActiveSubscription 10 | { 11 | public static bool Perform(Guild guild, DataBase db) 12 | { 13 | if (guild.DiscordId == "426894892262752256") 14 | { 15 | return true; 16 | } 17 | 18 | if (guild.SubscriptionId != null) 19 | { 20 | var service = new SubscriptionService(); 21 | try 22 | { 23 | var subscription = service.Get(guild.SubscriptionId); 24 | if (subscription != null && subscription.CanceledAt == null) 25 | { 26 | return true; 27 | } 28 | } catch (Exception e) 29 | { 30 | Console.WriteLine(e.ToString()); 31 | } 32 | guild.SubscriptionId = null; 33 | db.SaveChanges(); 34 | } 35 | return QueryForSubscription(guild, db); 36 | } 37 | 38 | private static bool QueryForSubscription(Guild guild, DataBase db) 39 | { 40 | var service = new SubscriptionService(); 41 | var response = service.List(new SubscriptionListOptions 42 | { 43 | Limit = 100 44 | }); 45 | var result = response.FirstOrDefault(x => x.Metadata.GetValueOrDefault("serverId") == guild.DiscordId) ?? null; 46 | if (result == null) 47 | { 48 | return false; 49 | } 50 | guild.SubscriptionId = result.Id; 51 | db.SaveChanges(); 52 | return true; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Voltaire/Modules/PromptInteractions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Discord.Interactions; 3 | using System.Linq; 4 | using Discord.WebSocket; 5 | 6 | namespace Voltaire.Modules 7 | { 8 | public class PromptInteractions : InteractionsBase 9 | { 10 | 11 | public PromptInteractions(DataBase database): base(database) {} 12 | 13 | [ComponentInteraction("prompt-message:*,*")] 14 | public async Task PromptUserForInput(string channelId, string repliable) { 15 | 16 | var channel = Context.Guild.TextChannels.Where(x => x.Id.ToString() == channelId).FirstOrDefault(); 17 | 18 | if (channel == null) { 19 | await RespondAsync("It looks like this prompt is no longer working. Please contact your admin!", ephemeral: true); 20 | return; 21 | } 22 | 23 | var prompt = $"Anonymous message to #{channel}"; 24 | 25 | var truncatedPrompt = prompt.Length > 45 ? $"{prompt.Substring(0, 42)}..." : prompt; 26 | 27 | await Context.Interaction.RespondWithModalAsync($"send-message:{channelId},{repliable}", null, modifyModal: builder => { 28 | builder.Title = truncatedPrompt; 29 | }); 30 | 31 | } 32 | 33 | [ComponentInteraction("prompt-reply:*:*")] 34 | public async Task PromptUserForReploy(string replyHash, string replyable) { 35 | 36 | var originalInteraction = Context.Interaction as SocketMessageComponent; 37 | var author = originalInteraction.Message.Embeds.First().Author.ToString(); 38 | 39 | if (author.EndsWith(" replied")) { 40 | author = author.Remove(author.Length - 8); 41 | } 42 | 43 | var prompt = $"Anonymous reply to {author}"; 44 | 45 | if (string.IsNullOrEmpty(author)) { 46 | prompt = "Send Anonymous Reply"; 47 | } 48 | 49 | await Context.Interaction.RespondWithModalAsync($"send-reply:{replyHash}:::{replyable}", null, modifyModal: builder => { 50 | builder.Title = prompt; 51 | }); 52 | 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/SlashAdmin.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | 4 | namespace Voltaire.Views.Info 5 | { 6 | public static class SlashAdmin 7 | { 8 | 9 | public static Tuple Response(UnifiedContext context) 10 | { 11 | 12 | var embed = new EmbedBuilder 13 | { 14 | Author = new EmbedAuthorBuilder 15 | { 16 | Name = "Admin Guide" 17 | }, 18 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 19 | Description = "These commands are only callable by admin users.\n\n" + 20 | "Commands should be sent to the bot in a server channel.\n\n" + 21 | "**Guild Channel Commands:**", 22 | Color = new Color(111, 111, 111) 23 | }; 24 | 25 | embed.AddField("/volt-admin settings", "Configure Voltaire's general settings, including DMs, identifiers, the use of embeds, and permitted role."); 26 | 27 | embed.AddField("/volt-admin new-identifiers ", "rotate user identifiers"); 28 | 29 | embed.AddField("/pro", "Upgrade and monitor your Pro subscription."); 30 | 31 | embed.AddField("/volt-admin ban", "Blacklists a user from the bot by user ID. This is the 4 digit number after their identifier when the \"use-identifiers\" setting is enabled."); 32 | 33 | embed.AddField("/volt-admin unban", "Unban a user from the bot by user ID."); 34 | 35 | embed.AddField("/volt-admin list-bans list_bans", "List currently banned user IDs."); 36 | 37 | embed.AddField("/volt-admin clear-bans", "Clear the currently banned user list."); 38 | 39 | embed.AddField("/volt-admin refresh", "Refresh the user list for this server. This command isn't regularly necessary, but may be helpful when the bot first joins your server."); 40 | 41 | embed.AddField("/volt-admin role", "Set a role which can use admin commands and ban users on this server. (True server admins can always use admin commands as well)"); 42 | 43 | embed.AddField("/volt-admin help", "(callable from anywhere) Display this help dialogue."); 44 | 45 | return new Tuple("", embed.Build()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Messages/SendReply.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Rijndael256; 5 | using Voltaire.Controllers.Helpers; 6 | 7 | namespace Voltaire.Controllers.Messages 8 | { 9 | class SendReply 10 | { 11 | public static async Task PerformAsync(UnifiedContext context, string replyKey, string message, bool replyable, DataBase db) 12 | { 13 | var candidateGuilds = Send.GuildList(context); 14 | 15 | var key = LoadConfig.Instance.config["encryptionKey"]; 16 | var candidateId = Rijndael.Decrypt(replyKey, key, KeySize.Aes256); 17 | 18 | // TODO: potentially want to bake guilds into reply codes so we can ensure that the the replier isn't banned on the server where the original 19 | // message was sent 20 | var users = SendDirectMessage.ToUserList(candidateGuilds).Where(x => x.Id.ToString() == candidateId); 21 | if(users.Count() == 0) 22 | { 23 | await Send.SendErrorWithDeleteReaction(context, "Something is wrong with that reply code. It is possible the sender has left your server."); 24 | return; 25 | } 26 | 27 | var allowedGuild = users.ToList().Select(async x => await FindOrCreateGuild.Perform(x.Guild, db)).FirstOrDefault(x => !PrefixHelper.UserBlocked(context.User.Id, x.Result)); 28 | 29 | if (allowedGuild == null) 30 | { 31 | await Send.SendErrorWithDeleteReaction(context, "It appears that you have been banned from using Voltaire on the targeted server. If you think this is an error, contact one of your admins."); 32 | return; 33 | } 34 | 35 | var prefix = $"{PrefixHelper.ComputePrefix(context, allowedGuild.Result, "someone")} replied"; 36 | 37 | // all 'users' here are technically the same user, so just take the first 38 | var channel = await users.First().CreateDMChannelAsync(); 39 | var messageFunction = Send.SendMessageToChannel(channel, replyable, context); 40 | var sentMessage = await messageFunction(prefix, message); 41 | await Send.AddReactionToMessage(sentMessage); 42 | await Send.SendSentEmoteIfCommand(context); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/SlashHelp.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using System; 4 | 5 | namespace Voltaire.Views.Info 6 | { 7 | public static class SlashHelp 8 | { 9 | 10 | public static Tuple Response(UnifiedContext context) 11 | { 12 | 13 | var embed = new EmbedBuilder 14 | { 15 | Author = new EmbedAuthorBuilder 16 | { 17 | Name = "Guide" 18 | }, 19 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 20 | Description = "Voltaire allows you to send messages to a discord server anonymously.\n\n" + 21 | "Support Server: https://discord.gg/xyzMyJH \n\n" + 22 | "**Direct Message Commands:**", 23 | Color = new Color(111, 111, 111), 24 | }; 25 | 26 | embed.AddField("/volt", "Sends an anonymous message to the current channel."); 27 | embed.AddField("/send-dm", "Sends an anonymous message to the specified user."); 28 | embed.AddField("/send", "Sends an anonymous message to the specified channel in the current server."); 29 | embed.AddField("/send-reply", "Sends a reply to the specified message in the current server."); 30 | 31 | embed.AddField("/react", 32 | "Send a reaction to a message. [Enable dev settings to get message IDs](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID). " + 33 | "The `emote/emoji` param can be either a unicode emoji, or the name of a custom emote. This must be used in the same channel as the original message."); 34 | 35 | embed.AddField("/volt-link", "Display the [bot's invite link](https://discordapp.com/oauth2/authorize?client_id=425833927517798420&permissions=2147998784&scope=bot%20applications.commands)."); 36 | embed.AddField("/volt-faq", "Display the [FAQ link](https://discordapp.com/channels/426894892262752256/581280324340940820/612849796025155585)."); 37 | embed.AddField("/volt-admin help", "Get a list of admin commands, including details on Voltaire Pro."); 38 | embed.AddField("/volt-help", "(callable from anywhere) Display this help dialogue."); 39 | 40 | return new Tuple("", embed.Build()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20190220044024_BannedUsers.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Voltaire; 9 | 10 | namespace Voltaire.Migrations 11 | { 12 | [DbContext(typeof(DataBase))] 13 | [Migration("20190220044024_BannedUsers")] 14 | partial class BannedUsers 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 25 | { 26 | b.Property("ID") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("GuildID"); 31 | 32 | b.Property("Identifier"); 33 | 34 | b.HasKey("ID"); 35 | 36 | b.HasIndex("GuildID"); 37 | 38 | b.ToTable("BannedIdentifiers"); 39 | }); 40 | 41 | modelBuilder.Entity("Voltaire.Models.Guild", b => 42 | { 43 | b.Property("ID") 44 | .ValueGeneratedOnAdd() 45 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 46 | 47 | b.Property("AllowDirectMessage"); 48 | 49 | b.Property("AllowedRole"); 50 | 51 | b.Property("DiscordId"); 52 | 53 | b.Property("UseUserIdentifiers"); 54 | 55 | b.Property("UserIdentifierSeed"); 56 | 57 | b.HasKey("ID"); 58 | 59 | b.ToTable("Guilds"); 60 | }); 61 | 62 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 63 | { 64 | b.HasOne("Voltaire.Models.Guild") 65 | .WithMany("BannedIdentifiers") 66 | .HasForeignKey("GuildID"); 67 | }); 68 | #pragma warning restore 612, 618 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20190312035652_SubscriptionId.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Voltaire; 9 | 10 | namespace Voltaire.Migrations 11 | { 12 | [DbContext(typeof(DataBase))] 13 | [Migration("20190312035652_SubscriptionId")] 14 | partial class SubscriptionId 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 25 | { 26 | b.Property("ID") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("GuildID"); 31 | 32 | b.Property("Identifier"); 33 | 34 | b.HasKey("ID"); 35 | 36 | b.HasIndex("GuildID"); 37 | 38 | b.ToTable("BannedIdentifiers"); 39 | }); 40 | 41 | modelBuilder.Entity("Voltaire.Models.Guild", b => 42 | { 43 | b.Property("ID") 44 | .ValueGeneratedOnAdd() 45 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 46 | 47 | b.Property("AllowDirectMessage"); 48 | 49 | b.Property("AllowedRole"); 50 | 51 | b.Property("DiscordId"); 52 | 53 | b.Property("SubscriptionId"); 54 | 55 | b.Property("UseUserIdentifiers"); 56 | 57 | b.Property("UserIdentifierSeed"); 58 | 59 | b.HasKey("ID"); 60 | 61 | b.ToTable("Guilds"); 62 | }); 63 | 64 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 65 | { 66 | b.HasOne("Voltaire.Models.Guild") 67 | .WithMany("BannedIdentifiers") 68 | .HasForeignKey("GuildID"); 69 | }); 70 | #pragma warning restore 612, 618 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20190319045013_AddAdminRole.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Voltaire; 9 | 10 | namespace Voltaire.Migrations 11 | { 12 | [DbContext(typeof(DataBase))] 13 | [Migration("20190319045013_AddAdminRole")] 14 | partial class AddAdminRole 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 25 | { 26 | b.Property("ID") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("GuildID"); 31 | 32 | b.Property("Identifier"); 33 | 34 | b.HasKey("ID"); 35 | 36 | b.HasIndex("GuildID"); 37 | 38 | b.ToTable("BannedIdentifiers"); 39 | }); 40 | 41 | modelBuilder.Entity("Voltaire.Models.Guild", b => 42 | { 43 | b.Property("ID") 44 | .ValueGeneratedOnAdd() 45 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 46 | 47 | b.Property("AdminRole"); 48 | 49 | b.Property("AllowDirectMessage"); 50 | 51 | b.Property("AllowedRole"); 52 | 53 | b.Property("DiscordId"); 54 | 55 | b.Property("SubscriptionId"); 56 | 57 | b.Property("UseUserIdentifiers"); 58 | 59 | b.Property("UserIdentifierSeed"); 60 | 61 | b.HasKey("ID"); 62 | 63 | b.ToTable("Guilds"); 64 | }); 65 | 66 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 67 | { 68 | b.HasOne("Voltaire.Models.Guild") 69 | .WithMany("BannedIdentifiers") 70 | .HasForeignKey("GuildID"); 71 | }); 72 | #pragma warning restore 612, 618 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Messages/PrefixHelper.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using System; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using Voltaire.Models; 7 | 8 | namespace Voltaire.Controllers.Messages 9 | { 10 | class PrefixHelper 11 | { 12 | 13 | public static string ComputePrefix(UnifiedContext context, Guild guild, string defaultValue = "") 14 | { 15 | if (!guild.UseUserIdentifiers) 16 | { 17 | return defaultValue; 18 | } 19 | return Generate(GetIdentifierInteger(context.User.Id, guild)); 20 | } 21 | 22 | public static bool UserBlocked(ulong userId, Guild guild) 23 | { 24 | var identifier = IdentifierString(GetIdentifierInteger(userId, guild)); 25 | return guild.BannedIdentifiers.Any(x => x.Identifier == identifier); 26 | } 27 | 28 | public static string GetIdentifierString(ulong userId, Guild guild) 29 | { 30 | return IdentifierString(GetIdentifierInteger(userId, guild)); 31 | } 32 | 33 | private static string IdentifierString(int identifier) 34 | { 35 | return Math.Abs(identifier).ToString("0000").Substring(0, 4); 36 | } 37 | 38 | private static string Generate(int identifierInt) 39 | { 40 | //Console.WriteLine($"{resultString} {integer} {offset}"); 41 | 42 | //var generator = new Generator(seed: identifierInt) 43 | //{ 44 | // Casing = Casing.PascalCase, 45 | // Parts = new WordBank[] { WordBank.Titles, WordBank.Nouns } 46 | //}; 47 | return $"User#{IdentifierString(identifierInt)}"; 48 | } 49 | 50 | public static int GetIdentifierInteger(ulong userId, Guild guild) 51 | { 52 | var seed = guild.UserIdentifierSeed; 53 | 54 | string password = LoadConfig.Instance.config["encryptionKey"]; 55 | 56 | //var offset = (ulong)(new Random().Next(0, 10000)); 57 | 58 | var id = (userId + (ulong)seed).ToString(); 59 | 60 | var bytes = GetHash(id, password); 61 | 62 | var resultString = BitConverter.ToString(bytes); 63 | 64 | var integer = BitConverter.ToInt32(bytes, bytes.Length - 4); 65 | return integer; 66 | } 67 | 68 | public static Byte[] GetHash(String text, String key) 69 | { 70 | ASCIIEncoding encoding = new ASCIIEncoding(); 71 | Byte[] textBytes = encoding.GetBytes(text); 72 | Byte[] keyBytes = encoding.GetBytes(key); 73 | 74 | Byte[] hashBytes; 75 | 76 | using (HMACSHA256 hash = new HMACSHA256(keyBytes)) 77 | hashBytes = hash.ComputeHash(textBytes); 78 | 79 | return hashBytes; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Voltaire/Migrations/DataBaseModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Voltaire; 8 | 9 | namespace Voltaire.Migrations 10 | { 11 | [DbContext(typeof(DataBase))] 12 | partial class DataBaseModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 23 | { 24 | b.Property("ID") 25 | .ValueGeneratedOnAdd() 26 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 27 | 28 | b.Property("GuildID"); 29 | 30 | b.Property("Identifier"); 31 | 32 | b.HasKey("ID"); 33 | 34 | b.HasIndex("GuildID"); 35 | 36 | b.ToTable("BannedIdentifiers"); 37 | }); 38 | 39 | modelBuilder.Entity("Voltaire.Models.Guild", b => 40 | { 41 | b.Property("ID") 42 | .ValueGeneratedOnAdd() 43 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 44 | 45 | b.Property("AdminRole"); 46 | 47 | b.Property("AllowDirectMessage"); 48 | 49 | b.Property("AllowedRole"); 50 | 51 | b.Property("DiscordId"); 52 | 53 | b.Property("MessagesSentThisMonth"); 54 | 55 | b.Property("SubscriptionId"); 56 | 57 | b.Property("TrackingMonth"); 58 | 59 | b.Property("UseEmbed"); 60 | 61 | b.Property("UseUserIdentifiers"); 62 | 63 | b.Property("UserIdentifierSeed"); 64 | 65 | b.HasKey("ID"); 66 | 67 | b.ToTable("Guilds"); 68 | }); 69 | 70 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 71 | { 72 | b.HasOne("Voltaire.Models.Guild") 73 | .WithMany("BannedIdentifiers") 74 | .HasForeignKey("GuildID"); 75 | }); 76 | #pragma warning restore 612, 618 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20191006234916_MessageLimit.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Voltaire; 9 | 10 | namespace Voltaire.Migrations 11 | { 12 | [DbContext(typeof(DataBase))] 13 | [Migration("20191006234916_MessageLimit")] 14 | partial class MessageLimit 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 25 | { 26 | b.Property("ID") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("GuildID"); 31 | 32 | b.Property("Identifier"); 33 | 34 | b.HasKey("ID"); 35 | 36 | b.HasIndex("GuildID"); 37 | 38 | b.ToTable("BannedIdentifiers"); 39 | }); 40 | 41 | modelBuilder.Entity("Voltaire.Models.Guild", b => 42 | { 43 | b.Property("ID") 44 | .ValueGeneratedOnAdd() 45 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 46 | 47 | b.Property("AdminRole"); 48 | 49 | b.Property("AllowDirectMessage"); 50 | 51 | b.Property("AllowedRole"); 52 | 53 | b.Property("DiscordId"); 54 | 55 | b.Property("MessagesSentThisMonth"); 56 | 57 | b.Property("SubscriptionId"); 58 | 59 | b.Property("TrackingMonth"); 60 | 61 | b.Property("UseUserIdentifiers"); 62 | 63 | b.Property("UserIdentifierSeed"); 64 | 65 | b.HasKey("ID"); 66 | 67 | b.ToTable("Guilds"); 68 | }); 69 | 70 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 71 | { 72 | b.HasOne("Voltaire.Models.Guild") 73 | .WithMany("BannedIdentifiers") 74 | .HasForeignKey("GuildID"); 75 | }); 76 | #pragma warning restore 612, 618 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Voltaire/Migrations/20201025184258_AddEmbedOption.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Voltaire; 9 | 10 | namespace Voltaire.Migrations 11 | { 12 | [DbContext(typeof(DataBase))] 13 | [Migration("20201025184258_AddEmbedOption")] 14 | partial class AddEmbedOption 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 25 | { 26 | b.Property("ID") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("GuildID"); 31 | 32 | b.Property("Identifier"); 33 | 34 | b.HasKey("ID"); 35 | 36 | b.HasIndex("GuildID"); 37 | 38 | b.ToTable("BannedIdentifiers"); 39 | }); 40 | 41 | modelBuilder.Entity("Voltaire.Models.Guild", b => 42 | { 43 | b.Property("ID") 44 | .ValueGeneratedOnAdd() 45 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 46 | 47 | b.Property("AdminRole"); 48 | 49 | b.Property("AllowDirectMessage"); 50 | 51 | b.Property("AllowedRole"); 52 | 53 | b.Property("DiscordId"); 54 | 55 | b.Property("MessagesSentThisMonth"); 56 | 57 | b.Property("SubscriptionId"); 58 | 59 | b.Property("TrackingMonth"); 60 | 61 | b.Property("UseEmbed"); 62 | 63 | b.Property("UseUserIdentifiers"); 64 | 65 | b.Property("UserIdentifierSeed"); 66 | 67 | b.HasKey("ID"); 68 | 69 | b.ToTable("Guilds"); 70 | }); 71 | 72 | modelBuilder.Entity("Voltaire.Models.BannedIdentifier", b => 73 | { 74 | b.HasOne("Voltaire.Models.Guild") 75 | .WithMany("BannedIdentifiers") 76 | .HasForeignKey("GuildID"); 77 | }); 78 | #pragma warning restore 612, 618 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/UpgradeNotification.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | using Voltaire.Models; 4 | 5 | namespace Voltaire.Views.Info 6 | { 7 | public static class UpgradeNotification 8 | { 9 | 10 | public static Tuple Response() 11 | { 12 | 13 | var embed = new EmbedBuilder 14 | { 15 | Author = new EmbedAuthorBuilder 16 | { 17 | Name = "Legacy Command Detected" 18 | }, 19 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 20 | Description = "Due to changes in the discord api, commands sent directly to Voltiare will soon quit functioning. " + 21 | "But fear not! New versions of all commands have been implemented via slash " + 22 | "commands. To get more info about slash commands, use the `/volt-help` command in a DM with the bot or in your target server.\n\n" + 23 | "If slash commands aren't appearing in your server, one of your server admins will need to reinvite the bot with this link:\n"+ 24 | "https://discordapp.com/oauth2/authorize?client_id=425833927517798420&permissions=2147998784&scope=bot%20applications.commands \n\n" + 25 | "If you are haveing any issues or would like more info, hop into our " + 26 | "Support Server: https://discord.gg/xyzMyJH", 27 | Color = new Color(111, 111, 111) 28 | }; 29 | 30 | return new Tuple("", embed.Build()); 31 | } 32 | } 33 | 34 | public static class UpgradeNotificationWithSendInfo 35 | { 36 | 37 | public static Tuple Response() 38 | { 39 | 40 | var embed = new EmbedBuilder 41 | { 42 | Author = new EmbedAuthorBuilder 43 | { 44 | Name = "Legacy Command Detected" 45 | }, 46 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 47 | Description = "Due to changes in the discord api, commands sent directly to Voltiare will soon quit functioning. \n\n" + 48 | "To send an anonymous message to a channel, the `/volt` command should be used in the server where that channel resides. " + 49 | "The command accepts a `channel` parameter, which will allow you to select a channel, or a `channel-name` parameter which will allow you to " + 50 | "specify a channel by name or id.\n\n" + 51 | "To get more info about slash commands, use the `/volt-help` command in a DM with the bot or in your target server.\n\n" + 52 | "If slash commands aren't appearing in your server, one of your server admins will need to reinvite the bot with this link:\n"+ 53 | "https://discordapp.com/oauth2/authorize?client_id=425833927517798420&permissions=2147998784&scope=bot%20applications.commands \n\n" + 54 | "If you are haveing any issues or would like more info, hop into our " + 55 | "Support Server: https://discord.gg/xyzMyJH", 56 | Color = new Color(111, 111, 111) 57 | }; 58 | 59 | return new Tuple("", embed.Build()); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Voltaire/Views/Info/Admin.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace Voltaire.Views.Info 9 | { 10 | public static class Admin 11 | { 12 | 13 | public static Tuple Response(UnifiedContext context) 14 | { 15 | 16 | var embed = new EmbedBuilder 17 | { 18 | Author = new EmbedAuthorBuilder 19 | { 20 | Name = "Admin Guide" 21 | }, 22 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 23 | Description = "These commands are only callable by admin users.\n\n" + 24 | "Commands should be sent to the bot in a server channel.\n\n" + 25 | "**Guild Channel Commands:**", 26 | Color = new Color(111, 111, 111) 27 | }; 28 | 29 | embed.AddField("!volt allow_dm {true|false}", "Allow or disallow users to send direct messages to other members of the server." + 30 | $"\nex: `!volt allow_dm false`"); 31 | 32 | embed.AddField("!volt embeds {true|false}", "Make all messages sent via the bot appear as embeds on this server." + 33 | $"\nex: `!volt embeds true`"); 34 | 35 | embed.AddField("!volt permitted_role \"{role name}\"", "Only allow users with the supplied role to send server messages and direct messages on the server." + 36 | "Note that all users can still send replies. To clear, set the permitted role to @everyone." + 37 | $"\nex: `!volt permitted_role \"speakers of truth\"`"); 38 | 39 | embed.AddField("!volt user_identifiers {true|false}", "Enable or disable the use of a unique (yet annonymous) identifier for users when they send messages." + 40 | $"\nex: `!volt user_identifiers false`"); 41 | 42 | embed.AddField("!volt new_identifiers", "Generate new random identifiers for users. (Note: Banned users will still be banned.)" + 43 | $"\nex: `!volt new_identifiers`"); 44 | 45 | embed.AddField("!volt pro", "Upgrade and monitor your Pro subscription." + 46 | $"\nex: `!volt pro`"); 47 | 48 | embed.AddField("!volt ban {user id}", "Blacklists a user from the bot by user ID. This is the 4 digit number after their identifier when the \"user_identifiers\" setting is enabled." + 49 | $"\nex: `!volt ban 4321`"); 50 | 51 | embed.AddField("!volt list_bans", "List currently banned user IDs." + 52 | $"\nex: `!volt list_bans`"); 53 | 54 | embed.AddField("!volt clear_bans", "Clear the currently banned user list." + 55 | $"\nex: `!volt clear_bans`"); 56 | 57 | embed.AddField("!volt admin_role \"{role name}\"", "Set a role which can use admin commands and ban users on this server. (True server admins can always use commands as well)" + 58 | $"\nex: `!volt admin_role \"mods of truth\"`"); 59 | 60 | embed.AddField("!volt refresh ", "Refresh the user list for this server. This command isn't regularly necessary, but may be helpful when the bot first joins your server." + 61 | $"\nex: `!volt refresh`"); 62 | 63 | embed.AddField("!volt admin", "(callable from anywhere) Display this help dialogue."); 64 | 65 | return new Tuple("", embed.Build()); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Voltaire/Modules/Messages.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using System.Threading.Tasks; 3 | using Voltaire.Controllers.Messages; 4 | 5 | namespace Voltaire.Modules 6 | { 7 | public class Messages : ModuleBase 8 | { 9 | 10 | private DataBase _database; 11 | 12 | public Messages(DataBase database) 13 | { 14 | _database = database; 15 | } 16 | 17 | [Command("send", RunMode = RunMode.Async)] 18 | [Summary("error message for malformed send")] 19 | public async Task SendError(string _one) 20 | { 21 | await Controllers.Messages.Send.SendErrorWithDeleteReaction(new CommandBasedContext(Context), "Please specify your channel name, ex: `send some-channel hello`"); 22 | } 23 | 24 | [Command("send", RunMode = RunMode.Async)] 25 | public async Task Send(string channelName, [Remainder] string message) 26 | { 27 | await Controllers.Messages.Send.PerformAsync(new CommandBasedContext(Context), channelName, message, false, _database); 28 | } 29 | 30 | [Command("send+r", RunMode = RunMode.Async)] 31 | public async Task SendRepliable(string channelName, [Remainder] string message) 32 | { 33 | await Controllers.Messages.Send.PerformAsync(new CommandBasedContext(Context), channelName, message, true, _database); 34 | } 35 | 36 | [Command("send_server", RunMode = RunMode.Async)] 37 | public async Task SendServer(string guildName, string channelName, [Remainder] string message) 38 | { 39 | await SendToGuild.PerformAsync(new CommandBasedContext(Context), guildName, channelName, message, false, _database); 40 | } 41 | 42 | [Command("send_server+r", RunMode = RunMode.Async)] 43 | public async Task SendServerRepliable(string guildName, string channelName, [Remainder] string message) 44 | { 45 | await SendToGuild.PerformAsync(new CommandBasedContext(Context), guildName, channelName, message, true, _database); 46 | } 47 | 48 | [Command("send_dm", RunMode = RunMode.Async)] 49 | public async Task SendDirectMessage(string userName, [Remainder] string message) 50 | { 51 | await Controllers.Messages.SendDirectMessage.PerformAsync(new CommandBasedContext(Context), userName, message, false, _database); 52 | } 53 | 54 | [Command("send_dm+r", RunMode = RunMode.Async)] 55 | public async Task SendDirectMessageRepliable(string userName, [Remainder] string message) 56 | { 57 | await Controllers.Messages.SendDirectMessage.PerformAsync(new CommandBasedContext(Context), userName, message, true, _database); 58 | } 59 | 60 | [Command("send_reply", RunMode = RunMode.Async)] 61 | public async Task SendReplyError(string key) 62 | { 63 | await Context.Channel.SendMessageAsync("Please specify a message, ex: `send_reply iMIb62udZ7R/KCfhn634+AHvrrQ Don't make a girl a promise you know you can't keep.`"); 64 | } 65 | 66 | [Command("send_reply", RunMode = RunMode.Async)] 67 | public async Task SendReply(string key, [Remainder] string message) 68 | { 69 | await Controllers.Messages.SendReply.PerformAsync(new CommandBasedContext(Context), key, message, false,_database); 70 | } 71 | 72 | [Command("send_reply+r", RunMode = RunMode.Async)] 73 | public async Task SendReplyRepliableError(string key) 74 | { 75 | await Context.Channel.SendMessageAsync("Please specify a message, ex: `send_reply+r iMIb62udZ7R/KCfhn634+AHvrrQ Don't make a girl a promise you know you can't keep.`"); 76 | } 77 | 78 | [Command("send_reply+r", RunMode = RunMode.Async)] 79 | public async Task SendReplyRepliable(string key, [Remainder] string message) 80 | { 81 | await Controllers.Messages.SendReply.PerformAsync(new CommandBasedContext(Context), key, message, true, _database); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Voltaire/Modules/MessageInteractions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Discord.Interactions; 4 | using Discord.WebSocket; 5 | using System.Threading.Tasks; 6 | using Voltaire.Controllers.Messages; 7 | 8 | namespace Voltaire.Modules 9 | { 10 | public class MessageInteractions : InteractionsBase 11 | { 12 | public MessageInteractions(DataBase database): base(database) {} 13 | 14 | [SlashCommand("send", "send an anonymous message to the specified channel in the current server")] 15 | public async Task Send( 16 | SocketChannel channel, 17 | string message, 18 | bool repliable = false 19 | ) 20 | { 21 | // this is broken in DMs because of this: https://github.com/discord/discord-api-docs/issues/2820 22 | try { 23 | await Controllers.Messages.SendToGuild.SendToChannelById(channel.Id, new InteractionBasedContext(Context, Responder), message, repliable, _database); 24 | } 25 | catch (Exception ex) 26 | { 27 | Console.WriteLine(ex); 28 | } 29 | } 30 | 31 | [SlashCommand("volt", "send an anonymous message to the current channel")] 32 | public async Task Volt( 33 | string message, 34 | bool repliable = false, 35 | [Summary("channel", "send message to target channel")] SocketChannel channel = null, 36 | [Summary("channel-name", "send message to target channel by name or ID")] String channelName = null 37 | ) 38 | { 39 | if (Context.Guild == null) { 40 | var function = Controllers.Messages.Send.SendMessageToChannel(Context.Channel, repliable == true, new InteractionBasedContext(Context, Responder), false); 41 | await function("", message); 42 | await RespondAsync("Sent!", ephemeral: true); 43 | return; 44 | } 45 | if (channel != null) { 46 | await Controllers.Messages.SendToGuild.SendToChannelById(channel.Id, new InteractionBasedContext(Context, Responder), message, repliable, _database); 47 | return; 48 | } 49 | if (channelName != null) { 50 | await SendToGuild.LookupAndSendAsync(Context.Guild, new InteractionBasedContext(Context, Responder), channelName, message, repliable, _database); 51 | return; 52 | } 53 | await SendToGuild.LookupAndSendAsync(Context.Guild, new InteractionBasedContext(Context, Responder), Context.Channel.Id.ToString(), message, repliable, _database); 54 | } 55 | 56 | [SlashCommand("send-dm", "send an anonymous message to the specified user")] 57 | public async Task SendDirectMessage(SocketUser user, string message, bool repliable = false) 58 | { 59 | await Controllers.Messages.SendDirectMessage.PerformAsync(new InteractionBasedContext(Context, Responder), user.Id.ToString(), message, repliable, _database); 60 | } 61 | 62 | [SlashCommand("send-reply", "reply to an anonymous message with a reply code")] 63 | public async Task SendReply([Summary("reply-code", "the code on the message you'd like to reply to")] string reply_code, string message, bool repliable = false) 64 | { 65 | await Controllers.Messages.SendReply.PerformAsync(new InteractionBasedContext(Context, Responder), reply_code, message, repliable, _database); 66 | } 67 | 68 | 69 | [ModalInteraction("send-message:*,*")] 70 | public async Task SendMessageInteractionHandler(string channelId, string repliableString, Views.Modals.MessagePrompt prompt) { 71 | var repliable = bool.Parse(repliableString); 72 | 73 | await SendToGuild.LookupAndSendAsync(Context.Guild, new InteractionBasedContext(Context, Responder), channelId, prompt.message, repliable, _database); 74 | 75 | } 76 | 77 | [ModalInteraction("send-reply:*:::*")] 78 | public async Task SendReplyInteractionHandler(string replyHash, string repliableString, Views.Modals.MessagePrompt prompt) { 79 | var repliable = bool.Parse(repliableString); 80 | 81 | await Controllers.Messages.SendReply.PerformAsync(new InteractionBasedContext(Context, Responder), replyHash, prompt.message, repliable, _database); 82 | } 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /Voltaire/Views/Info/Help.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace Voltaire.Views.Info 10 | { 11 | public static class Help 12 | { 13 | 14 | public static Tuple Response(ShardedCommandContext context) 15 | { 16 | 17 | var embed = new EmbedBuilder 18 | { 19 | Author = new EmbedAuthorBuilder 20 | { 21 | Name = "Guide" 22 | }, 23 | ThumbnailUrl = "https://nminchow.github.io/VoltaireWeb/images/quill.png", 24 | Description = "Voltaire allows you to send messages to a discord server anonymously.\n\n" + 25 | "Most commands should be direct messaged to this bot user, which will then relay them to the desired channel.\n\n" + 26 | "Support Server: https://discord.gg/xyzMyJH \n\n" + 27 | "**Direct Message Commands:**", 28 | Color = new Color(111, 111, 111), 29 | Footer = new EmbedFooterBuilder 30 | { 31 | Text = "Developer note: the {user name}, {server name} and {channel name} arguments above can also be User, Server, and Channel IDs.", 32 | IconUrl = "" 33 | } 34 | }; 35 | 36 | embed.AddField("send {channel name} {message}", "Sends an anonymous message to the specified channel." + 37 | $"\nex: `send {ChannelName(context)} The cake is a lie.`"); 38 | embed.AddField("send_dm {user name} {message}", "Sends an anonymous message to the specified user." + 39 | $"\nex: `send_dm @Voltaire The right man in the wrong place can make all the difference in the world.`"); 40 | embed.AddField("send_server \"{server name}\" {channel name} {message}", "This command is only needed if you belong to " + 41 | "multiple servers that have Voltaire installed. It allows you to specify which server you are sending to." + 42 | $"\nex: `send_server \"{GuildName(context)}\" {ChannelName(context)} A man chooses, a slave obeys.`"); 43 | embed.AddField("+r", "All 3 of the above 'send' commands also have a version which will allow other users to reply anonymously. " + 44 | "The reply version of the command is appended with a `+r` suffix." + 45 | $"\nex: `send_server+r \"{GuildName(context)}\" {ChannelName(context)} If we can just get back to Earth, and find Halsey, she can fix this.`"); 46 | embed.AddField("react {message ID} {emote/emoji}", 47 | "Send a reaction to a message. [Enable dev settings to get message IDs](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID). " + 48 | "The `emote/emoji` param can be either a unicode emoji, or the name of a custom emote." + 49 | $"\nex: `react {context.Message.Id} 📜` or `react {context.Message.Id} your_custom_emote`"); 50 | //embed.AddField("send_reply {reply code} {message}", "To reply to a message, it will need to have been originally sent with the +r suffix. The message will include" + 51 | // "a code at the bottom which can be used to reply." + 52 | // $"\nex: `send_reply iMIb62udZ7R/KCfhn634+AHvrrQ Don't make a girl a promise you know you can't keep.`"); 53 | embed.AddField("!volt link", "Display the [bot's invite link](https://discordapp.com/oauth2/authorize?client_id=425833927517798420&permissions=2147998784&scope=bot%20applications.commands)."); 54 | embed.AddField("!volt faq", "Display the [FAQ link](https://discordapp.com/channels/426894892262752256/581280324340940820/612849796025155585)."); 55 | embed.AddField("!volt admin", "(server admin only - callable from server channel) Get a list of admin commands, including details on Voltaire Pro."); 56 | embed.AddField("!volt help", "(callable from anywhere) Display this help dialogue."); 57 | 58 | return new Tuple("", embed.Build()); 59 | } 60 | 61 | private static string ChannelName(ShardedCommandContext context) 62 | { 63 | return context == null || context.IsPrivate ? "some-channel" : context.Channel.Name; 64 | } 65 | 66 | private static string GuildName(ShardedCommandContext context) 67 | { 68 | return context.IsPrivate ? "l33t g4merz" : context.Guild.Name; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Messages/SendDirectMessage.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using Voltaire.Controllers.Helpers; 9 | 10 | namespace Voltaire.Controllers.Messages 11 | { 12 | class SendDirectMessage 13 | { 14 | public static async Task PerformAsync(UnifiedContext context, string userName, string message, bool replyable, DataBase db) 15 | { 16 | // convert special discord tag to regular ID format 17 | userName = userName.StartsWith("<@!") && userName.EndsWith('>') ? userName.Substring(3, userName.Length - 4) : userName; 18 | userName = userName.StartsWith("<@") && userName.EndsWith('>') ? userName.Substring(2, userName.Length - 3) : userName; 19 | 20 | userName = userName.StartsWith('@') ? userName.Substring(1) : userName; 21 | try 22 | { 23 | var guildList = Send.GuildList(context); 24 | List allUsersList = ToUserList(guildList); 25 | 26 | var userList = allUsersList.Where(x => x.Username != null && 27 | ( 28 | // simple username 29 | x.Username.ToLower() == userName.ToLower() || 30 | // id 31 | x.Id.ToString() == userName || 32 | // username with discriminator 33 | $"{x.Username}#{x.Discriminator}".ToLower() == userName.ToLower() 34 | ) 35 | && !x.IsBot); 36 | 37 | var allowDmList = userList.Where(x => FilterGuildByDirectMessageSetting(x, db)); 38 | 39 | if (!allowDmList.Any() && userList.Any()) 40 | { 41 | await Send.SendErrorWithDeleteReaction(context, "user found, but channel permissions do not allow annonymous direct messaging"); 42 | return; 43 | } 44 | 45 | var requiredRoleList = allowDmList.Where(x => FilterGuildByRole(x, context.User, db)); 46 | 47 | if (!requiredRoleList.Any() && allowDmList.Any()) 48 | { 49 | await Send.SendErrorWithDeleteReaction(context, "user found, but you do not have the role required to DM them"); 50 | return; 51 | } 52 | 53 | var list = requiredRoleList.ToList().Select(async x => Tuple.Create(x, await FindOrCreateGuild.Perform(x.Guild, db))); 54 | 55 | var userGuild = list.FirstOrDefault(x => !PrefixHelper.UserBlocked(context.User.Id, x.Result.Item2)); 56 | 57 | if (userGuild == null && requiredRoleList.Any()) 58 | { 59 | await Send.SendErrorWithDeleteReaction(context, "user found, but you have been banned from using Voltaire on your shared server"); 60 | } 61 | else if (userGuild == null) 62 | { 63 | await Send.SendErrorWithDeleteReaction(context, "user not found"); 64 | return; 65 | } 66 | 67 | var userChannel = await userGuild.Result.Item1.CreateDMChannelAsync(); 68 | var prefix = PrefixHelper.ComputePrefix(context, userGuild.Result.Item2, "anonymous user"); 69 | var messageFunction = Send.SendMessageToChannel(userChannel, replyable, context); 70 | var sentMessage = await messageFunction(prefix, message); 71 | await Send.AddReactionToMessage(sentMessage); 72 | await Send.SendSentEmoteIfCommand(context); 73 | } 74 | catch (Exception ex) 75 | { 76 | Console.WriteLine(ex.ToString()); 77 | } 78 | return; 79 | } 80 | 81 | public static List ToUserList(IEnumerable guildList) 82 | { 83 | return guildList.Aggregate(new List(), (acc, item) => acc.Concat(item.Users).ToList()); 84 | } 85 | 86 | private static bool FilterGuildByDirectMessageSetting(SocketGuildUser user, DataBase db) 87 | { 88 | return ! db.Guilds.Any(x => x.DiscordId == user.Guild.Id.ToString() && !x.AllowDirectMessage); 89 | } 90 | 91 | private static bool FilterGuildByRole(SocketGuildUser reciver, IUser sender, DataBase db) 92 | { 93 | var guild = db.Guilds.FirstOrDefault(x => x.DiscordId == reciver.Guild.Id.ToString()); 94 | 95 | return UserHasRole.Perform(reciver.Guild, sender, guild); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Voltaire/Controllers/Messages/SendToGuild.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Discord.WebSocket; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using System; 6 | using Voltaire.Controllers.Helpers; 7 | 8 | namespace Voltaire.Controllers.Messages 9 | { 10 | class SendToGuild 11 | { 12 | public static async Task PerformAsync(UnifiedContext context, string guildName, string channelName, string message, bool replyable, DataBase db) 13 | { 14 | var unfilteredList = Send.GuildList(context); 15 | var candidateGuilds = unfilteredList.Where(x => x.Id.ToString() == guildName || x.Name.ToLower().Contains(guildName.ToLower())); 16 | 17 | switch (candidateGuilds.Count()) 18 | { 19 | case 0: 20 | await Send.SendErrorWithDeleteReaction(context, "No servers with the specified name could be found. The servers must have Voltaire installed and you must be a member of the server."); 21 | break; 22 | case 1: 23 | await LookupAndSendAsync(candidateGuilds.First(), context, channelName, message, replyable, db); 24 | break; 25 | default: 26 | // check for exact match 27 | var exactNameMatch = candidateGuilds.First(x => x.Id.ToString() == guildName || x.Name.ToLower() == guildName.ToLower()); 28 | if (exactNameMatch != null) 29 | { 30 | await LookupAndSendAsync(exactNameMatch, context, channelName, message, replyable, db); 31 | return; 32 | } 33 | await Send.SendErrorWithDeleteReaction(context, "More than one server with the spcified name was found. Please use a more specific server name."); 34 | break; 35 | } 36 | } 37 | 38 | public static async Task SendToChannelById(ulong channelId, UnifiedContext context, string message, bool replyable, DataBase db) 39 | { 40 | var unfilteredList = ToChannelList(Send.GuildList(context)); 41 | var target = unfilteredList.FirstOrDefault(x => x.Id == channelId); 42 | await LookupAndSendAsync(target.Guild, context, channelId.ToString(), message, replyable, db); 43 | return; 44 | } 45 | 46 | public static List ToChannelList(IEnumerable guildList) 47 | { 48 | return guildList.Aggregate(new List(), (acc, item) => acc.Concat(item.Channels).ToList()); 49 | } 50 | 51 | public static async Task LookupAndSendAsync(SocketGuild guild, UnifiedContext context, string channelName, string message, bool replyable, DataBase db) 52 | { 53 | var dbGuild = await FindOrCreateGuild.Perform(guild, db); 54 | if (!UserHasRole.Perform(guild, context.User, dbGuild)) 55 | { 56 | await Send.SendErrorWithDeleteReaction(context, "You do not have the role required to send messages to this server."); 57 | return; 58 | } 59 | 60 | var candidateChannels = guild.TextChannels.Where(x => x.Name.ToLower().Contains(channelName.ToLower()) || x.Id.ToString() == channelName); 61 | if (!candidateChannels.Any()) 62 | { 63 | await Send.SendErrorWithDeleteReaction(context, "The channel you specified couldn't be found. Please specify your desired channel before your message: `send (channel_name) (message)` ex: `send some-channel Nothing is true, everything is permitted.`"); 64 | return; 65 | } 66 | 67 | if (PrefixHelper.UserBlocked(context.User.Id, dbGuild)) 68 | { 69 | await Send.SendErrorWithDeleteReaction(context, "It appears that you have been banned from using Voltaire on the targeted server. If you think this is an error, contact one of your admins."); 70 | return; 71 | } 72 | 73 | if(! await IncrementAndCheckMessageLimit.Perform(dbGuild, db)) 74 | { 75 | await Send.SendErrorWithDeleteReaction(context, "This server has reached its limit of 50 messages for the month. To lift this limit, ask an admin or moderator to upgrade your server to Voltaire Pro. (This can be done via the `/pro` command.)"); 76 | return; 77 | } 78 | 79 | var prefix = PrefixHelper.ComputePrefix(context, dbGuild); 80 | var channel = candidateChannels.OrderBy(x => x.Name.Length).First(); 81 | var messageFunction = Send.SendMessageToChannel(channel, replyable, context, dbGuild.UseEmbed); 82 | await messageFunction(prefix, message); 83 | await Send.SendSentEmoteIfCommand(context); 84 | return; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Voltaire/Modules/Admin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Discord.Commands; 4 | using Discord; 5 | using Discord.WebSocket; 6 | 7 | namespace Voltaire.Modules 8 | { 9 | public class Admin : ModuleBase 10 | { 11 | private DataBase _database; 12 | 13 | public Admin(DataBase database) 14 | { 15 | _database = database; 16 | } 17 | 18 | [Command("allow_dm", RunMode = RunMode.Async)] 19 | [Summary("Allow Direct Messages To Be Sent Anonymously Through This Server")] 20 | [Preconditions.Administrator] 21 | public async Task AllowDm(Boolean allow) 22 | { 23 | await Controllers.Settings.SetDirectMessageAccess.PerformAsync(new CommandBasedContext(Context), allow, _database); 24 | } 25 | 26 | [Command("user_identifiers", RunMode = RunMode.Async)] 27 | [Summary("Use a Unique (Yet Anonymous) Identifier For Users When Sending Messages")] 28 | [Preconditions.Administrator] 29 | public async Task UserIdentifiers(Boolean allow) 30 | { 31 | await Controllers.Settings.SetUseUserIdentifiers.PerformAsync(new CommandBasedContext(Context), allow, _database); 32 | } 33 | 34 | [Command("embeds", RunMode = RunMode.Async)] 35 | [Summary("Make All Messages Sent Via the Bot Appear as Embeds")] 36 | [Preconditions.Administrator] 37 | public async Task Embeds(Boolean allow) 38 | { 39 | await Controllers.Settings.SetEmbeds.PerformAsync(new CommandBasedContext(Context), allow, _database); 40 | } 41 | 42 | [Command("permitted_role", RunMode = RunMode.Async)] 43 | [Summary("Set the Role Allowed to Use Voltaire")] 44 | [Preconditions.Administrator] 45 | public async Task PermittedRole(SocketRole role) 46 | { 47 | await Controllers.Settings.SetAllowedRole.PerformAsync(new CommandBasedContext(Context), role, _database); 48 | } 49 | 50 | [Command("permitted_role all", RunMode = RunMode.Async)] 51 | [Summary("Allow All Users to Use Voltaire")] 52 | [Preconditions.Administrator] 53 | public async Task PermittedRoleClear() 54 | { 55 | await Controllers.Settings.ClearAllowedRole.PerformAsync(new CommandBasedContext(Context), _database); 56 | } 57 | 58 | [Command("new_identifiers", RunMode = RunMode.Async)] 59 | [Summary("Rotate User Identifiers")] 60 | [Preconditions.Administrator] 61 | public async Task NewIdentifiers() 62 | { 63 | await Controllers.Settings.GenerateGuildUserIdentifierSeed.PerformAsync(new CommandBasedContext(Context), _database); 64 | } 65 | 66 | [Command("ban", RunMode = RunMode.Async)] 67 | [Summary("Ban a given identifer seed")] 68 | [Preconditions.Administrator] 69 | public async Task Ban(string identifier) 70 | { 71 | await Controllers.Settings.BanIdentifier.PerformAsync(new CommandBasedContext(Context), identifier, _database); 72 | } 73 | 74 | [Command("unban", RunMode = RunMode.Async)] 75 | [Summary("Unban a given identifer seed")] 76 | [Preconditions.Administrator] 77 | public async Task UnBan(string identifier) 78 | { 79 | await Controllers.Settings.UnBanIdentifier.PerformAsync(new CommandBasedContext(Context), identifier, _database); 80 | } 81 | 82 | [Command("list_bans", RunMode = RunMode.Async)] 83 | [Summary("list current bans")] 84 | [Preconditions.Administrator] 85 | public async Task ListBans() 86 | { 87 | await Controllers.Settings.ListBans.PerformAsync(new CommandBasedContext(Context), _database); 88 | } 89 | 90 | [Command("clear_bans", RunMode = RunMode.Async)] 91 | [Summary("list current bans")] 92 | [Preconditions.Administrator] 93 | public async Task ClearBans() 94 | { 95 | await Controllers.Settings.ClearBans.PerformAsync(new CommandBasedContext(Context), _database); 96 | } 97 | 98 | // Require true admin 99 | [Command("admin_role", RunMode = RunMode.Async)] 100 | [Summary("Set the Role Allowed to Configure Voltaire and Ban Users")] 101 | [RequireUserPermission(GuildPermission.Administrator)] 102 | public async Task AdminRole(SocketRole role) 103 | { 104 | await Controllers.Settings.SetAdminRole.PerformAsync(new CommandBasedContext(Context), role, _database); 105 | } 106 | 107 | // no precondition 108 | [Command("refresh", RunMode = RunMode.Async)] 109 | [Summary("Refresh the bot's user cache for this server")] 110 | public async Task Refresh() 111 | { 112 | await Controllers.Settings.Refresh.PerformAsync(new CommandBasedContext(Context), _database); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | appsettings.json 263 | notes.md 264 | -------------------------------------------------------------------------------- /Voltaire/Utility/Encryption.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * This work (Modern Encryption of a String C#, by James Tuley), 3 | * identified by James Tuley, is free of known copyright restrictions. 4 | * https://gist.github.com/4336842 5 | * http://creativecommons.org/publicdomain/mark/1.0/ 6 | */ 7 | 8 | using System; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Security.Cryptography; 12 | using System.Text; 13 | 14 | namespace Voltaire.Utility 15 | { 16 | public static class Encryption 17 | { 18 | // This constant is used to determine the keysize of the encryption algorithm in bits. 19 | // We divide this by 8 within the code below to get the equivalent number of bytes. 20 | private const int Keysize = 256; 21 | 22 | // This constant determines the number of iterations for the password bytes generation function. 23 | private const int DerivationIterations = 100; 24 | 25 | public static string Encrypt(string plainText, string passPhrase) 26 | { 27 | // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text 28 | // so that the same Salt and IV values can be used when decrypting. 29 | var saltStringBytes = Generate256BitsOfRandomEntropy(); 30 | var ivStringBytes = Generate256BitsOfRandomEntropy(); 31 | var plainTextBytes = Encoding.UTF8.GetBytes(plainText); 32 | using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)) 33 | { 34 | var keyBytes = password.GetBytes(Keysize / 8); 35 | using (var symmetricKey = new RijndaelManaged()) 36 | { 37 | symmetricKey.BlockSize = 256; 38 | symmetricKey.Mode = CipherMode.CBC; 39 | symmetricKey.Padding = PaddingMode.PKCS7; 40 | using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes)) 41 | { 42 | using (var memoryStream = new MemoryStream()) 43 | { 44 | using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) 45 | { 46 | cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); 47 | cryptoStream.FlushFinalBlock(); 48 | // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes. 49 | var cipherTextBytes = saltStringBytes; 50 | cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray(); 51 | cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray(); 52 | memoryStream.Close(); 53 | cryptoStream.Close(); 54 | return Convert.ToBase64String(cipherTextBytes); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | public static string Decrypt(string cipherText, string passPhrase) 63 | { 64 | // Get the complete stream of bytes that represent: 65 | // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText] 66 | var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText); 67 | // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes. 68 | var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray(); 69 | // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes. 70 | var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray(); 71 | // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string. 72 | var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((Keysize / 8) * 2)).ToArray(); 73 | 74 | using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)) 75 | { 76 | var keyBytes = password.GetBytes(Keysize / 8); 77 | using (var symmetricKey = new RijndaelManaged()) 78 | { 79 | symmetricKey.BlockSize = 256; 80 | symmetricKey.Mode = CipherMode.CBC; 81 | symmetricKey.Padding = PaddingMode.PKCS7; 82 | using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes)) 83 | { 84 | using (var memoryStream = new MemoryStream(cipherTextBytes)) 85 | { 86 | using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) 87 | { 88 | var plainTextBytes = new byte[cipherTextBytes.Length]; 89 | var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); 90 | memoryStream.Close(); 91 | cryptoStream.Close(); 92 | return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | private static byte[] Generate256BitsOfRandomEntropy() 101 | { 102 | var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits. 103 | //using (var rngCsp = new RNGCryptoServiceProvider()) 104 | //{ 105 | // // Fill the array with cryptographically secure random bytes. 106 | // rngCsp.GetBytes(randomBytes); 107 | //} 108 | return randomBytes; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /Voltaire/Modules/AdminInteractions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Discord; 3 | using Discord.Interactions; 4 | using Discord.WebSocket; 5 | using System.Threading.Tasks; 6 | 7 | namespace Voltaire.Modules 8 | { 9 | public class AdminInteractions 10 | { 11 | [Group("volt-admin", "Voltaire admin commands")] 12 | public class AdminGroup : InteractionsBase 13 | { 14 | public AdminGroup(DataBase database): base(database) {} 15 | 16 | [SlashCommand("settings", "configure Voltaire's general settings")] 17 | [Preconditions.AdministratorInteraction] 18 | public async Task Settings( 19 | [Summary("allow-DM", "allow users to anonymously message one another via the bot")] Boolean? allowDM = null, 20 | [Summary("use-identifiers", "use a unique (yet anonymous) identifier for users when sending messages")] Boolean? identifiers = null, 21 | [Summary("embeds", "make all messages sent via the bot appear as embeds")] Boolean? embeds = null, 22 | [Summary("permitted-role", "set the role allowed to use voltaire")] SocketRole role = null 23 | ) 24 | { 25 | Func SilentResponder = (response, embed) => { return Task.CompletedTask; }; 26 | var context = new InteractionBasedContext(Context, SilentResponder); 27 | if (allowDM is Boolean allowDMvalue) { 28 | await Controllers.Settings.SetDirectMessageAccess.PerformAsync(context, allowDMvalue, _database); 29 | } 30 | if (identifiers is Boolean identifiersValue) { 31 | await Controllers.Settings.SetUseUserIdentifiers.PerformAsync(context, identifiersValue, _database); 32 | } 33 | if (embeds is Boolean embedsValue) { 34 | await Controllers.Settings.SetEmbeds.PerformAsync(context, embedsValue, _database); 35 | } 36 | if (role is SocketRole roleValue) { 37 | Console.WriteLine("setting allowed role"); 38 | await Controllers.Settings.SetAllowedRole.PerformAsync(context, roleValue, _database); 39 | } 40 | await RespondAsync("settings updated!", ephemeral: true); 41 | return; 42 | } 43 | 44 | [SlashCommand("new-identifiers", "rotate user identifiers")] 45 | [Preconditions.AdministratorInteraction] 46 | public async Task RotateIdentifiers() 47 | { 48 | await Controllers.Settings.GenerateGuildUserIdentifierSeed.PerformAsync(new InteractionBasedContext(Context, Responder), _database); 49 | } 50 | 51 | [SlashCommand("ban", "ban a given identifier")] 52 | [Preconditions.AdministratorInteraction] 53 | public async Task Ban(string identifier) 54 | { 55 | await Controllers.Settings.BanIdentifier.PerformAsync(new InteractionBasedContext(Context, Responder), identifier, _database); 56 | } 57 | 58 | [SlashCommand("unban", "unban a given identifier")] 59 | [Preconditions.AdministratorInteraction] 60 | public async Task UnBan(string identifier) 61 | { 62 | await Controllers.Settings.UnBanIdentifier.PerformAsync(new InteractionBasedContext(Context, Responder), identifier, _database); 63 | } 64 | 65 | [SlashCommand("list-bans", "list current bans")] 66 | [Preconditions.AdministratorInteraction] 67 | public async Task ListBans() 68 | { 69 | await Controllers.Settings.ListBans.PerformAsync(new InteractionBasedContext(Context, Responder), _database); 70 | } 71 | 72 | [SlashCommand("clear-bans", "clear current bans")] 73 | [Preconditions.AdministratorInteraction] 74 | public async Task ClearBans() 75 | { 76 | await Controllers.Settings.ClearBans.PerformAsync(new InteractionBasedContext(Context, Responder), _database); 77 | } 78 | 79 | [SlashCommand("refresh", "refresh the bot's user cache for this server")] 80 | public async Task Refresh() 81 | { 82 | await Controllers.Settings.Refresh.PerformAsync(new InteractionBasedContext(Context, Responder), _database); 83 | } 84 | 85 | [SlashCommand("role", "set the admin role Allowed to Configure Voltaire and Ban Users")] 86 | [RequireUserPermission(GuildPermission.Administrator)] 87 | public async Task AdminRole(SocketRole role) 88 | { 89 | await Controllers.Settings.SetAdminRole.PerformAsync(new InteractionBasedContext(Context, Responder), role, _database); 90 | } 91 | 92 | [SlashCommand("help", "get admin command overview")] 93 | public async Task Help( 94 | [Summary("private", "show the help dialogue privately")] Boolean? ephemeral = null 95 | ) 96 | { 97 | var view = Views.Info.SlashAdmin.Response(new InteractionBasedContext(Context, Responder)); 98 | await RespondAsync(view.Item1, embed: view.Item2, ephemeral: ephemeral == true); 99 | } 100 | 101 | [SlashCommand("create-prompt", "create a prompt for users to send DMs to your target channel")] 102 | [RequireUserPermission(GuildPermission.Administrator)] 103 | public async Task CreatePrompt( 104 | [Summary("channel", "channel where responses will be sent")] SocketChannel channel, 105 | [Summary("repliable", "set whether sent messages will be repliable")] bool repliable = false, 106 | [Summary("prompt", "the text you'd like users to see when responding")] string prompt = null 107 | ) 108 | { 109 | var builder = new ComponentBuilder().WithButton("Send Message", $"prompt-message:{channel.Id},{repliable}"); 110 | 111 | var embed = Views.Info.Prompt.Response(prompt, channel, Context); 112 | 113 | await RespondAsync(components: builder.Build(), ephemeral: false, embed: embed.Item2); 114 | } 115 | 116 | } 117 | 118 | 119 | } 120 | } -------------------------------------------------------------------------------- /Voltaire/Controllers/Messages/Send.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.WebSocket; 3 | using Rijndael256; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Voltaire.Controllers.Messages 10 | { 11 | class Send 12 | { 13 | public static async Task PerformAsync(UnifiedContext context, string channelName, string message, bool reply, DataBase db) 14 | { 15 | var candidateGuilds = GuildList(context); 16 | switch (candidateGuilds.Count()) 17 | { 18 | case 0: 19 | await SendErrorWithDeleteReaction(context, "It doesn't look like you belong to any servers where Voltaire is installed. Please add Voltaire to your desired server."); 20 | break; 21 | case 1: 22 | await SendToGuild.LookupAndSendAsync(candidateGuilds.First(), context, channelName, message, reply, db); 23 | break; 24 | default: 25 | var view = Views.Info.MultipleGuildSendResponse.Response(candidateGuilds, message); 26 | await SendErrorWithDeleteReaction(context, view.Item1, view.Item2); 27 | break; 28 | } 29 | } 30 | 31 | public static Func> SendMessageToChannel(IMessageChannel channel, bool replyable, UnifiedContext context, bool forceEmbed = false) 32 | { 33 | if (!replyable) 34 | { 35 | return async (username, message) => 36 | { 37 | message = CheckForMentions(channel, message); 38 | if (forceEmbed) 39 | { 40 | var view = Views.Message.Response(username, message); 41 | return await SendMessageAndCatchError(() => { return channel.SendMessageAsync(view.Item1, embed: view.Item2); }, context); 42 | } 43 | 44 | if (string.IsNullOrEmpty(username)) 45 | { 46 | return await SendMessageAndCatchError(() => { return channel.SendMessageAsync(message); }, context); 47 | } 48 | return await SendMessageAndCatchError(() => { return channel.SendMessageAsync($"**{username}**: {message}"); }, context); 49 | }; 50 | } 51 | return async (username, message) => 52 | { 53 | var key = LoadConfig.Instance.config["encryptionKey"]; 54 | var replyHash = Rijndael.Encrypt(context.User.Id.ToString(), key, KeySize.Aes256); 55 | var view = Views.Message.Response(username, message); 56 | return await SendMessageAndCatchError(() => { 57 | var builder = new ComponentBuilder() 58 | .WithButton("Send DM Reply", $"prompt-reply:{replyHash}:{false}") 59 | .WithButton("Send Repliable DM", $"prompt-reply:{replyHash}:{true}"); 60 | return channel.SendMessageAsync(view.Item1, embed: view.Item2, components: builder.Build()); 61 | }, context); 62 | }; 63 | } 64 | 65 | public static async Task SendMessageAndCatchError(Func> send, UnifiedContext context) 66 | { 67 | try 68 | { 69 | return await send(); 70 | } 71 | catch (Discord.Net.HttpException e) 72 | { 73 | switch (e.DiscordCode) 74 | { 75 | case DiscordErrorCode.CannotSendMessageToUser: 76 | await SendMessageToContext(context, "Voltaire has been blocked by this user, or they have DMs dsiabled."); 77 | break; 78 | case DiscordErrorCode.InsufficientPermissions: 79 | case DiscordErrorCode.MissingPermissions: 80 | await SendMessageToContext(context, "Voltaire doesn't have the " + 81 | "permissions required to send this message. Ensure Voltaire can access the channel you are trying to send to, and that it has " + 82 | " \"Embed Links\" and \"Use External Emojis\" permission."); 83 | break; 84 | } 85 | 86 | throw e; 87 | } 88 | } 89 | 90 | private static string CheckForMentions(IMessageChannel channel, string message) 91 | { 92 | var words = message.Split().Where(x => x.StartsWith("@")); 93 | if (!words.Any()) 94 | return message; 95 | 96 | var users = AsyncEnumerableExtensions.Flatten(channel.GetUsersAsync()); 97 | 98 | users.Select(x => $"@{x.Username}").Intersect(words.ToAsyncEnumerable()).ForEachAsync(async x => 99 | { 100 | var user = await users.FirstAsync(y => y.Username == x.Substring(1)); 101 | message = message.Replace(x, user.Mention); 102 | }); 103 | 104 | users.Select(x => $"@{x.Username}#{x.Discriminator}").Intersect(words.ToAsyncEnumerable()).ForEachAsync(async x => 105 | { 106 | var user = await users.FirstAsync(y => $"@{y.Username}#{y.Discriminator}" == x); 107 | message = message.Replace(x, user.Mention); 108 | }); 109 | 110 | if (channel is SocketTextChannel) 111 | { 112 | var castChannel = (SocketTextChannel)channel; 113 | var roles = castChannel.Guild.Roles; 114 | roles.Select(x => $"@{x.Name}").Intersect(words).ToList().ForEach(x => 115 | { 116 | var role = roles.First(y => y.Name == x.Substring(1)); 117 | message = message.Replace(x, role.Mention); 118 | }); 119 | } 120 | 121 | return message; 122 | } 123 | 124 | public static IEnumerable GuildList(UnifiedContext currentContext) 125 | { 126 | var guilds = currentContext.Client.Guilds.Where(x => x.Users.Any(u => u.Id == currentContext.User.Id)); 127 | return guilds; 128 | } 129 | 130 | public static async Task SendMessageToContext(UnifiedContext context, string message, Embed embed = null) 131 | { 132 | if (context is CommandBasedContext commandContext) { 133 | await commandContext.Channel.SendMessageAsync(message, embed: embed); 134 | } else if ( context is InteractionBasedContext interactionContext) { 135 | await interactionContext.Responder(message, embed); 136 | } 137 | } 138 | 139 | public static async Task SendSentEmoteIfCommand(UnifiedContext context) 140 | { 141 | if (context is CommandBasedContext commandContext) { 142 | var emote = Emote.Parse(LoadConfig.Instance.config["sent_emoji"]); 143 | await commandContext.Message.AddReactionAsync(emote); 144 | } else if ( context is InteractionBasedContext interactionContext) { 145 | await interactionContext.Responder("Sent!", null); 146 | } 147 | } 148 | 149 | public static async Task SendErrorWithDeleteReaction(UnifiedContext context, string errorMessage, Embed embed = null) 150 | { 151 | if (context is CommandBasedContext commandContext) { 152 | var message = await commandContext.Channel.SendMessageAsync(errorMessage, embed: embed); 153 | await AddReactionToMessage(message); 154 | } else if ( context is InteractionBasedContext interactionContext) { 155 | await interactionContext.Responder(errorMessage, embed); 156 | } 157 | } 158 | 159 | public static async Task AddReactionToMessage(IUserMessage message) 160 | { 161 | var emote = new Emoji(DeleteEmote); 162 | await message.AddReactionAsync(emote); 163 | } 164 | 165 | public static string DeleteEmote = "🗑"; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Voltaire/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Reflection; 4 | using Discord; 5 | using Discord.WebSocket; 6 | using Discord.Commands; 7 | using Discord.Interactions; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Configuration; 10 | using Stripe; 11 | using System.Linq; 12 | using Microsoft.EntityFrameworkCore; 13 | using Microsoft.Extensions.Configuration; 14 | 15 | namespace Voltaire 16 | { 17 | class Program 18 | { 19 | private CommandService _commands; 20 | private InteractionService _interactions; 21 | private DiscordShardedClient _client; 22 | private IServiceProvider _services; 23 | 24 | private System.Collections.Generic.IEnumerable _interactionModules; 25 | 26 | public static void Main(string[] args) 27 | => new Program().MainAsync().GetAwaiter().GetResult(); 28 | 29 | public async Task MainAsync() 30 | { 31 | 32 | IConfiguration configuration = LoadConfig.Instance.config; 33 | var optionsBuilder = new DbContextOptionsBuilder(); 34 | optionsBuilder.UseSqlServer($@"{configuration.GetConnectionString("sql")}"); 35 | var db = new DataBase(optionsBuilder.Options); 36 | 37 | var config = new DiscordSocketConfig { 38 | // LogLevel = LogSeverity.Debug, 39 | AlwaysDownloadUsers = true, 40 | GatewayIntents = GatewayIntents.GuildMembers | 41 | GatewayIntents.Guilds | 42 | GatewayIntents.GuildEmojis | 43 | GatewayIntents.GuildMessages | 44 | GatewayIntents.GuildMessageReactions | 45 | GatewayIntents.DirectMessages | 46 | GatewayIntents.DirectMessageReactions 47 | }; 48 | 49 | _client = new DiscordShardedClient(config); 50 | _client.Log += Log; 51 | _client.JoinedGuild += Controllers.Helpers.JoinedGuild.Joined(db); 52 | // disable joined message for now 53 | //_client.UserJoined += Controllers.Helpers.UserJoined.SendJoinedMessage; 54 | 55 | 56 | _commands = new CommandService(); 57 | _interactions = new InteractionService(_client); 58 | 59 | string token = configuration["discordAppToken"]; 60 | 61 | StripeConfiguration.SetApiKey(configuration["stripe_api_key"]); 62 | 63 | _services = new ServiceCollection() 64 | .AddSingleton(_client) 65 | .AddSingleton(_commands) 66 | .AddSingleton(_interactions) 67 | .AddDbContext(options => options.UseSqlServer($@"{configuration.GetConnectionString("sql")}")) 68 | .BuildServiceProvider(); 69 | 70 | await InstallCommandsAsync(); 71 | 72 | await _client.LoginAsync(TokenType.Bot, token); 73 | await _client.SetGameAsync("/volt-help", null, ActivityType.Watching); 74 | 75 | await _client.StartAsync(); 76 | 77 | 78 | await Task.Delay(-1); 79 | 80 | } 81 | 82 | public async Task InstallCommandsAsync() 83 | { 84 | // Hook the MessageReceived Event into our Command Handler 85 | _client.MessageReceived += HandleCommandAsync; 86 | _client.ReactionAdded += HandleReaction; 87 | _client.ShardReady += RegisterCommands; 88 | // Discover all of the commands in this assembly and load them. 89 | await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); 90 | _interactionModules = await _interactions.AddModulesAsync(Assembly.GetEntryAssembly(), _services); 91 | _interactions.SlashCommandExecuted += SlashCommandExecuted; 92 | _client.InteractionCreated += HandleInteraction; 93 | } 94 | 95 | private async Task RegisterCommands(DiscordSocketClient client) 96 | { 97 | if (client.ShardId != 0) return; 98 | if (LoadConfig.Instance.config["dev_server"] != null) { 99 | var guild = client.Guilds.First(x => x.Id.ToString() == LoadConfig.Instance.config["dev_server"]); 100 | Console.WriteLine($"Registering commands for {guild.Name}"); 101 | var result = await _interactions.AddModulesToGuildAsync( 102 | guild, 103 | true, 104 | _interactionModules.ToArray() 105 | ); 106 | foreach (var module in _interactionModules) 107 | { 108 | Console.WriteLine($"Module: {module.Name}"); 109 | foreach (var command in module.SlashCommands) 110 | { 111 | Console.WriteLine($"Command: {command.Name}"); 112 | } 113 | } 114 | foreach (var res in result) 115 | { 116 | Console.WriteLine(res.Name); 117 | } 118 | return; 119 | } 120 | await _interactions.AddModulesGloballyAsync(true, _interactionModules.ToArray()); 121 | } 122 | 123 | 124 | async Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionContext arg2, Discord.Interactions.IResult arg3) 125 | { 126 | if (!arg3.IsSuccess) 127 | { 128 | switch (arg3.Error) 129 | { 130 | case InteractionCommandError.UnmetPrecondition: 131 | await arg2.Interaction.RespondAsync($"Unmet Precondition: {arg3.ErrorReason}", ephemeral: true); 132 | break; 133 | case InteractionCommandError.UnknownCommand: 134 | await arg2.Interaction.RespondAsync("Unknown command", ephemeral: true); 135 | break; 136 | case InteractionCommandError.BadArgs: 137 | await arg2.Interaction.RespondAsync("Invalid number or arguments", ephemeral: true); 138 | break; 139 | case InteractionCommandError.Exception: 140 | Console.WriteLine("Command Error:"); 141 | Console.WriteLine(arg3.ErrorReason); 142 | await arg2.Interaction.RespondAsync($"Command exception: {arg3.ErrorReason}. If this message persists, please let us know in the support server (https://discord.gg/xyzMyJH) !", ephemeral: true); 143 | break; 144 | case InteractionCommandError.Unsuccessful: 145 | await arg2.Interaction.RespondAsync("Command could not be executed", ephemeral: true); 146 | break; 147 | default: 148 | break; 149 | } 150 | } 151 | } 152 | 153 | private async Task SendNotificaiton(ShardedCommandContext context) 154 | { 155 | try { 156 | var response = NotificationText(context.Message.Content); 157 | await context.User.SendMessageAsync(embed: response); 158 | } catch (Discord.Net.HttpException e) { 159 | Console.WriteLine("unable to alert user of deprication"); 160 | } 161 | } 162 | 163 | private Embed NotificationText(string message) { 164 | if (message.StartsWith("send") || message.StartsWith("!volt send")) { 165 | return Views.Info.UpgradeNotificationWithSendInfo.Response().Item2; 166 | } 167 | return Views.Info.UpgradeNotification.Response().Item2; 168 | } 169 | 170 | private async Task HandleCommandAsync(SocketMessage messageParam) 171 | { 172 | // Don't process the command if it was a System Message 173 | var message = messageParam as SocketUserMessage; 174 | if (message == null) return; 175 | //var context = new ShardedCommandContext(_client, message); 176 | var context = new ShardedCommandContext(_client, message); 177 | 178 | // Create a number to track where the prefix ends and the command begins 179 | var prefix = $"!volt "; 180 | int argPos = prefix.Length - 1; 181 | 182 | // short circut DMs 183 | if (context.IsPrivate && !context.User.IsBot && !(message.HasStringPrefix(prefix, ref argPos))) 184 | { 185 | if (!message.Content.StartsWith('/')) { 186 | await SendNotificaiton(context); 187 | } 188 | await SendCommandAsync(context, 0); 189 | Console.WriteLine("processed message!"); 190 | return; 191 | } 192 | 193 | // Determine if the message is a command, based on if it starts with '!' or a mention prefix 194 | if (!(message.HasStringPrefix(prefix, ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; 195 | // quick logging 196 | Console.WriteLine("processed message!"); 197 | 198 | // Execute the command. (result does not indicate a return value, 199 | // rather an object stating if the command executed successfully) 200 | await SendNotificaiton(context); 201 | await SendCommandAsync(context, argPos); 202 | } 203 | 204 | private async Task HandleInteraction (SocketInteraction arg) 205 | { 206 | try 207 | { 208 | // Create an execution context that matches the generic type parameter of your InteractionModuleBase modules 209 | var ctx = new ShardedInteractionContext(_client, arg); 210 | await _interactions.ExecuteCommandAsync(ctx, _services); 211 | Console.WriteLine("processed interaction!"); 212 | } 213 | catch (Exception ex) 214 | { 215 | Console.WriteLine("Caught exception:"); 216 | Console.WriteLine(ex); 217 | 218 | // If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original 219 | // response, or at least let the user know that something went wrong during the command execution. 220 | if(arg.Type == InteractionType.ApplicationCommand) 221 | await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync()); 222 | } 223 | } 224 | 225 | private async Task HandleReaction(Cacheable cache, Cacheable channelCache, SocketReaction reaction) 226 | { 227 | if (reaction.Emote.Name != Controllers.Messages.Send.DeleteEmote) 228 | { 229 | return; 230 | } 231 | 232 | try 233 | { 234 | var message = await cache.GetOrDownloadAsync(); 235 | if (!message.Reactions[reaction.Emote].IsMe || message.Reactions[reaction.Emote].ReactionCount == 1) 236 | { 237 | return; 238 | } 239 | Console.WriteLine("processed reaction!"); 240 | await message.DeleteAsync(); 241 | } 242 | catch (Exception e) 243 | { 244 | await channelCache.Value.SendMessageAsync("Error deleting message. Does the bot have needed permission?"); 245 | } 246 | return; 247 | } 248 | 249 | 250 | private async Task SendCommandAsync(ShardedCommandContext context, int argPos) 251 | { 252 | var result = await _commands.ExecuteAsync(context, argPos, _services); 253 | if (!result.IsSuccess) 254 | await Controllers.Messages.Send.SendErrorWithDeleteReaction(new CommandBasedContext(context), result.ErrorReason); 255 | } 256 | 257 | private Task Log(LogMessage msg) 258 | { 259 | Console.WriteLine(msg.ToString()); 260 | return Task.FromResult(0); 261 | } 262 | } 263 | } 264 | --------------------------------------------------------------------------------