Added .imageonlychannel / .imageonly to prevent users from posting anything but images in the channel

This commit is contained in:
Kwoth
2021-09-15 01:08:39 +02:00
parent 35d549f4e6
commit cccb37854c
14 changed files with 2951 additions and 15 deletions

View File

@@ -8,6 +8,8 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
- Added `.massban` to ban multiple people at once. 30 second cooldown - Added `.massban` to ban multiple people at once. 30 second cooldown
- Added `.youtubeuploadnotif` / `.yun` as a shortcut for subscribing to a youtube channel's rss feed - Added `.youtubeuploadnotif` / `.yun` as a shortcut for subscribing to a youtube channel's rss feed
- Added `.imageonlychannel` / `.imageonly` to prevent users from posting anything but images in the channel
- Fully translated to Spanish 🎉
### Changed ### Changed

View File

@@ -2,12 +2,10 @@
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using AngleSharp.Common;
using Discord.Commands; using Discord.Commands;
using NadekoBot.Common.Attributes; using NadekoBot.Common.Attributes;
using NadekoBot.Services; using NadekoBot.Services;
using NadekoBot.Modules; using NadekoBot.Modules;
using YamlDotNet.Serialization;
namespace NadekoBot.Tests namespace NadekoBot.Tests
{ {

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Services.Database.Models
{
public class ImageOnlyChannel : DbEntity
{
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
}
}

View File

@@ -2,7 +2,6 @@
namespace NadekoBot.Services.Database.Models namespace NadekoBot.Services.Database.Models
{ {
public class LogSetting : DbEntity public class LogSetting : DbEntity
{ {
public HashSet<IgnoredLogChannel> IgnoredChannels { get; set; } = new HashSet<IgnoredLogChannel>(); public HashSet<IgnoredLogChannel> IgnoredChannels { get; set; } = new HashSet<IgnoredLogChannel>();

View File

@@ -2,11 +2,9 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Design;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using NadekoBot.Services;
using System; using System;
using System.IO; using System.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NadekoBot.Common;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
namespace NadekoBot.Services.Database namespace NadekoBot.Services.Database
@@ -60,6 +58,7 @@ namespace NadekoBot.Services.Database
public DbSet<Repeater> Repeaters { get; set; } public DbSet<Repeater> Repeaters { get; set; }
public DbSet<Poll> Poll { get; set; } public DbSet<Poll> Poll { get; set; }
public DbSet<WaifuInfo> WaifuInfo { get; set; } public DbSet<WaifuInfo> WaifuInfo { get; set; }
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
public NadekoContext(DbContextOptions<NadekoContext> options) : base(options) public NadekoContext(DbContextOptions<NadekoContext> options) : base(options)
{ {
@@ -345,6 +344,10 @@ namespace NadekoBot.Services.Database
.IsUnique()); .IsUnique());
#endregion #endregion
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc
.HasIndex(x => x.ChannelId)
.IsUnique());
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations
{
public partial class imageonlychannels : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ImageOnlyChannels",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ImageOnlyChannels", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ImageOnlyChannels_ChannelId",
table: "ImageOnlyChannels",
column: "ChannelId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ImageOnlyChannels");
}
}
}

View File

@@ -187,6 +187,29 @@ namespace NadekoBot.Migrations
b.ToTable("FollowedStream"); b.ToTable("FollowedStream");
}); });
modelBuilder.Entity("NadekoBot.Services.Database.ImageOnlyChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId")
.IsUnique();
b.ToTable("ImageOnlyChannels");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View File

@@ -13,13 +13,31 @@ namespace NadekoBot.Modules.Administration
{ {
public partial class Administration : NadekoModule<AdministrationService> public partial class Administration : NadekoModule<AdministrationService>
{ {
private readonly ImageOnlyChannelService _imageOnly;
public Administration(ImageOnlyChannelService imageOnly)
{
_imageOnly = imageOnly;
}
public enum List public enum List
{ {
List = 0, List = 0,
Ls = 0 Ls = 0
} }
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.Administrator)]
public async Task ImageOnlyChannel(StoopidTime time = null)
{
var newValue = _imageOnly.ToggleImageOnlyChannel(ctx.Guild.Id, ctx.Channel.Id);
if (newValue)
await ReplyConfirmLocalizedAsync(strs.imageonly_enable);
else
await ReplyPendingLocalizedAsync(strs.imageonly_disable);
}
[NadekoCommand, Aliases] [NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]

View File

@@ -1,9 +1,7 @@
using System; using Discord;
using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Collections; using NadekoBot.Common.Collections;
using NadekoBot.Common.Replacements; using NadekoBot.Common.Replacements;
using NadekoBot.Services; using NadekoBot.Services;
@@ -24,14 +22,11 @@ namespace NadekoBot.Modules.Administration.Services
private readonly DbService _db; private readonly DbService _db;
private readonly ILogCommandService _logService; private readonly ILogCommandService _logService;
private readonly IEmbedBuilderService _eb;
public AdministrationService(Bot bot, CommandHandler cmdHandler, DbService db, public AdministrationService(Bot bot, CommandHandler cmdHandler, DbService db, ILogCommandService logService)
ILogCommandService logService, IEmbedBuilderService eb)
{ {
_db = db; _db = db;
_logService = logService; _logService = logService;
_eb = eb;
DeleteMessagesOnCommand = new ConcurrentHashSet<ulong>(bot.AllGuildConfigs DeleteMessagesOnCommand = new ConcurrentHashSet<ulong>(bot.AllGuildConfigs
.Where(g => g.DeleteMessageOnCommand) .Where(g => g.DeleteMessageOnCommand)

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Threading.Channels;
using System.Threading.Tasks;
using Discord;
using Discord.Net;
using Discord.WebSocket;
using LinqToDB;
using Microsoft.Extensions.Caching.Memory;
using NadekoBot.Common.Collections;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using NadekoBot.Services;
using Serilog;
namespace NadekoBot.Modules.Administration.Services
{
public sealed class ImageOnlyChannelService : IEarlyBehavior
{
private readonly IMemoryCache _ticketCache;
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _enabledOn;
private Channel<IUserMessage> _deleteQueue = Channel.CreateBounded<IUserMessage>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false,
});
public ImageOnlyChannelService(IMemoryCache ticketCache, DiscordSocketClient client, DbService db)
{
_ticketCache = ticketCache;
_client = client;
_db = db;
var uow = _db.GetDbContext();
_enabledOn = uow.ImageOnlyChannels
.ToList()
.GroupBy(x => x.GuildId)
.ToDictionary(x => x.Key, x => new ConcurrentHashSet<ulong>(x.Select(x => x.ChannelId)))
.ToConcurrent();
_ = Task.Run(DeleteQueueRunner);
_client.ChannelDestroyed += ClientOnChannelDestroyed;
}
private Task ClientOnChannelDestroyed(SocketChannel ch)
{
if (ch is not IGuildChannel gch)
return Task.CompletedTask;
if (_enabledOn.TryGetValue(gch.GuildId, out var channels) && channels.TryRemove(ch.Id))
ToggleImageOnlyChannel(gch.GuildId, ch.Id, true);
return Task.CompletedTask;
}
private async Task DeleteQueueRunner()
{
while (true)
{
var toDelete = await _deleteQueue.Reader.ReadAsync();
try
{
await toDelete.DeleteAsync();
await Task.Delay(1000);
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{
// disable if bot can't delete messages in the channel
ToggleImageOnlyChannel(((ITextChannel)toDelete.Channel).GuildId, toDelete.Channel.Id, true);
}
}
}
public bool ToggleImageOnlyChannel(ulong guildId, ulong channelId, bool forceDisable = false)
{
var newState = false;
using var uow = _db.GetDbContext();
if (forceDisable
|| (_enabledOn.TryGetValue(guildId, out var channels)
&& channels.TryRemove(channelId)))
{
uow.ImageOnlyChannels.Delete(x => x.ChannelId == channelId);
}
else
{
uow.ImageOnlyChannels.Add(new()
{
GuildId = guildId,
ChannelId = channelId
});
channels = _enabledOn.GetOrAdd(guildId, new ConcurrentHashSet<ulong>());
channels.Add(channelId);
newState = true;
}
uow.SaveChanges();
return newState;
}
public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
{
if (msg.Channel is not ITextChannel tch)
return false;
if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 }))
return false;
if (!_enabledOn.TryGetValue(tch.GuildId, out var chs)
|| !chs.Contains(msg.Channel.Id))
return false;
var user = await tch.Guild.GetUserAsync(msg.Author.Id)
?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id);
if (user is null)
return false;
// ignore owner and admin
if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator)
{
Log.Information("Image-Only: Ignoring owner od admin ({ChannelId})", msg.Channel.Id);
return false;
}
// ignore users higher in hierarchy
var botUser = await tch.Guild.GetCurrentUserAsync();
if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position))
return false;
// can't modify channel perms if not admin apparently
if (!botUser.GuildPermissions.ManageGuild)
{
ToggleImageOnlyChannel( tch.GuildId, tch.Id, true);;
return false;
}
var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id);
if (shouldLock)
{
await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny));
Log.Warning("Image-Only: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]",
msg.Author,
msg.Author.Id,
msg.Channel.Id);
}
try
{
await _deleteQueue.Writer.WriteAsync(msg);
}
catch (Exception ex)
{
Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}.",
msg.Id,
tch.Id);
}
return true;
}
private bool AddUserTicket(ulong guildId, ulong userId)
{
var old = _ticketCache.GetOrCreate($"{guildId}_{userId}", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
return 0;
});
_ticketCache.Set($"{guildId}_{userId}", ++old);
// if this is the third time that the user posts a
// non image in an image-only channel on this server
return old > 2;
}
public int Priority { get; } = 0;
}
}

View File

@@ -1255,3 +1255,7 @@ crsimport:
crsexport: crsexport:
- crsexport - crsexport
- exexport - exexport
imageonlychannel:
- imageonlychannel
- imageonly
- imagesonly

View File

@@ -2114,3 +2114,9 @@ nhentai:
args: args:
- "273426" - "273426"
- "cute girl" - "cute girl"
imageonlychannel:
desc: |-
Toggles whether the channel only allows images.
Users who send more than a few non-image messages will be banned from using the channel.
args:
- ""

View File

@@ -962,5 +962,7 @@
"empty_page": "This page is empty.", "empty_page": "This page is empty.",
"pages": "Pages", "pages": "Pages",
"favorites": "Favorites", "favorites": "Favorites",
"tags": "Tags" "tags": "Tags",
"imageonly_enable": "This channel is now image-only.",
"imageonly_disable": "This channel is no longer image-only."
} }