Added .honeypot command
This commit is contained in:
parent
6b0d784454
commit
c5c307b440
19 changed files with 18183 additions and 7328 deletions
|
@ -59,6 +59,7 @@ public abstract class EllieContext : DbContext
|
||||||
|
|
||||||
public DbSet<TodoModel> Todos { get; set; }
|
public DbSet<TodoModel> Todos { get; set; }
|
||||||
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
||||||
|
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
|
||||||
|
|
||||||
// todo add guild colors
|
// todo add guild colors
|
||||||
// public DbSet<GuildColors> GuildColors { get; set; }
|
// public DbSet<GuildColors> GuildColors { get; set; }
|
||||||
|
|
11
src/EllieBot/Db/Models/HoneypotChannel.cs
Normal file
11
src/EllieBot/Db/Models/HoneypotChannel.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace EllieBot.Db.Models;
|
||||||
|
|
||||||
|
public class HoneypotChannel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
|
||||||
|
public ulong ChannelId { get; set; }
|
||||||
|
}
|
3803
src/EllieBot/Migrations/Mysql/20240627033532_honeypot.Designer.cs
generated
Normal file
3803
src/EllieBot/Migrations/Mysql/20240627033532_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
36
src/EllieBot/Migrations/Mysql/20240627033532_honeypot.cs
Normal file
36
src/EllieBot/Migrations/Mysql/20240627033532_honeypot.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EllieBot.Migrations.Mysql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class honeypot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "honeypotchannels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_honeypotchannels", x => x.guildid);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "honeypotchannels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
3798
src/EllieBot/Migrations/PostgreSql/20240627033522_honeypot.Designer.cs
generated
Normal file
3798
src/EllieBot/Migrations/PostgreSql/20240627033522_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,33 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EllieBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class honeypot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "honeypotchannels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_honeypotchannels", x => x.guildid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "honeypotchannels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
2935
src/EllieBot/Migrations/Sqlite/20240627033508_honeypot.Designer.cs
generated
Normal file
2935
src/EllieBot/Migrations/Sqlite/20240627033508_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
34
src/EllieBot/Migrations/Sqlite/20240627033508_honeypot.cs
Normal file
34
src/EllieBot/Migrations/Sqlite/20240627033508_honeypot.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace EllieBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class honeypot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "HoneyPotChannels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_HoneyPotChannels", x => x.GuildId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "HoneyPotChannels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,94 @@
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using EllieBot.Common.ModuleBehaviors;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoCommand, IEService
|
||||||
|
{
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly CommandHandler _handler;
|
||||||
|
|
||||||
|
private ConcurrentHashSet<ulong> _channels = new();
|
||||||
|
|
||||||
|
private Channel<SocketGuildUser> _punishments = Channel.CreateBounded<SocketGuildUser>(
|
||||||
|
new BoundedChannelOptions(100)
|
||||||
|
{
|
||||||
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
public HoneyPotService(DbService db, CommandHandler handler)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var deleted = await uow.HoneyPotChannels
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.DeleteWithOutputAsync();
|
||||||
|
|
||||||
|
if (deleted.Length > 0)
|
||||||
|
{
|
||||||
|
_channels.TryRemove(deleted[0].ChannelId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.HoneyPotChannels
|
||||||
|
.ToLinqToDBTable()
|
||||||
|
.InsertAsync(() => new HoneypotChannel
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
ChannelId = channelId
|
||||||
|
});
|
||||||
|
|
||||||
|
_channels.Add(channelId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var channels = await uow.HoneyPotChannels
|
||||||
|
.Select(x => x.ChannelId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
_channels = new(channels);
|
||||||
|
|
||||||
|
while (await _punishments.Reader.WaitToReadAsync())
|
||||||
|
{
|
||||||
|
while (_punishments.Reader.TryRead(out var user))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id);
|
||||||
|
await user.BanAsync();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Warning(e, "Failed banning {User} due to {Error}", user, e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
||||||
|
{
|
||||||
|
if (_channels.Contains(msg.Channel.Id) && msg.Author is SocketGuildUser sgu)
|
||||||
|
{
|
||||||
|
if (!sgu.GuildPermissions.BanMembers)
|
||||||
|
await _punishments.Writer.WriteAsync(sgu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using EllieBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class HoneypotCommands : EllieModule
|
||||||
|
{
|
||||||
|
private readonly IHoneyPotService _service;
|
||||||
|
|
||||||
|
public HoneypotCommands(IHoneyPotService service)
|
||||||
|
=> _service = service;
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[RequireUserPermission(GuildPermission.Administrator)]
|
||||||
|
[RequireBotPermission(GuildPermission.BanMembers)]
|
||||||
|
public async Task Honeypot()
|
||||||
|
{
|
||||||
|
var enabled = await _service.ToggleHoneypotChannel(ctx.Guild.Id, ctx.Channel.Id);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response().Confirm(strs.honeypot_on).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.honeypot_off).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace EllieBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
public interface IHoneyPotService
|
||||||
|
{
|
||||||
|
public Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId);
|
||||||
|
}
|
|
@ -17,7 +17,8 @@ public partial class Gambling
|
||||||
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
|
|
||||||
public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs)
|
public DrawCommands(IImageCache images, GamblingConfigService gcs)
|
||||||
|
: base(gcs)
|
||||||
=> _images = images;
|
=> _images = images;
|
||||||
|
|
||||||
private async Task InternalDraw(int count, ulong? guildId = null)
|
private async Task InternalDraw(int count, ulong? guildId = null)
|
||||||
|
@ -56,8 +57,8 @@ public partial class Gambling
|
||||||
i.Dispose();
|
i.Dispose();
|
||||||
|
|
||||||
var eb = _sender.CreateEmbed()
|
var eb = _sender.CreateEmbed()
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
var toSend = string.Empty;
|
var toSend = string.Empty;
|
||||||
if (cardObjects.Count == 5)
|
if (cardObjects.Count == 5)
|
||||||
eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true);
|
eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true);
|
||||||
|
@ -71,7 +72,7 @@ public partial class Gambling
|
||||||
|
|
||||||
if (count > 1)
|
if (count > 1)
|
||||||
eb.AddField(GetText(strs.cards), count.ToString(), true);
|
eb.AddField(GetText(strs.cards), count.ToString(), true);
|
||||||
|
|
||||||
await using var imageStream = await img.ToStreamAsync();
|
await using var imageStream = await img.ToStreamAsync();
|
||||||
await ctx.Channel.SendFileAsync(imageStream,
|
await ctx.Channel.SendFileAsync(imageStream,
|
||||||
imgName,
|
imgName,
|
||||||
|
@ -84,7 +85,7 @@ public partial class Gambling
|
||||||
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
|
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
|
||||||
return Image.Load<Rgba32>(cardBytes);
|
return Image.Load<Rgba32>(cardBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Image<Rgba32>> GetCardImageAsync(Deck.Card currentCard)
|
private async Task<Image<Rgba32>> GetCardImageAsync(Deck.Card currentCard)
|
||||||
{
|
{
|
||||||
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
||||||
|
@ -98,7 +99,7 @@ public partial class Gambling
|
||||||
{
|
{
|
||||||
if (num < 1)
|
if (num < 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (num > 10)
|
if (num > 10)
|
||||||
num = 10;
|
num = 10;
|
||||||
|
|
||||||
|
@ -110,7 +111,7 @@ public partial class Gambling
|
||||||
{
|
{
|
||||||
if (num < 1)
|
if (num < 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (num > 10)
|
if (num > 10)
|
||||||
num = 10;
|
num = 10;
|
||||||
|
|
||||||
|
@ -136,19 +137,29 @@ public partial class Gambling
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputValueGuess val, InputColorGuess? col = null)
|
public Task BetDraw(
|
||||||
|
[OverrideTypeReader(typeof(BalanceTypeReader))]
|
||||||
|
long amount,
|
||||||
|
InputValueGuess val,
|
||||||
|
InputColorGuess? col = null)
|
||||||
=> BetDrawInternal(amount, val, col);
|
=> BetDrawInternal(amount, val, col);
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputColorGuess col, InputValueGuess? val = null)
|
public Task BetDraw(
|
||||||
|
[OverrideTypeReader(typeof(BalanceTypeReader))]
|
||||||
|
long amount,
|
||||||
|
InputColorGuess col,
|
||||||
|
InputValueGuess? val = null)
|
||||||
=> BetDrawInternal(amount, val, col);
|
=> BetDrawInternal(amount, val, col);
|
||||||
|
|
||||||
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
|
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
|
||||||
{
|
{
|
||||||
if (amount <= 0)
|
if (!await CheckBetMandatory(amount))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var res = await _service.BetDrawAsync(ctx.User.Id,
|
var res = await _service.BetDrawAsync(ctx.User.Id,
|
||||||
amount,
|
amount,
|
||||||
(byte?)val,
|
(byte?)val,
|
||||||
|
@ -161,13 +172,13 @@ public partial class Gambling
|
||||||
}
|
}
|
||||||
|
|
||||||
var eb = _sender.CreateEmbed()
|
var eb = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(ctx.User)
|
.WithAuthor(ctx.User)
|
||||||
.WithDescription(result.Card.GetEmoji())
|
.WithDescription(result.Card.GetEmoji())
|
||||||
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
|
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
|
||||||
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
|
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
|
||||||
.AddField(GetText(strs.won), N((long)result.Won), false)
|
.AddField(GetText(strs.won), N((long)result.Won), false)
|
||||||
.WithImageUrl("attachment://card.png");
|
.WithImageUrl("attachment://card.png");
|
||||||
|
|
||||||
using var img = await GetCardImageAsync(result.Card);
|
using var img = await GetCardImageAsync(result.Card);
|
||||||
await using var imgStream = await img.ToStreamAsync();
|
await using var imgStream = await img.ToStreamAsync();
|
||||||
|
@ -189,9 +200,10 @@ public partial class Gambling
|
||||||
InputColorGuess.Black => "B ⚫",
|
InputColorGuess.Black => "B ⚫",
|
||||||
_ => "❓"
|
_ => "❓"
|
||||||
};
|
};
|
||||||
|
|
||||||
return $"{val} / {col}";
|
return $"{val} / {col}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCardInfo(RegularCard card)
|
private string GetCardInfo(RegularCard card)
|
||||||
{
|
{
|
||||||
var val = (int)card.Value switch
|
var val = (int)card.Value switch
|
||||||
|
@ -208,7 +220,7 @@ public partial class Gambling
|
||||||
RegularSuit.Diamonds or RegularSuit.Hearts => "R 🔴",
|
RegularSuit.Diamonds or RegularSuit.Hearts => "R 🔴",
|
||||||
_ => "B ⚫"
|
_ => "B ⚫"
|
||||||
};
|
};
|
||||||
|
|
||||||
return $"{val} / {col}";
|
return $"{val} / {col}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ public partial class ResponseBuilder
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private int currentPage;
|
private int currentPage;
|
||||||
|
|
||||||
private EllieButtonInteractionHandler left;
|
private EllieButtonInteractionHandler? left;
|
||||||
private EllieButtonInteractionHandler right;
|
private EllieButtonInteractionHandler? right;
|
||||||
private EllieInteractionBase? extra;
|
private EllieInteractionBase? extra;
|
||||||
|
|
||||||
public PaginationSender(
|
public PaginationSender(
|
||||||
|
@ -120,8 +120,8 @@ public partial class ResponseBuilder
|
||||||
if (_paginationBuilder.AddPaginatedFooter)
|
if (_paginationBuilder.AddPaginatedFooter)
|
||||||
toSend.AddPaginatedFooter(currentPage, lastPage);
|
toSend.AddPaginatedFooter(currentPage, lastPage);
|
||||||
|
|
||||||
left.SetCompleted();
|
left?.SetCompleted();
|
||||||
right.SetCompleted();
|
right?.SetCompleted();
|
||||||
extra?.SetCompleted();
|
extra?.SetCompleted();
|
||||||
(left, extra, right) = (await GetInteractions());
|
(left, extra, right) = (await GetInteractions());
|
||||||
|
|
||||||
|
|
|
@ -1400,4 +1400,6 @@ stickyroles:
|
||||||
cleanupguilddata:
|
cleanupguilddata:
|
||||||
- cleanupguilddata
|
- cleanupguilddata
|
||||||
prompt:
|
prompt:
|
||||||
- prompt
|
- prompt
|
||||||
|
honeypot:
|
||||||
|
- honeypot
|
|
@ -4522,4 +4522,13 @@ prompt:
|
||||||
- What's the weather like today?
|
- What's the weather like today?
|
||||||
params:
|
params:
|
||||||
- query:
|
- query:
|
||||||
desc: "The message to send to the bot."
|
desc: "The message to send to the bot."
|
||||||
|
honeypot:
|
||||||
|
desc: |-
|
||||||
|
Toggles honeypot on the current channel.
|
||||||
|
Anyone sending a message in this channel will be soft banned. (Banned and then unbanned)
|
||||||
|
This is useful for automatically getting rid of spam bots.
|
||||||
|
ex:
|
||||||
|
- ''
|
||||||
|
params:
|
||||||
|
- {}
|
|
@ -1101,5 +1101,7 @@
|
||||||
"todo_archived_list": "Archived Todo List",
|
"todo_archived_list": "Archived Todo List",
|
||||||
"search_results": "Search results",
|
"search_results": "Search results",
|
||||||
"queue_search_results": "Type the number of the search result to queue up that track.",
|
"queue_search_results": "Type the number of the search result to queue up that track.",
|
||||||
"overloads": "Overloads"
|
"overloads": "Overloads",
|
||||||
|
"honeypot_on": "Honeypot enabled on this channel.",
|
||||||
|
"honeypot_off": "Honeypot disabled."
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue