mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Added .imageonlychannel / .imageonly to prevent users from posting anything but images in the channel
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
{
|
{
|
||||||
|
8
src/NadekoBot/Db/Models/ImageOnlyChannel.cs
Normal file
8
src/NadekoBot/Db/Models/ImageOnlyChannel.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NadekoBot.Services.Database.Models
|
||||||
|
{
|
||||||
|
public class ImageOnlyChannel : DbEntity
|
||||||
|
{
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
public ulong ChannelId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -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>();
|
||||||
|
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2653
src/NadekoBot/Migrations/20210914180026_image-only-channels.Designer.cs
generated
Normal file
2653
src/NadekoBot/Migrations/20210914180026_image-only-channels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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")
|
||||||
|
@@ -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)]
|
||||||
|
@@ -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)
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1254,4 +1254,8 @@ crsimport:
|
|||||||
- eximport
|
- eximport
|
||||||
crsexport:
|
crsexport:
|
||||||
- crsexport
|
- crsexport
|
||||||
- exexport
|
- exexport
|
||||||
|
imageonlychannel:
|
||||||
|
- imageonlychannel
|
||||||
|
- imageonly
|
||||||
|
- imagesonly
|
||||||
|
@@ -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:
|
||||||
|
- ""
|
@@ -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."
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user