Skip to content

Commit

Permalink
Merge pull request #166 from zaanposni/155-PunishmentPreview
Browse files Browse the repository at this point in the history
  • Loading branch information
zaanposni authored Dec 4, 2020
2 parents df70f9e + cc7d6bc commit 29977eb
Show file tree
Hide file tree
Showing 45 changed files with 2,147 additions and 431 deletions.
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
<img src="https://img.shields.io/badge/contributions-welcome-lightgreen">
<img src="https://img.shields.io/github/contributors/zaanposni/discord-masz">
<a href="https://github.com/zaanposni/discord-masz/blob/master/LICENSE"><img src="https://img.shields.io/github/license/zaanposni/discord-masz.svg"/></a>
<img src="https://img.shields.io/badge/using-asp.net-blueviolet">
<img src="https://img.shields.io/badge/using-symfony-black">
<img src="https://img.shields.io/badge/using-docker-blue">
<img src="https://img.shields.io/badge/using-nginx-green">
<img src="https://img.shields.io/badge/using-mysql-orange">
</p>

MASZ is a management and moderation overview tool for **Discord Moderators** and **Admins**. <br/>
Keep track of all **moderation events** on your server, **search reliably** for entries and be one step ahead of trolls and rule breakers. <br/>
Keep track of all **moderation events** and **current punishments** on your server, **search reliably** for entries and be one step ahead of trolls and rule breakers. <br/>
The core of this tool are the **modcases**, a case represents a rule violation, an important note or similar. <br/>
The server members and your moderators can be **notified** individually about the creation. <br/>
The user for whom the case was created can also see it on the website, take a stand and your server is moderated **transparently**.
The user for whom the case was created can also see it on the website, take a stand and your server is moderated **transparently**. <br/>
This application can also **manage temporary punishments** just as temp mutes for a variable time you can define.

# Support and Discussion Server

Expand Down Expand Up @@ -43,7 +40,7 @@ Join our discord server for support or similar https://discord.gg/5zjpzw6h3S.

# Setup - Installation

The following guide assumes you want to deploy a production environment.
The following guide assumes you want to deploy a production environment using docker.

## Operation System

Expand All @@ -54,6 +51,7 @@ Since I use Docker you can use an operating system of your choice, but I recomme
- [docker](https://docs.docker.com/engine/install/ubuntu/) & [docker-compose](https://docs.docker.com/compose/)
- [jq](https://stedolan.github.io/jq/download/) - a bash tool for json
- a subdomain to host the application on
- a reverse proxy on your host

## Discord OAuth

Expand All @@ -67,6 +65,12 @@ https://yourdomain.com/
https://yourdomain.com/signin-discord
```

### Bot Intents

Enable **Server Members Intent** in your bot settings.

<img src="/docs/intents.png"/>

## Setup

- Download this repository `git clone https://github.com/zaanposni/discord-masz` ([zip link](https://codeload.github.com/zaanposni/discord-masz/zip/master))
Expand All @@ -83,12 +87,23 @@ https://yourdomain.com/signin-discord
- You can visit your application at `yourdomain.com`. You will see a login screen that will ask you to authenticate yourself using Discord OAuth2.
- After authorizing your service to use your Discord account you will see your profile picture in the top right corner of the index page.
- If you are logged in as a site admin you can use the "register guild" button to register your guilds and to get started. If you do not see the button please verify that your discord user id is in the `site_admins` list of your `config.json`
- Based on wanted features and functionalities you might have to grant your bot advanced permissions, read below for more info.

## Unban request feature

If you want banned users to see their cases, grant your bot the `ban people` permission. <br/>
This way they can see the reason for their ban and comment or send an unban request.

## Punishment feature

If you want the application to execute punishments like mutes and bans and manage them automatically (like unban after defined time on tempban), grant your bot the following permissions based on your needs:

```
Manage roles - for muted role
Kick people
Ban people
```

## Update

To install a new update of MASZ just use:
Expand Down Expand Up @@ -116,5 +131,5 @@ If you are using a local deployed backend you have to define `https://127.0.0.1:
# Contribute

Contributions are welcome. <br/>
If you are new to open source, checkout [this tutorial](https://github.com/firstcontributions/first-contributions).

If you are new to open source, checkout [this tutorial](https://github.com/firstcontributions/first-contributions). <br/>
Feel free to get in touch with me via our support server https://discord.gg/5zjpzw6h3S or via friend request on discord: **zaanposni#9295**.
2 changes: 2 additions & 0 deletions backend/masz/Controllers/api/v1/GuildConfigController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public async Task<IActionResult> CreateItem([FromBody] GuildConfigForCreateDto g
guildConfig.ModRoleId = guildConfigForCreateDto.ModRoleId;
guildConfig.AdminRoleId = guildConfigForCreateDto.AdminRoleId;
guildConfig.ModNotificationDM = guildConfigForCreateDto.ModNotificationDM;
guildConfig.MutedRoleId = guildConfigForCreateDto.MutedRoleId;
guildConfig.ModPublicNotificationWebhook = guildConfigForCreateDto.ModPublicNotificationWebhook;
guildConfig.ModInternalNotificationWebhook = guildConfigForCreateDto.ModInternalNotificationWebhook;

Expand Down Expand Up @@ -182,6 +183,7 @@ public async Task<IActionResult> UpdateSpecificItem([FromRoute] string guildid,

guildConfig.ModRoleId = newValue.ModRoleId;
guildConfig.AdminRoleId = newValue.AdminRoleId;
guildConfig.MutedRoleId = newValue.MutedRoleId;
guildConfig.ModNotificationDM = newValue.ModNotificationDM;
guildConfig.ModInternalNotificationWebhook = newValue.ModInternalNotificationWebhook;
guildConfig.ModPublicNotificationWebhook = newValue.ModPublicNotificationWebhook;
Expand Down
97 changes: 88 additions & 9 deletions backend/masz/Controllers/api/v1/ModCaseController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ public class ModCaseController : ControllerBase
private readonly IDiscordAnnouncer discordAnnouncer;
private readonly IDiscordAPIInterface discord;
private readonly IFilesHandler filesHandler;
private readonly IPunishmentHandler punishmentHandler;

public ModCaseController(ILogger<ModCaseController> logger, IDatabase database, IOptions<InternalConfig> config, IIdentityManager identityManager, IDiscordAPIInterface discordInterface, IDiscordAnnouncer modCaseAnnouncer, IFilesHandler filesHandler)
public ModCaseController(ILogger<ModCaseController> logger, IDatabase database, IOptions<InternalConfig> config, IIdentityManager identityManager, IDiscordAPIInterface discordInterface, IDiscordAnnouncer modCaseAnnouncer, IFilesHandler filesHandler, IPunishmentHandler punishmentHandler)
{
this.logger = logger;
this.database = database;
Expand All @@ -42,6 +43,7 @@ public ModCaseController(ILogger<ModCaseController> logger, IDatabase database,
this.discordAnnouncer = modCaseAnnouncer;
this.discord = discordInterface;
this.filesHandler = filesHandler;
this.punishmentHandler = punishmentHandler;
}

[HttpGet("{modcaseid}")]
Expand Down Expand Up @@ -87,7 +89,7 @@ public async Task<IActionResult> GetSpecificItem([FromRoute] string guildid, [Fr
}

[HttpDelete("{modcaseid}")]
public async Task<IActionResult> DeleteSpecificItem([FromRoute] string guildid, [FromRoute] string modcaseid, [FromQuery] bool sendNotification = true)
public async Task<IActionResult> DeleteSpecificItem([FromRoute] string guildid, [FromRoute] string modcaseid, [FromQuery] bool sendNotification = true, [FromQuery] bool handlePunishment = true)
{
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Incoming request.");
Identity currentIdentity = await identityManager.GetIdentity(HttpContext);
Expand Down Expand Up @@ -126,6 +128,17 @@ public async Task<IActionResult> DeleteSpecificItem([FromRoute] string guildid,
database.DeleteSpecificModCase(modCase);
await database.SaveChangesAsync();

if (handlePunishment)
{
try {
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Handling punishment.");
await punishmentHandler.UndoPunishment(modCase, database);
}
catch(Exception e){
logger.LogError(e, "Failed to handle punishment for modcase.");
}
}

logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Sending notification.");
try {
await discordAnnouncer.AnnounceModCase(modCase, RestAction.Deleted, sendNotification);
Expand All @@ -139,7 +152,7 @@ public async Task<IActionResult> DeleteSpecificItem([FromRoute] string guildid,
}

[HttpPut("{modcaseid}")]
public async Task<IActionResult> PutSpecificItem([FromRoute] string guildid, [FromRoute] string modcaseid, [FromBody] ModCaseForPutDto newValue, [FromQuery] bool sendNotification = true)
public async Task<IActionResult> PutSpecificItem([FromRoute] string guildid, [FromRoute] string modcaseid, [FromBody] ModCaseForPutDto newValue, [FromQuery] bool sendNotification = true, [FromQuery] bool handlePunishment = true)
{
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Incoming request.");
Identity currentIdentity = await identityManager.GetIdentity(HttpContext);
Expand All @@ -156,7 +169,8 @@ public async Task<IActionResult> PutSpecificItem([FromRoute] string guildid, [Fr
}
// ========================================================

if (await database.SelectSpecificGuildConfig(guildid) == null)
GuildConfig guildConfig = await database.SelectSpecificGuildConfig(guildid);
if (guildConfig == null)
{
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | 400 Guild not registered.");
return BadRequest("Guild not registered.");
Expand All @@ -178,6 +192,17 @@ public async Task<IActionResult> PutSpecificItem([FromRoute] string guildid, [Fr
modCase.Punishment = newValue.Punishment;
modCase.Labels = newValue.Labels.Distinct().ToArray();
modCase.Others = newValue.Others;
modCase.PunishmentType = newValue.PunishmentType;
modCase.PunishedUntil = newValue.PunishedUntil;
if (modCase.PunishmentType == PunishmentType.None) {
modCase.PunishedUntil = null;
modCase.PunishmentActive = false;
}
if (modCase.PunishedUntil == null) {
modCase.PunishmentActive = modCase.PunishmentType != PunishmentType.None && modCase.PunishmentType != PunishmentType.Kick;
} else {
modCase.PunishmentActive = modCase.PunishedUntil > DateTime.UtcNow && modCase.PunishmentType != PunishmentType.None && modCase.PunishmentType != PunishmentType.Kick;
}

modCase.Id = oldModCase.Id;
modCase.CaseId = oldModCase.CaseId;
Expand All @@ -204,13 +229,38 @@ public async Task<IActionResult> PutSpecificItem([FromRoute] string guildid, [Fr
var currentReportedMember = await discord.FetchMemberInfo(guildid, modCase.UserId);
if (currentReportedMember != null)
{
if (currentReportedMember.Roles.Contains(guildConfig.ModRoleId) || currentReportedMember.Roles.Contains(guildConfig.AdminRoleId)) {
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | 400 Cannot create cases for team members.");
return BadRequest("Cannot create cases for team members.");
}
modCase.Nickname = currentReportedMember.Nick; // update to new nickname if no member anymore leave old fetched nickname
}
}

database.UpdateModCase(modCase);
await database.SaveChangesAsync();

if (handlePunishment)
{
if ( oldModCase.UserId != modCase.UserId || oldModCase.PunishmentType != modCase.PunishmentType || oldModCase.PunishedUntil != modCase.PunishedUntil)
{
try {
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Handling punishment.");
await punishmentHandler.UndoPunishment(oldModCase, database);
if (modCase.PunishmentActive || (modCase.PunishmentType == PunishmentType.Kick && oldModCase.PunishmentType != PunishmentType.Kick))
{
if (modCase.PunishedUntil == null || modCase.PunishedUntil > DateTime.UtcNow)
{
await punishmentHandler.ExecutePunishment(modCase, database);
}
}
}
catch(Exception e){
logger.LogError(e, "Failed to handle punishment for modcase.");
}
}
}

logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Sending notification.");
try {
await discordAnnouncer.AnnounceModCase(modCase, RestAction.Edited, sendNotification);
Expand All @@ -224,7 +274,7 @@ public async Task<IActionResult> PutSpecificItem([FromRoute] string guildid, [Fr
}

[HttpPost]
public async Task<IActionResult> CreateItem([FromRoute] string guildid, [FromBody] ModCaseForCreateDto modCase, [FromQuery] bool sendNotification = true)
public async Task<IActionResult> CreateItem([FromRoute] string guildid, [FromBody] ModCaseForCreateDto modCase, [FromQuery] bool sendNotification = true, [FromQuery] bool handlePunishment = true)
{
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Incoming request.");
Identity currentIdentity = await identityManager.GetIdentity(HttpContext);
Expand All @@ -241,7 +291,8 @@ public async Task<IActionResult> CreateItem([FromRoute] string guildid, [FromBod
}
// ========================================================

if (await database.SelectSpecificGuildConfig(guildid) == null)
GuildConfig guildConfig = await database.SelectSpecificGuildConfig(guildid);
if (guildConfig == null)
{
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | 400 Guild not registered.");
return BadRequest("Guild not registered.");
Expand Down Expand Up @@ -272,6 +323,10 @@ public async Task<IActionResult> CreateItem([FromRoute] string guildid, [FromBod

if (currentReportedMember != null)
{
if (currentReportedMember.Roles.Contains(guildConfig.ModRoleId) || currentReportedMember.Roles.Contains(guildConfig.AdminRoleId)) {
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | 400 Cannot create cases for team members.");
return BadRequest("Cannot create cases for team members.");
}
newModCase.Nickname = currentReportedMember.Nick;
}

Expand All @@ -286,17 +341,42 @@ public async Task<IActionResult> CreateItem([FromRoute] string guildid, [FromBod
if (modCase.OccuredAt.HasValue)
newModCase.OccuredAt = modCase.OccuredAt.Value;
else
newModCase.OccuredAt = DateTime.UtcNow;
newModCase.LastEditedAt = DateTime.UtcNow;
newModCase.OccuredAt = newModCase.CreatedAt;
newModCase.LastEditedAt = newModCase.CreatedAt;
newModCase.LastEditedByModId = currentModerator;
newModCase.Punishment = modCase.Punishment;
newModCase.Labels = modCase.Labels.Distinct().ToArray();
newModCase.Others = modCase.Others;
newModCase.Valid = true;
newModCase.PunishmentType = modCase.PunishmentType;
newModCase.PunishedUntil = modCase.PunishedUntil;
if (modCase.PunishmentType == PunishmentType.None) {
modCase.PunishedUntil = null;
modCase.PunishmentActive = false;
}
if (modCase.PunishedUntil == null) {
newModCase.PunishmentActive = modCase.PunishmentType != PunishmentType.None && modCase.PunishmentType != PunishmentType.Kick;
} else {
newModCase.PunishmentActive = modCase.PunishedUntil > DateTime.UtcNow && modCase.PunishmentType != PunishmentType.None && modCase.PunishmentType != PunishmentType.Kick;
}

await database.SaveModCase(newModCase);
await database.SaveChangesAsync();

if (handlePunishment && (newModCase.PunishmentActive || newModCase.PunishmentType == PunishmentType.Kick))
{
if (newModCase.PunishedUntil == null || newModCase.PunishedUntil > DateTime.UtcNow)
{
try {
logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Handling punishment.");
await punishmentHandler.ExecutePunishment(newModCase, database);
}
catch(Exception e){
logger.LogError(e, "Failed to handle punishment for modcase.");
}
}
}

logger.LogInformation($"{HttpContext.Request.Method} {HttpContext.Request.Path} | Sending notification.");
try {
await discordAnnouncer.AnnounceModCase(newModCase, RestAction.Created, sendNotification);
Expand Down Expand Up @@ -345,7 +425,6 @@ public async Task<IActionResult> GetAllItems([FromRoute] string guildid)
return Ok(modCases);
}


[HttpGet("@me")]
public async Task<IActionResult> GetSpecificItem([FromRoute] string guildid)
{
Expand Down
8 changes: 5 additions & 3 deletions backend/masz/Controllers/api/v1/StatsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public async Task<IActionResult> GetGuildStats([FromRoute] string guildid, [From
List<GuildConfig> guildConfigs = await database.SelectAllGuildConfigs();
List<ModCase> modCases = await database.SelectAllModCases();
return Ok(new {
guild_config_count = guildConfigs.Count,
modcase_count = modCases.Count
guilds = guildConfigs.Count,
modCaseCount = modCases.Count
});
}

Expand All @@ -51,8 +51,10 @@ public async Task<IActionResult> GetGuildStats([FromRoute] string guildid)
}

List<ModCase> modCases = await database.SelectAllModCasesForGuild(guildid);
List<ModCase> activePunishments = await database.SelectAllModCasesWithActivePunishmentForGuild(guildid);
return Ok(new {
modcase_count = modCases.Count
modCaseCount = modCases.Count,
activePunishments = activePunishments.Count
});
}
}
Expand Down
6 changes: 6 additions & 0 deletions backend/masz/Dtos/ModCase/ModCaseForCreateDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using masz.Models;

namespace masz.Dtos.ModCase
{
Expand All @@ -19,9 +20,14 @@ public class ModCaseForCreateDto
public int Severity { get; set; }
[DataType(DataType.Date)]
public DateTime? OccuredAt { get; set; }
[Required(ErrorMessage = "Punishment field is required")]
[MaxLength(100)]
public string Punishment { get; set; }
public string[] Labels { get; set; } = new string[0];
public string Others { get; set; }
[Required(ErrorMessage = "PunishmentType field is required")]
public PunishmentType PunishmentType { get; set; }
public DateTime? PunishedUntil { get; set; }
public bool PunishmentActive { get; set; } = false;
}
}
Loading

0 comments on commit 29977eb

Please sign in to comment.