This commit is contained in:
Toastie (DCS Team) 2024-06-29 15:39:59 +12:00
commit 151c3d5a6a
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
36 changed files with 18322 additions and 7524 deletions

View file

@ -2,15 +2,24 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.1.1] - 29.06.2024
### Added
- Added `.honeypot` command, which automatically softbans (ban and immediate unban) any user who posts in that channel.
- Useful to auto softban bots who spam every channel upon joining
- Users who run commands or expressions won't be softbanned.
- Users who have ban member permissions are also excluded.
### Fixed
- Fixed `.betdraw` not respecting maxbet
- Fixed `'xpshop` pagination for real this time?
## [5.1.0] - 28.06.2024 ## [5.1.0] - 28.06.2024
### Added ### Added
- Added `'prompt` command, Ellie Ai Assistant
- You can send natural language questions, queries or execute commands. For example "@Ellie how's the weather in paris" and it will return `'we Paris` and run it for you.
- In case the bot can't execute a command using your query, It will fall back to your chatter bot, in case you have it enabled in data/games.yml. (Cleverbot or chatgpt)
- (It's far from perfect so please don't ask the bot to do dangerous things like banning or pruning)
- Requires Patreon subscription, after which you'll be able to run it on global @Ellie bot. If you're selfhosting, you also will need to acquire the api key from <https://dashy.elliebot.net/api> (coming soon(ish)...)
- Added support for `gpt-4o` in `data/games.yml` - Added support for `gpt-4o` in `data/games.yml`
- Added EllieAiToken to `creds.yml` - Added EllieAiToken to `creds.yml`

View file

@ -93,7 +93,7 @@ public sealed class Bot : IBot
private void AddServices() private void AddServices()
{ {
var startingGuildIdList = GetCurrentGuildIds(); var startingGuildIdList = GetCurrentGuildIds();
var sw = Stopwatch.StartNew(); var startTime = Stopwatch.GetTimestamp();
var bot = Client.CurrentUser; var bot = Client.CurrentUser;
using (var uow = _db.GetDbContext()) using (var uow = _db.GetDbContext())
@ -161,8 +161,7 @@ public sealed class Bot : IBot
LoadTypeReaders(a); LoadTypeReaders(a);
} }
sw.Stop(); Log.Information("All services loaded in {ServiceLoadTime:F2}s", Stopwatch.GetElapsedTime(startTime).TotalSeconds);
Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
} }
private void LoadTypeReaders(Assembly assembly) private void LoadTypeReaders(Assembly assembly)
@ -259,7 +258,7 @@ public sealed class Bot : IBot
if (ShardId == 0) if (ShardId == 0)
await _db.SetupAsync(); await _db.SetupAsync();
var sw = Stopwatch.StartNew(); var startTime = Stopwatch.GetTimestamp();
await LoginAsync(_creds.Token); await LoginAsync(_creds.Token);
@ -274,8 +273,7 @@ public sealed class Bot : IBot
Helpers.ReadErrorAndExit(9); Helpers.ReadErrorAndExit(9);
} }
sw.Stop(); Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, Stopwatch.GetElapsedTime(startTime).TotalSeconds);
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
var commandHandler = Services.GetRequiredService<CommandHandler>(); var commandHandler = Services.GetRequiredService<CommandHandler>();
// start handling messages received in commandhandler // start handling messages received in commandhandler

View file

@ -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; }

View 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; }
}

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.0</Version> <Version>5.1.1</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -27,79 +27,79 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<Publish>True</Publish> <Publish>True</Publish>
</PackageReference> </PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/> <PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
<PackageReference Include="CommandLineParser" Version="2.9.1"/> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.204.0"/> <PackageReference Include="Discord.Net" Version="3.204.0" />
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246"/> <PackageReference Include="CoreCLR-NCalc" Version="3.1.246" />
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/> <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/> <PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" />
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/> <PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />--> <!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1"/> <PackageReference Include="Google.Protobuf" Version="3.26.1" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0"/> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0" />
<PackageReference Include="Grpc.Tools" Version="2.63.0"> <PackageReference Include="Grpc.Tools" Version="2.63.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="MorseCode.ITask" Version="2.0.3"/> <PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0"/> <PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<!-- DI --> <!-- DI -->
<!-- <PackageReference Include="Ninject" Version="3.3.6"/>--> <!-- <PackageReference Include="Ninject" Version="3.3.6"/>-->
<!-- <PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0"/>--> <!-- <PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0"/>-->
<!-- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />--> <!-- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />-->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.3"/> <PackageReference Include="DryIoc.dll" Version="5.4.3" />
<!-- <PackageReference Include="Scrutor" Version="4.2.0" />--> <!-- <PackageReference Include="Scrutor" Version="4.2.0" />-->
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2"/> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NonBlocking" Version="2.1.2"/> <PackageReference Include="NonBlocking" Version="2.1.2" />
<PackageReference Include="OneOf" Version="3.0.263"/> <PackageReference Include="OneOf" Version="3.0.263" />
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.263"/> <PackageReference Include="OneOf.SourceGenerator" Version="3.0.263" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/> <PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17"/> <PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.8" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/> <PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
<PackageReference Include="StackExchange.Redis" Version="2.7.33"/> <PackageReference Include="StackExchange.Redis" Version="2.8.0" />
<PackageReference Include="YamlDotNet" Version="15.1.4"/> <PackageReference Include="YamlDotNet" Version="15.1.4" />
<PackageReference Include="SharpToken" Version="2.0.3" /> <PackageReference Include="SharpToken" Version="2.0.3" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<!-- Db-related packages --> <!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/> <PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2"/> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<!-- Used by stream notifications --> <!-- Used by stream notifications -->
<PackageReference Include="TwitchLib.Api" Version="3.4.1"/> <PackageReference Include="TwitchLib.Api" Version="3.4.1" />
<!-- sqlselectcsv and stock --> <!-- sqlselectcsv and stock -->
<PackageReference Include="CsvHelper" Version="32.0.3"/> <PackageReference Include="CsvHelper" Version="32.0.3" />
</ItemGroup> </ItemGroup>

File diff suppressed because it is too large Load diff

View 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");
}
}
}

View file

@ -1388,6 +1388,25 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("guildconfigs", (string)null); b.ToTable("guildconfigs", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.HoneypotChannel", b =>
{
b.Property<ulong>("GuildId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint unsigned")
.HasColumnName("guildid");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<ulong>("GuildId"));
b.Property<ulong>("ChannelId")
.HasColumnType("bigint unsigned")
.HasColumnName("channelid");
b.HasKey("GuildId")
.HasName("pk_honeypotchannels");
b.ToTable("honeypotchannels", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b => modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

File diff suppressed because it is too large Load diff

View file

@ -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");
}
}
}

View file

@ -1387,6 +1387,23 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("guildconfigs", (string)null); b.ToTable("guildconfigs", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.HoneypotChannel", b =>
{
b.Property<decimal>("GuildId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.HasKey("GuildId")
.HasName("pk_honeypotchannels");
b.ToTable("honeypotchannels", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b => modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

File diff suppressed because it is too large Load diff

View 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");
}
}
}

View file

@ -1033,6 +1033,20 @@ namespace EllieBot.Migrations
b.ToTable("GuildConfigs"); b.ToTable("GuildConfigs");
}); });
modelBuilder.Entity("EllieBot.Db.Models.HoneypotChannel", b =>
{
b.Property<ulong>("GuildId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.HasKey("GuildId");
b.ToTable("HoneyPotChannels");
});
modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b => modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -0,0 +1,6 @@
namespace EllieBot.Modules.Administration.Honeypot;
public interface IHoneyPotService
{
public Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId);
}

View file

@ -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)
@ -136,18 +137,28 @@ 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,
@ -192,6 +203,7 @@ public partial class Gambling
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

View file

@ -165,7 +165,7 @@ public class ChatterBotService : IExecOnMessage
} }
else else
{ {
Log.Warning("Error in chatterbot: {Error}", error); Log.Warning("Error in chatterbot: {Error}", error.Value);
} }
Log.Information(""" Log.Information("""

View file

@ -103,6 +103,8 @@ public class OfficialGptSession : IChatterBotSession
try try
{ {
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString); var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
Log.Information("Received response: {response}", dataString);
var res = response?.Choices?[0]; var res = response?.Choices?[0];
var message = res?.Message?.Content; var message = res?.Message?.Content;

View file

@ -32,20 +32,21 @@ public sealed class SearxSearchService : SearchServiceBase, IEService
var instanceUrl = GetRandomInstance(); var instanceUrl = GetRandomInstance();
Log.Information("Using {Instance} instance for web search...", instanceUrl); Log.Information("Using {Instance} instance for web search...", instanceUrl);
var sw = Stopwatch.StartNew(); var startTime = Stopwatch.GetTimestamp();
using var http = _http.CreateClient(); using var http = _http.CreateClient();
await using var res = await http.GetStreamAsync($"{instanceUrl}" await using var res = await http.GetStreamAsync($"{instanceUrl}"
+ $"?q={Uri.EscapeDataString(query)}" + $"?q={Uri.EscapeDataString(query)}"
+ $"&format=json" + $"&format=json"
+ $"&strict=2"); + $"&strict=2");
sw.Stop(); var elapsed = Stopwatch.GetElapsedTime(startTime);
var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res); var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
if (dat is null) if (dat is null)
return new SearxSearchResult(); return new SearxSearchResult();
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture); dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
return dat; return dat;
} }
@ -56,7 +57,7 @@ public sealed class SearxSearchService : SearchServiceBase, IEService
var instanceUrl = GetRandomInstance(); var instanceUrl = GetRandomInstance();
Log.Information("Using {Instance} instance for img search...", instanceUrl); Log.Information("Using {Instance} instance for img search...", instanceUrl);
var sw = Stopwatch.StartNew(); var startTime = Stopwatch.GetTimestamp();
using var http = _http.CreateClient(); using var http = _http.CreateClient();
await using var res = await http.GetStreamAsync($"{instanceUrl}" await using var res = await http.GetStreamAsync($"{instanceUrl}"
+ $"?q={Uri.EscapeDataString(query)}" + $"?q={Uri.EscapeDataString(query)}"
@ -64,13 +65,13 @@ public sealed class SearxSearchService : SearchServiceBase, IEService
+ $"&category_images=on" + $"&category_images=on"
+ $"&strict=2"); + $"&strict=2");
sw.Stop(); var elapsed = Stopwatch.GetElapsedTime(startTime);
var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res); var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
if (dat is null) if (dat is null)
return new SearxImageSearchResult(); return new SearxImageSearchResult();
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture); dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
return dat; return dat;
} }
} }

View file

@ -8,9 +8,9 @@ public sealed class CommandPromptResultModel
public required string Name { get; set; } public required string Name { get; set; }
[JsonPropertyName("arguments")] [JsonPropertyName("arguments")]
public required Dictionary<string, string> Arguments { get; set; } public Dictionary<string, string> Arguments { get; set; } = new();
[JsonPropertyName("remaining")] [JsonPropertyName("remaining")]
[JsonConverter(typeof(NumberToStringConverter))] [JsonConverter(typeof(NumberToStringConverter))]
public required string Remaining { get; set; } public string Remaining { get; set; } = string.Empty;
} }

View file

@ -480,7 +480,8 @@ public partial class Xp : EllieModule<XpService>
ctx.User.Id, ctx.User.Id,
button, button,
OnShopUse, OnShopUse,
(key, itemType)); (key, itemType),
clearAfter: false);
return inter; return inter;
} }
@ -494,7 +495,9 @@ public partial class Xp : EllieModule<XpService>
ctx.User.Id, ctx.User.Id,
button, button,
OnShopBuy, OnShopBuy,
(key, itemType)); (key, itemType),
singleUse: true,
clearAfter: false);
return inter; return inter;
} }
@ -577,6 +580,10 @@ public partial class Xp : EllieModule<XpService>
{ {
await Response().Error(strs.not_enough(_gss.GetCurrencySign())).SendAsync(); await Response().Error(strs.not_enough(_gss.GetCurrencySign())).SendAsync();
} }
else if (result == BuyResult.Success)
{
await _service.UseShopItemAsync(ctx.User.Id, type, key);
}
} }
private string GetNotifLocationString(XpNotificationLocation loc) private string GetNotifLocationString(XpNotificationLocation loc)

View file

@ -133,52 +133,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
File.WriteAllText(CREDS_FILE_NAME, ymlData); File.WriteAllText(CREDS_FILE_NAME, ymlData);
} }
private string OldCredsJsonPath
=> Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
private string OldCredsJsonBackupPath
=> Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak");
private void MigrateCredentials() private void MigrateCredentials()
{ {
if (File.Exists(OldCredsJsonPath))
{
Log.Information("Migrating old creds...");
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
var oldCreds = JsonConvert.DeserializeObject<OldCreds>(jsonCredentialsFileText);
if (oldCreds is null)
{
Log.Error("Error while reading old credentials file. Make sure that the file is formatted correctly");
return;
}
var creds = new Creds
{
Version = 1,
Token = oldCreds.Token,
OwnerIds = oldCreds.OwnerIds.Distinct().ToHashSet(),
GoogleApiKey = oldCreds.GoogleApiKey,
RapidApiKey = oldCreds.MashapeKey,
OsuApiKey = oldCreds.OsuApiKey,
CleverbotApiKey = oldCreds.CleverbotApiKey,
TotalShards = oldCreds.TotalShards <= 1 ? 1 : oldCreds.TotalShards,
Patreon = new Creds.PatreonSettings(oldCreds.PatreonAccessToken, null, null, oldCreds.PatreonCampaignId),
Votes = new Creds.VotesSettings(oldCreds.VotesUrl, oldCreds.VotesToken, string.Empty, string.Empty),
BotListToken = oldCreds.BotListToken,
RedisOptions = oldCreds.RedisOptions,
LocationIqApiKey = oldCreds.LocationIqApiKey,
TimezoneDbApiKey = oldCreds.TimezoneDbApiKey,
CoinmarketcapApiKey = oldCreds.CoinmarketcapApiKey
};
File.Move(OldCredsJsonPath, OldCredsJsonBackupPath, true);
File.WriteAllText(CredsPath, Yaml.Serializer.Serialize(creds));
Log.Warning(
"Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
}
if (File.Exists(CREDS_FILE_NAME)) if (File.Exists(CREDS_FILE_NAME))
{ {
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME)); var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
@ -192,9 +148,9 @@ public sealed class BotCredsProvider : IBotCredsProvider
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
} }
if (creds.Version <= 7) if (creds.Version <= 8)
{ {
creds.Version = 8; creds.Version = 9;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
} }
} }

View file

@ -31,7 +31,7 @@ public sealed class Creds : IBotCredentials
[Comment(""" [Comment("""
Pledge 5$ or more on https://patreon.com/elliebot and connect your discord account to Patreon. Pledge 5$ or more on https://patreon.com/elliebot and connect your discord account to Patreon.
Go to https://dashy.elliebot.net and login with your discord account Go to https://dashy.elliebot.net/me and login with your discord account
Go to the Keys page and click "Generate New Key" and copy it here Go to the Keys page and click "Generate New Key" and copy it here
You and anyone else with the permission to run `.prompt` command will be able to use natural language to run bot's commands. You and anyone else with the permission to run `.prompt` command will be able to use natural language to run bot's commands.
For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command
@ -156,7 +156,7 @@ public sealed class Creds : IBotCredentials
public Creds() public Creds()
{ {
Version = 7; Version = 9;
Token = string.Empty; Token = string.Empty;
UsePrivilegedIntents = true; UsePrivilegedIntents = true;
OwnerIds = new List<ulong>(); OwnerIds = new List<ulong>();

View file

@ -12,6 +12,7 @@ public abstract class EllieInteractionBase
private IUserMessage message = null!; private IUserMessage message = null!;
private readonly string _customId; private readonly string _customId;
private readonly bool _singleUse; private readonly bool _singleUse;
private readonly bool _clearAfter;
public EllieInteractionBase( public EllieInteractionBase(
DiscordSocketClient client, DiscordSocketClient client,
@ -19,13 +20,16 @@ public abstract class EllieInteractionBase
string customId, string customId,
Func<SocketMessageComponent, Task> onAction, Func<SocketMessageComponent, Task> onAction,
bool onlyAuthor, bool onlyAuthor,
bool singleUse = true) bool singleUse = true,
bool clearAfter = true)
{ {
_authorId = authorId; _authorId = authorId;
_customId = customId; _customId = customId;
_onAction = onAction; _onAction = onAction;
_onlyAuthor = onlyAuthor; _onlyAuthor = onlyAuthor;
_singleUse = singleUse; _singleUse = singleUse;
_clearAfter = clearAfter;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
Client = client; Client = client;
@ -36,12 +40,10 @@ public abstract class EllieInteractionBase
message = msg; message = msg;
Client.InteractionCreated += OnInteraction; Client.InteractionCreated += OnInteraction;
if (_singleUse)
await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task); await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task);
else
await Task.Delay(30_000);
Client.InteractionCreated -= OnInteraction; Client.InteractionCreated -= OnInteraction;
if (_clearAfter)
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build()); await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
} }
@ -59,10 +61,14 @@ public abstract class EllieInteractionBase
if (smc.Data.CustomId != _customId) if (smc.Data.CustomId != _customId)
return Task.CompletedTask; return Task.CompletedTask;
if (_interactionCompletedSource.Task.IsCompleted)
return Task.CompletedTask;
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try try
{ {
if (_singleUse)
_interactionCompletedSource.TrySetResult(true); _interactionCompletedSource.TrySetResult(true);
await ExecuteOnActionAsync(smc); await ExecuteOnActionAsync(smc);

View file

@ -13,25 +13,29 @@ public class EllieInteractionService : IEllieInteractionService, IEService
ulong userId, ulong userId,
ButtonBuilder button, ButtonBuilder button,
Func<SocketMessageComponent, Task> onTrigger, Func<SocketMessageComponent, Task> onTrigger,
bool singleUse = true) bool singleUse = true,
bool clearAfter = true)
=> new EllieButtonInteractionHandler(_client, => new EllieButtonInteractionHandler(_client,
userId, userId,
button, button,
onTrigger, onTrigger,
onlyAuthor: true, onlyAuthor: true,
singleUse: singleUse); singleUse: singleUse,
clearAfter: clearAfter);
public EllieInteractionBase Create<T>( public EllieInteractionBase Create<T>(
ulong userId, ulong userId,
ButtonBuilder button, ButtonBuilder button,
Func<SocketMessageComponent, T, Task> onTrigger, Func<SocketMessageComponent, T, Task> onTrigger,
in T state, in T state,
bool singleUse = true) bool singleUse = true,
bool clearAfter = true)
=> Create(userId, => Create(userId,
button, button,
((Func<T, Func<SocketMessageComponent, Task>>)((data) ((Func<T, Func<SocketMessageComponent, Task>>)((data)
=> smc => onTrigger(smc, data)))(state), => smc => onTrigger(smc, data)))(state),
singleUse); singleUse,
clearAfter);
public EllieInteractionBase Create( public EllieInteractionBase Create(
ulong userId, ulong userId,

View file

@ -6,14 +6,16 @@ public interface IEllieInteractionService
ulong userId, ulong userId,
ButtonBuilder button, ButtonBuilder button,
Func<SocketMessageComponent, Task> onTrigger, Func<SocketMessageComponent, Task> onTrigger,
bool singleUse = true); bool singleUse = true,
bool clearAfter = true);
public EllieInteractionBase Create<T>( public EllieInteractionBase Create<T>(
ulong userId, ulong userId,
ButtonBuilder button, ButtonBuilder button,
Func<SocketMessageComponent, T, Task> onTrigger, Func<SocketMessageComponent, T, Task> onTrigger,
in T state, in T state,
bool singleUse = true); bool singleUse = true,
bool clearAfter = true);
EllieInteractionBase Create( EllieInteractionBase Create(
ulong userId, ulong userId,

View file

@ -8,7 +8,8 @@ public sealed class EllieButtonInteractionHandler : EllieInteractionBase
ButtonBuilder button, ButtonBuilder button,
Func<SocketMessageComponent, Task> onAction, Func<SocketMessageComponent, Task> onAction,
bool onlyAuthor, bool onlyAuthor,
bool singleUse = true) bool singleUse = true,
bool clearAfter = true)
: base(client, authorId, button.CustomId, onAction, onlyAuthor, singleUse) : base(client, authorId, button.CustomId, onAction, onlyAuthor, singleUse)
{ {
Button = button; Button = button;

View file

@ -1,45 +0,0 @@
#nullable disable
namespace EllieBot.Common;
public class OldCreds
{
public string Token { get; set; } = string.Empty;
public ulong[] OwnerIds { get; set; } = new ulong[1];
public string LoLApiKey { get; set; } = string.Empty;
public string GoogleApiKey { get; set; } = string.Empty;
public string MashapeKey { get; set; } = string.Empty;
public string OsuApiKey { get; set; } = string.Empty;
public string CleverbotApiKey { get; set; } = string.Empty;
public string CarbonKey { get; set; } = string.Empty;
public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038";
public RestartConfig RestartCommand { get; set; }
public string ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { get; set; } = string.Empty;
public int? ShardRunPort { get; set; }
public string MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty;
public string BotListToken { get; set; } = string.Empty;
public string TwitchClientId { get; set; } = string.Empty;
public string VotesToken { get; set; } = string.Empty;
public string VotesUrl { get; set; } = string.Empty;
public string RedisOptions { get; set; } = string.Empty;
public string LocationIqApiKey { get; set; } = string.Empty;
public string TimezoneDbApiKey { get; set; } = string.Empty;
public string CoinmarketcapApiKey { get; set; } = string.Empty;
public class RestartConfig
{
public string Cmd { get; set; }
public string Args { get; set; }
public RestartConfig(string cmd, string args)
{
Cmd = cmd;
Args = args;
}
}
}

View file

@ -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(
@ -71,7 +71,8 @@ public partial class ResponseBuilder
return Task.CompletedTask; return Task.CompletedTask;
}, },
true, true,
singleUse: false); singleUse: false,
clearAfter: false);
if (_paginationBuilder.InteractionFunc is not null) if (_paginationBuilder.InteractionFunc is not null)
{ {
@ -106,7 +107,8 @@ public partial class ResponseBuilder
return Task.CompletedTask; return Task.CompletedTask;
}, },
true, true,
singleUse: false); singleUse: false,
clearAfter: false);
return (leftBtnInter, maybeInter, rightBtnInter); return (leftBtnInter, maybeInter, rightBtnInter);
} }
@ -120,8 +122,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());

View file

@ -229,10 +229,4 @@ public static class Extensions
public static IEnumerable<IRole> GetRoles(this IGuildUser user) public static IEnumerable<IRole> GetRoles(this IGuildUser user)
=> user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r is not null); => user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r is not null);
// todo remove
public static void Lap(this Stopwatch sw, string checkpoint)
{
Log.Information("Checkpoint {CheckPoint}: {Time}ms", checkpoint, sw.Elapsed.TotalMilliseconds);
sw.Restart();
}
} }

View file

@ -13,6 +13,12 @@ usePrivilegedIntents: true
# note: If you are planning to have more than one shard, then you must change botCache to 'redis'. # note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
# Also, in that case you should be using EllieBot.Coordinator to start the bot, and it will correctly override this value. # Also, in that case you should be using EllieBot.Coordinator to start the bot, and it will correctly override this value.
totalShards: 1 totalShards: 1
# Pledge 5$ or more on https://patreon.com/elliebot and connect your discord account to Patreon.
# Go to https://dashy.elliebot.net/me and login with your discord account
# Go to the Keys page and click "Generate New Key" and copy it here
# You and anyone else with the permission to run `.prompt` command will be able to use natural language to run bot's commands.
# For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command.
ellieAiToken:
# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. # Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
# Then, go to APIs and Services -> Credentials and click Create credentials -> API key. # Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
# Used only for Youtube Data Api (at the moment). # Used only for Youtube Data Api (at the moment).
@ -56,7 +62,7 @@ patreon:
botListToken: "" botListToken: ""
# Official cleverbot api key. # Official cleverbot api key.
cleverbotApiKey: "" cleverbotApiKey: ""
# Official GPT-3 api key. # OpenAi api key.
gpt3ApiKey: "" gpt3ApiKey: ""
# Which cache implementation should bot use. # Which cache implementation should bot use.
# 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. # 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.

View file

@ -1401,3 +1401,5 @@ cleanupguilddata:
- cleanupguilddata - cleanupguilddata
prompt: prompt:
- prompt - prompt
honeypot:
- honeypot

View file

@ -4523,3 +4523,12 @@ prompt:
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:
- {}

View file

@ -619,7 +619,7 @@
"quote_deleted": "Quote #{0} deleted.", "quote_deleted": "Quote #{0} deleted.",
"quote_edited": "Quote Edited", "quote_edited": "Quote Edited",
"region": "Region", "region": "Region",
"remind2": "I will remind {0} to {1} {2} ({3})`", "remind2": "I will remind {0} to {1} {2} ({3})",
"remind_timely": "I will remind you about your timely reward {0}", "remind_timely": "I will remind you about your timely reward {0}",
"remind_invalid": "Not a valid remind format. Remind must have a target, timer and a reason. Check the command list.", "remind_invalid": "Not a valid remind format. Remind must have a target, timer and a reason. Check the command list.",
"remind_too_long": "Remind time has exceeded maximum.", "remind_too_long": "Remind time has exceeded maximum.",
@ -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."
} }