Committing in-progress implementation of ban petitions

This commit is contained in:
Noikoio 2017-12-05 14:23:01 -08:00
parent 1d24e2d839
commit 4ef860790c
2 changed files with 139 additions and 9 deletions

View file

@ -8,7 +8,6 @@ using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.ModTools.Commands namespace Noikoio.RegexBot.Module.ModTools.Commands
{ {
class BanKick : CommandBase class BanKick : CommandBase
{ {
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class. // Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
@ -20,6 +19,11 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
private readonly string _successMsg; private readonly string _successMsg;
private readonly string _notifyMsg; private readonly string _notifyMsg;
const string DefaultMsg = "You have been {0} from $s for the following reason:\n$r";
const string DefaultMsgBanAppend = "\n\nIf the moderators have allowed it, you may petition your ban by" +
" submitting **one** message to the moderation team. To do so, reply to this message with" +
" `!petition [Your message here]`.";
// Configuration: // Configuration:
// "forcereason" - boolean; Force a reason to be given. Defaults to false. // "forcereason" - boolean; Force a reason to be given. Defaults to false.
// "purgedays" - integer; Number of days of target's post history to delete, if banning. // "purgedays" - integer; Number of days of target's post history to delete, if banning.
@ -41,8 +45,8 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
if (conf["notifymsg"] == null) if (conf["notifymsg"] == null)
{ {
// Message not specified - use default // Message not specified - use default
string act = _mode == CommandMode.Ban ? "banned" : "kicked"; _notifyMsg = string.Format(DefaultMsg, mode == CommandMode.Ban ? "banned" : "kicked");
_notifyMsg = $"You have been {act} from $s for the following reason:\n$r"; if (_mode == CommandMode.Ban) _notifyMsg += DefaultMsgBanAppend;
} }
else else
{ {

View file

@ -25,13 +25,26 @@ namespace Noikoio.RegexBot.Module.ModTools
private async Task Client_MessageReceived(SocketMessage arg) private async Task Client_MessageReceived(SocketMessage arg)
{ {
await CommandCheckInvoke(arg); if (arg.Channel is IDMChannel) await PetitionRelayCheck(arg);
else if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg);
} }
#region Config
[ConfigSection("modtools")] [ConfigSection("modtools")]
public override async Task<object> ProcessConfiguration(JToken configSection) public override async Task<object> ProcessConfiguration(JToken configSection)
{ {
// TODO: put command definitions elsewhere, not in root of this config if (configSection.Type != JTokenType.Object)
{
throw new RuleImportException("Configuration for this section is invalid.");
}
// BIG TO DO LIST:
/*
* 1. Have commands go into their own space within modtools. Candidate name: "banappeal"
* 2. Add a property for where to put the petition channel
* 3. Within ban cmd load, have it check for the existence of a petition channel... if possible?
* I guess otherwise silently discard, if the info isn't readily available. I don't know.
*/
var commands = new Dictionary<string, CommandBase>(StringComparer.OrdinalIgnoreCase); var commands = new Dictionary<string, CommandBase>(StringComparer.OrdinalIgnoreCase);
@ -50,13 +63,24 @@ namespace Noikoio.RegexBot.Module.ModTools
return new ReadOnlyDictionary<string, CommandBase>(commands); return new ReadOnlyDictionary<string, CommandBase>(commands);
} }
/*
* Config is stored in a tuple. I admit, not the best choice...
* Consider a different approach if more data needs to be stored in the future.
* Item 1: Command config (Dictionary<string, CommandBase>)
* Item 2: Ban petition channel (EntityName)
*/
private new Tuple<Dictionary<string, CommandBase>, EntityName> GetConfig(ulong guildId)
=> (Tuple<Dictionary<string, CommandBase>, EntityName>)base.GetConfig(guildId);
private Dictionary<string, CommandBase> GetCommandConfig(ulong guild) => GetConfig(guild).Item1;
private EntityName GetPetitionConfig(ulong guild) => GetConfig(guild).Item2;
#endregion
public new Task Log(string text) => base.Log(text); public new Task Log(string text) => base.Log(text);
private async Task CommandCheckInvoke(SocketMessage arg) private async Task CommandCheckInvoke(SocketMessage arg)
{ {
// Disregard if not in a guild SocketGuild g = ((SocketGuildUser)arg.Author).Guild;
SocketGuild g = (arg.Author as SocketGuildUser)?.Guild;
if (g == null) return;
// Get guild config // Get guild config
ServerConfig sc = RegexBot.Config.Servers.FirstOrDefault(s => s.Id == g.Id); ServerConfig sc = RegexBot.Config.Servers.FirstOrDefault(s => s.Id == g.Id);
@ -73,7 +97,7 @@ namespace Noikoio.RegexBot.Module.ModTools
int spc = arg.Content.IndexOf(' '); int spc = arg.Content.IndexOf(' ');
if (spc != -1) cmdchk = arg.Content.Substring(0, spc); if (spc != -1) cmdchk = arg.Content.Substring(0, spc);
else cmdchk = arg.Content; else cmdchk = arg.Content;
if (((IDictionary<string, CommandBase>)GetConfig(g.Id)).TryGetValue(cmdchk, out var c)) if ((GetCommandConfig(g.Id)).TryGetValue(cmdchk, out var c))
{ {
try try
{ {
@ -87,5 +111,107 @@ namespace Noikoio.RegexBot.Module.ModTools
} }
} }
} }
/// <summary>
/// List of available appeals. Key is user (for quick lookup). Value is guild (for quick config resolution).
/// TODO expiration?
/// </summary>
private Dictionary<ulong, ulong> _openPetitions; // Key: user, Value: guild
public void AddPetition(ulong guild, ulong user)
{
lock (_openPetitions) _openPetitions[user] = guild;
}
private async Task PetitionRelayCheck(SocketMessage msg)
{
const string PetitionAccepted = "Your petition has been forwarded to the moderators for review.";
const string PetitionDenied = "You may not submit a ban petition.";
if (!msg.Content.StartsWith("!petition ", StringComparison.InvariantCultureIgnoreCase)) return;
// Input validation
string ptext = msg.Content.Substring(10);
if (string.IsNullOrWhiteSpace(ptext))
{
// Just ignore.
return;
}
ulong targetGuild = 0;
lock (_openPetitions)
{
if (_openPetitions.TryGetValue(msg.Author.Id, out targetGuild))
{
_openPetitions.Remove(msg.Author.Id);
}
}
// It's possible the sender may still block messages sent to them,
// hence the empty catch blocks you'll see up ahead.
if (targetGuild == 0)
{
try { await msg.Author.SendMessageAsync(PetitionDenied); }
catch (Discord.Net.HttpException) { }
return;
}
var gObj = Client.GetGuild(targetGuild);
if (gObj == null)
{
// Guild is missing. No longer in guild?
try { await msg.Author.SendMessageAsync(PetitionDenied); }
catch (Discord.Net.HttpException) { }
return;
}
// Get petition reporting target if not already known
var rch = GetPetitionConfig(targetGuild);
ISocketMessageChannel rchObj;
if (!rch.Id.HasValue)
{
rchObj = gObj.TextChannels
.Where(c => c.Name.Equals(rch.Name, StringComparison.InvariantCultureIgnoreCase))
.FirstOrDefault();
}
else
{
rchObj = (ISocketMessageChannel)gObj.GetChannel(rch.Id.Value);
}
if (rchObj == null)
{
// Channel not found.
await Log("Petition reporting channel could not be resolved.");
try { await msg.Author.SendMessageAsync(PetitionDenied); }
catch (Discord.Net.HttpException) { }
return;
}
// Ready to relay as embed
try
{
await rchObj.SendMessageAsync("", embed: new EmbedBuilder()
{
Color = new Color(0x00FFD9),
Author = new EmbedAuthorBuilder()
{
Name = $"{msg.Author.ToString()} - Ban petition:",
IconUrl = msg.Author.GetAvatarUrl()
},
Description = ptext,
Timestamp = msg.Timestamp
});
}
catch (Discord.Net.HttpException ex)
{
await Log("Failed to relay petition message by " + msg.Author.ToString());
await Log(ex.Message);
// For the user's point of view, fail silently.
try { await msg.Author.SendMessageAsync(PetitionDenied); }
catch (Discord.Net.HttpException) { }
}
// Success. Notify user.
try { await msg.Author.SendMessageAsync(PetitionAccepted); }
catch (Discord.Net.HttpException) { }
}
} }
} }