mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3a5b482884 | ||
|
db66264bc6 | ||
|
ae1ddd82d0 | ||
|
8523abd6f1 | ||
|
e1892c4ff4 | ||
|
a50a7b3b0e | ||
|
9d2268a925 | ||
|
d77a86c08b | ||
|
d605f685cf | ||
|
bbc1fd28c2 | ||
|
cff8a258d0 | ||
|
1d760a548e | ||
|
25fa8a3852 | ||
|
ca13684c0d | ||
|
0ad6b741e7 | ||
|
4ce756d760 | ||
|
5f2813d3af | ||
|
1b7458529c | ||
|
9c9c8d7490 | ||
|
2700bfdce8 | ||
|
5498c5ce3f | ||
|
6f444a8da0 | ||
|
454c14eee1 | ||
|
30aa8e8186 | ||
|
2ca141810c | ||
|
9da8e4f1c1 | ||
|
ef471c32bb | ||
|
49d557caec | ||
|
4366f908f3 | ||
|
237e66495b |
63
CHANGELOG.md
63
CHANGELOG.md
@@ -2,6 +2,65 @@
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||
|
||||
## [5.1.4] - 13.07.2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added `.coins` command which lists top 10 cryptos ordered by marketcap
|
||||
- Added Clubs rank in the leaderboard to `.clubinfo`
|
||||
- Bot owners can now check other people's bank balance (Not server owners, only bot owner, the person who is hosting the bot)
|
||||
- You can now send multiple waifu gifts at once to waifus. For example `.waifugift 3xRose @user` will give that user 3 roses
|
||||
- The format is `<NUMBER>x<ITEM>`, no spaces
|
||||
- Added `.boosttest` command
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated command strings to clarify `.say` and `.send` usages
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.waifugift` help string
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed selfhost button from `.donate` command, no idea why it was there in the first place
|
||||
|
||||
## [5.1.3] - 06.07.2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added `.quran` command, which will show the provided ayah in english and arabic, including recitation by Alafasy
|
||||
|
||||
### Changed
|
||||
|
||||
- Replying to the bot's message in the channel where chatterbot is enabled will also trigger the ai response, as if you pinged the bot. This only works for chatterbot, but not for nadeko ai command prompts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.stickeradd` it now properly supports 300x300 image uploads.
|
||||
- Bot should now trim the invalid characters from chatterbot usernames to avoid openai errors
|
||||
- Fixed prompt triggering chatterbot responses twice
|
||||
|
||||
## [5.1.2] - 29.06.2024
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.honeypot` not unbanning and not pruning messages
|
||||
|
||||
## [5.1.1] - 27.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] - 25.06.2024
|
||||
|
||||
### Added
|
||||
@@ -10,9 +69,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
|
||||
- You can send natural language questions, queries or execute commands. For example "@Nadeko 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 @Nadeko bot. If you're selfhosting, you also will need to acquire the api key from <https://dashy.nadeko.bot/api> (coming soon(ish)...)
|
||||
- Requires Patreon subscription, after which you'll be able to run it on global @Nadeko bot.
|
||||
- Selfhosters: If you're selfhosting, you also will need to acquire the api key from <https://dashy.nadeko.bot/me> after pledging on patreon and put it in nadekoAiToken in creds.yml
|
||||
- Added support for `gpt-4o` in `data/games.yml`
|
||||
- Added nadekoAiToken to `creds.yml`
|
||||
|
||||
|
||||
### Changed
|
||||
|
@@ -181,9 +181,9 @@ dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
|
||||
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_readonly_field.severity = warning
|
||||
|
||||
dotnet_naming_rule.private_field.symbols = private_field
|
||||
dotnet_naming_rule.private_field.style = camel_case
|
||||
dotnet_naming_rule.private_field.severity = warning
|
||||
# dotnet_naming_rule.private_field.symbols = private_field
|
||||
# dotnet_naming_rule.private_field.style = camel_case
|
||||
# dotnet_naming_rule.private_field.severity = warning
|
||||
|
||||
dotnet_naming_rule.const_fields.symbols = const_fields
|
||||
dotnet_naming_rule.const_fields.style = all_upper
|
||||
|
@@ -93,7 +93,7 @@ public sealed class Bot : IBot
|
||||
private void AddServices()
|
||||
{
|
||||
var startingGuildIdList = GetCurrentGuildIds();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
var bot = Client.CurrentUser;
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
@@ -160,9 +160,8 @@ public sealed class Bot : IBot
|
||||
{
|
||||
LoadTypeReaders(a);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
|
||||
|
||||
Log.Information("All services loaded in {ServiceLoadTime:F2}s", Stopwatch.GetElapsedTime(startTime) .TotalSeconds);
|
||||
}
|
||||
|
||||
private void LoadTypeReaders(Assembly assembly)
|
||||
@@ -259,7 +258,7 @@ public sealed class Bot : IBot
|
||||
if (ShardId == 0)
|
||||
await _db.SetupAsync();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
|
||||
await LoginAsync(_creds.Token);
|
||||
|
||||
@@ -274,8 +273,7 @@ public sealed class Bot : IBot
|
||||
Helpers.ReadErrorAndExit(9);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, Stopwatch.GetElapsedTime(startTime).TotalSeconds);
|
||||
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
||||
|
||||
// start handling messages received in commandhandler
|
||||
|
11
src/NadekoBot/Db/Models/HoneypotChannel.cs
Normal file
11
src/NadekoBot/Db/Models/HoneypotChannel.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace NadekoBot.Db.Models;
|
||||
|
||||
public class HoneypotChannel
|
||||
{
|
||||
[Key]
|
||||
public ulong GuildId { get; set; }
|
||||
|
||||
public ulong ChannelId { get; set; }
|
||||
}
|
@@ -59,6 +59,7 @@ public abstract class NadekoContext : DbContext
|
||||
|
||||
public DbSet<TodoModel> Todos { get; set; }
|
||||
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
||||
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
|
||||
|
||||
// todo add guild colors
|
||||
// public DbSet<GuildColors> GuildColors { get; set; }
|
||||
|
3803
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.Designer.cs
generated
Normal file
3803
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.cs
Normal file
36
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1388,6 +1388,25 @@ namespace NadekoBot.Migrations.Mysql
|
||||
b.ToTable("guildconfigs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.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("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
3798
src/NadekoBot/Migrations/PostgreSql/20240627033522_honeypot.Designer.cs
generated
Normal file
3798
src/NadekoBot/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 NadekoBot.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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1387,6 +1387,23 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
b.ToTable("guildconfigs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.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("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
2935
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.Designer.cs
generated
Normal file
2935
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.cs
Normal file
34
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1033,6 +1033,20 @@ namespace NadekoBot.Migrations
|
||||
b.ToTable("GuildConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NadekoBot.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("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@@ -225,5 +225,19 @@ public partial class Administration
|
||||
if (!enabled)
|
||||
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageGuild)]
|
||||
[Ratelimit(5)]
|
||||
public async Task BoostTest([Leftover] IGuildUser? user = null)
|
||||
{
|
||||
user ??= (IGuildUser)ctx.User;
|
||||
|
||||
await _service.BoostTest((ITextChannel)ctx.Channel, user);
|
||||
var enabled = _service.GetBoostEnabled(ctx.Guild.Id);
|
||||
if (!enabled)
|
||||
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -242,7 +242,7 @@ public class GreetService : INService, IReadyExecutor
|
||||
guild: channel.Guild,
|
||||
channel: channel,
|
||||
users: users.ToArray());
|
||||
|
||||
|
||||
var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
|
||||
text = await _repSvc.ReplaceAsync(text, repCtx);
|
||||
try
|
||||
@@ -630,6 +630,13 @@ public class GreetService : INService, IReadyExecutor
|
||||
return conf.SendChannelByeMessage;
|
||||
}
|
||||
|
||||
public bool GetBoostEnabled(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var conf = uow.GuildConfigsForId(guildId, set => set);
|
||||
return conf.SendBoostMessage;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Messages
|
||||
|
@@ -0,0 +1,95 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Honeypot;
|
||||
|
||||
public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoCommand, INService
|
||||
{
|
||||
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(pruneDays: 1);
|
||||
await user.Guild.RemoveBanAsync(user.Id);
|
||||
}
|
||||
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 NadekoBot.Modules.Administration.Honeypot;
|
||||
|
||||
namespace NadekoBot.Modules.Administration;
|
||||
|
||||
public partial class Administration
|
||||
{
|
||||
[Group]
|
||||
public partial class HoneypotCommands : NadekoModule
|
||||
{
|
||||
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 NadekoBot.Modules.Administration.Honeypot;
|
||||
|
||||
public interface IHoneyPotService
|
||||
{
|
||||
public Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId);
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
// namespace NadekoBot.Modules.Administration;
|
||||
//
|
||||
// public partial class Administration
|
||||
// {
|
||||
// [Group]
|
||||
// public partial class TicketCommands : NadekoModule
|
||||
// {
|
||||
// [Cmd]
|
||||
// public async Task Ticket()
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
@@ -73,6 +73,27 @@ public partial class Gambling
|
||||
await Response().Error(strs.cant_dm).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task BankBalance([Leftover] IUser user)
|
||||
{
|
||||
var bal = await _bank.GetBalanceAsync(user.Id);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal))));
|
||||
|
||||
try
|
||||
{
|
||||
await Response().User(ctx.User).Embed(eb).SendAsync();
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Response().Error(strs.cant_dm).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BankTakeInternalAsync(long amount, ulong userId)
|
||||
{
|
||||
|
@@ -17,7 +17,8 @@ public partial class Gambling
|
||||
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
||||
private readonly IImageCache _images;
|
||||
|
||||
public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs)
|
||||
public DrawCommands(IImageCache images, GamblingConfigService gcs)
|
||||
: base(gcs)
|
||||
=> _images = images;
|
||||
|
||||
private async Task InternalDraw(int count, ulong? guildId = null)
|
||||
@@ -56,8 +57,8 @@ public partial class Gambling
|
||||
i.Dispose();
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor();
|
||||
|
||||
.WithOkColor();
|
||||
|
||||
var toSend = string.Empty;
|
||||
if (cardObjects.Count == 5)
|
||||
eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true);
|
||||
@@ -71,7 +72,7 @@ public partial class Gambling
|
||||
|
||||
if (count > 1)
|
||||
eb.AddField(GetText(strs.cards), count.ToString(), true);
|
||||
|
||||
|
||||
await using var imageStream = await img.ToStreamAsync();
|
||||
await ctx.Channel.SendFileAsync(imageStream,
|
||||
imgName,
|
||||
@@ -84,7 +85,7 @@ public partial class Gambling
|
||||
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
|
||||
return Image.Load<Rgba32>(cardBytes);
|
||||
}
|
||||
|
||||
|
||||
private async Task<Image<Rgba32>> GetCardImageAsync(Deck.Card currentCard)
|
||||
{
|
||||
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
||||
@@ -98,7 +99,7 @@ public partial class Gambling
|
||||
{
|
||||
if (num < 1)
|
||||
return;
|
||||
|
||||
|
||||
if (num > 10)
|
||||
num = 10;
|
||||
|
||||
@@ -110,7 +111,7 @@ public partial class Gambling
|
||||
{
|
||||
if (num < 1)
|
||||
return;
|
||||
|
||||
|
||||
if (num > 10)
|
||||
num = 10;
|
||||
|
||||
@@ -136,19 +137,29 @@ public partial class Gambling
|
||||
|
||||
[Cmd]
|
||||
[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);
|
||||
|
||||
|
||||
[Cmd]
|
||||
[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);
|
||||
|
||||
|
||||
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
|
||||
{
|
||||
if (amount <= 0)
|
||||
if (!await CheckBetMandatory(amount))
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
var res = await _service.BetDrawAsync(ctx.User.Id,
|
||||
amount,
|
||||
(byte?)val,
|
||||
@@ -161,13 +172,13 @@ public partial class Gambling
|
||||
}
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(result.Card.GetEmoji())
|
||||
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
|
||||
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), false)
|
||||
.WithImageUrl("attachment://card.png");
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(result.Card.GetEmoji())
|
||||
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
|
||||
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), false)
|
||||
.WithImageUrl("attachment://card.png");
|
||||
|
||||
using var img = await GetCardImageAsync(result.Card);
|
||||
await using var imgStream = await img.ToStreamAsync();
|
||||
@@ -189,9 +200,10 @@ public partial class Gambling
|
||||
InputColorGuess.Black => "B ⚫",
|
||||
_ => "❓"
|
||||
};
|
||||
|
||||
|
||||
return $"{val} / {col}";
|
||||
}
|
||||
|
||||
private string GetCardInfo(RegularCard card)
|
||||
{
|
||||
var val = (int)card.Value switch
|
||||
@@ -208,7 +220,7 @@ public partial class Gambling
|
||||
RegularSuit.Diamonds or RegularSuit.Hearts => "R 🔴",
|
||||
_ => "B ⚫"
|
||||
};
|
||||
|
||||
|
||||
return $"{val} / {col}";
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Waifu;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Db.Models;
|
||||
using TwitchLib.Api.Helix.Models.Teams;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
@@ -21,8 +22,8 @@ public partial class Gambling
|
||||
{
|
||||
var price = _service.GetResetPrice(ctx.User);
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithTitle(GetText(strs.waifu_reset_confirm))
|
||||
.WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price)))));
|
||||
.WithTitle(GetText(strs.waifu_reset_confirm))
|
||||
.WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price)))));
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
@@ -307,24 +308,26 @@ public partial class Gambling
|
||||
fansStr = "-";
|
||||
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.waifu)
|
||||
+ " "
|
||||
+ (wi.FullName ?? name ?? targetId.ToString())
|
||||
+ " - \"the "
|
||||
+ _service.GetClaimTitle(wi.ClaimCount)
|
||||
+ "\"")
|
||||
.AddField(GetText(strs.price), N(wi.Price), true)
|
||||
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
|
||||
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
|
||||
.AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true)
|
||||
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
|
||||
.AddField("\u200B", "\u200B", true)
|
||||
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
|
||||
.AddField($"Waifus ({wi.ClaimCount})",
|
||||
wi.ClaimCount == 0 ? nobody : claimsStr,
|
||||
true)
|
||||
.AddField(GetText(strs.gifts), itemsStr, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.waifu)
|
||||
+ " "
|
||||
+ (wi.FullName ?? name ?? targetId.ToString())
|
||||
+ " - \"the "
|
||||
+ _service.GetClaimTitle(wi.ClaimCount)
|
||||
+ "\"")
|
||||
.AddField(GetText(strs.price), N(wi.Price), true)
|
||||
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
|
||||
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
|
||||
.AddField(GetText(strs.changes_of_heart),
|
||||
$"{wi.AffinityCount} - \"the {affInfo}\"",
|
||||
true)
|
||||
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
|
||||
.AddField("\u200B", "\u200B", true)
|
||||
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
|
||||
.AddField($"Waifus ({wi.ClaimCount})",
|
||||
wi.ClaimCount == 0 ? nobody : claimsStr,
|
||||
true)
|
||||
.AddField(GetText(strs.gifts), itemsStr, true);
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
@@ -348,7 +351,7 @@ public partial class Gambling
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor();
|
||||
|
||||
|
||||
items
|
||||
.ToList()
|
||||
.ForEach(x => embed.AddField(
|
||||
@@ -364,30 +367,27 @@ public partial class Gambling
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(0)]
|
||||
public async Task WaifuGift(string itemName, [Leftover] IUser waifu)
|
||||
public async Task WaifuGift(MultipleWaifuItems items, [Leftover] IUser waifu)
|
||||
{
|
||||
if (waifu.Id == ctx.User.Id)
|
||||
return;
|
||||
|
||||
var allItems = _service.GetWaifuItems();
|
||||
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant());
|
||||
if (item is null)
|
||||
{
|
||||
await Response().Error(strs.waifu_gift_not_exist).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item);
|
||||
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, items.Item, items.Count);
|
||||
|
||||
if (sucess)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.waifu_gift(Format.Bold(item + " " + item.ItemEmoji),
|
||||
.Confirm(strs.waifu_gift(Format.Bold($"{GetCountString(items)}{items.Item} {items.Item.ItemEmoji}"),
|
||||
Format.Bold(waifu.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
||||
}
|
||||
|
||||
private static string GetCountString(MultipleWaifuItems items)
|
||||
=> items.Count > 1
|
||||
? $"{items.Count}x "
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Waifu;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services;
|
||||
|
||||
@@ -89,9 +90,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
if (waifu is null)
|
||||
return settings.Waifu.MinPrice;
|
||||
|
||||
var divorces = uow.Set<WaifuUpdate>().Count(x
|
||||
=> x.Old != null && x.Old.UserId == user.Id && x.UpdateType == WaifuUpdateType.Claimed && x.New == null);
|
||||
var affs = uow.Set<WaifuUpdate>().AsQueryable()
|
||||
var divorces = uow.Set<WaifuUpdate>()
|
||||
.Count(x
|
||||
=> x.Old != null
|
||||
&& x.Old.UserId == user.Id
|
||||
&& x.UpdateType == WaifuUpdateType.Claimed
|
||||
&& x.New == null);
|
||||
var affs = uow.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Where(w => w.User.UserId == user.Id
|
||||
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
||||
&& w.New != null)
|
||||
@@ -110,12 +116,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
|
||||
return false;
|
||||
|
||||
var affs = uow.Set<WaifuUpdate>().AsQueryable()
|
||||
var affs = uow.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Where(w => w.User.UserId == user.Id
|
||||
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
||||
&& w.New != null);
|
||||
|
||||
var divorces = uow.Set<WaifuUpdate>().AsQueryable()
|
||||
var divorces = uow.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Where(x => x.Old != null
|
||||
&& x.Old.UserId == user.Id
|
||||
&& x.UpdateType == WaifuUpdateType.Claimed
|
||||
@@ -158,20 +166,22 @@ public class WaifuService : INService, IReadyExecutor
|
||||
result = WaifuClaimResult.NotEnoughFunds;
|
||||
else
|
||||
{
|
||||
uow.Set<WaifuInfo>().Add(w = new()
|
||||
{
|
||||
Waifu = waifu,
|
||||
Claimer = claimer,
|
||||
Affinity = null,
|
||||
Price = amount
|
||||
});
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = waifu,
|
||||
Old = null,
|
||||
New = claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuInfo>()
|
||||
.Add(w = new()
|
||||
{
|
||||
Waifu = waifu,
|
||||
Claimer = claimer,
|
||||
Affinity = null,
|
||||
Price = amount
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = waifu,
|
||||
Old = null,
|
||||
New = claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
result = WaifuClaimResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -186,13 +196,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
w.Price = amount + (amount / 4);
|
||||
result = WaifuClaimResult.Success;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
|
||||
@@ -206,13 +217,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
w.Price = amount;
|
||||
result = WaifuClaimResult.Success;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -241,29 +253,31 @@ public class WaifuService : INService, IReadyExecutor
|
||||
|
||||
remaining = await _cache.GetRatelimitAsync(GetAffinityKey(user.Id),
|
||||
30.Minutes());
|
||||
|
||||
|
||||
if (remaining is not null)
|
||||
{
|
||||
}
|
||||
else if (w is null)
|
||||
{
|
||||
var thisUser = uow.GetOrCreateUser(user);
|
||||
uow.Set<WaifuInfo>().Add(new()
|
||||
{
|
||||
Affinity = newAff,
|
||||
Waifu = thisUser,
|
||||
Price = 1,
|
||||
Claimer = null
|
||||
});
|
||||
uow.Set<WaifuInfo>()
|
||||
.Add(new()
|
||||
{
|
||||
Affinity = newAff,
|
||||
Waifu = thisUser,
|
||||
Price = 1,
|
||||
Claimer = null
|
||||
});
|
||||
success = true;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = thisUser,
|
||||
Old = null,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = thisUser,
|
||||
Old = null,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -272,13 +286,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
w.Affinity = newAff;
|
||||
success = true;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldAff,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldAff,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -301,10 +316,10 @@ public class WaifuService : INService, IReadyExecutor
|
||||
|
||||
private static TypedKey<long> GetDivorceKey(ulong userId)
|
||||
=> new($"waifu:divorce_cd:{userId}");
|
||||
|
||||
|
||||
private static TypedKey<long> GetAffinityKey(ulong userId)
|
||||
=> new($"waifu:affinity:{userId}");
|
||||
|
||||
|
||||
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
|
||||
{
|
||||
DivorceResult result;
|
||||
@@ -343,13 +358,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
var oldClaimer = w.Claimer;
|
||||
w.Claimer = null;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = null,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = null,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -358,40 +374,54 @@ public class WaifuService : INService, IReadyExecutor
|
||||
return (w, result, amount, remaining);
|
||||
}
|
||||
|
||||
public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
|
||||
public async Task<bool> GiftWaifuAsync(
|
||||
IUser from,
|
||||
IUser giftedWaifu,
|
||||
WaifuItemModel itemObj,
|
||||
int count)
|
||||
{
|
||||
if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item")))
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(count));
|
||||
|
||||
if (!await _cs.RemoveAsync(from, itemObj.Price * count, new("waifu", "item")))
|
||||
return false;
|
||||
|
||||
var totalValue = itemObj.Price * count;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var w = uow.Set<WaifuInfo>().ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer));
|
||||
var w = uow.Set<WaifuInfo>()
|
||||
.ByWaifuUserId(giftedWaifu.Id,
|
||||
set => set
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Claimer));
|
||||
if (w is null)
|
||||
{
|
||||
uow.Set<WaifuInfo>().Add(w = new()
|
||||
{
|
||||
Affinity = null,
|
||||
Claimer = null,
|
||||
Price = 1,
|
||||
Waifu = uow.GetOrCreateUser(giftedWaifu)
|
||||
});
|
||||
uow.Set<WaifuInfo>()
|
||||
.Add(w = new()
|
||||
{
|
||||
Affinity = null,
|
||||
Claimer = null,
|
||||
Price = 1,
|
||||
Waifu = uow.GetOrCreateUser(giftedWaifu)
|
||||
});
|
||||
}
|
||||
|
||||
if (!itemObj.Negative)
|
||||
{
|
||||
w.Items.Add(new()
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji
|
||||
});
|
||||
w.Items.AddRange(Enumerable.Range(0, count)
|
||||
.Select((_) => new WaifuItem()
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji
|
||||
}));
|
||||
|
||||
if (w.Claimer?.UserId == from.Id)
|
||||
w.Price += (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
else
|
||||
w.Price += itemObj.Price / 2;
|
||||
w.Price += totalValue / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Price -= (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||
w.Price -= (long)(totalValue * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||
if (w.Price < 1)
|
||||
w.Price = 1;
|
||||
}
|
||||
@@ -492,6 +522,7 @@ public class WaifuService : INService, IReadyExecutor
|
||||
}
|
||||
|
||||
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
// only decay waifu values from shard 0
|
||||
@@ -513,7 +544,7 @@ public class WaifuService : INService, IReadyExecutor
|
||||
var nowB = now.ToBinary();
|
||||
|
||||
var result = await _cache.GetAsync(_waifuDecayKey);
|
||||
|
||||
|
||||
if (result.TryGetValue(out var val))
|
||||
{
|
||||
var lastDecay = DateTime.FromBinary(val);
|
||||
@@ -533,7 +564,6 @@ public class WaifuService : INService, IReadyExecutor
|
||||
{
|
||||
Price = (long)(old.Price * multi)
|
||||
});
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -550,33 +580,35 @@ public class WaifuService : INService, IReadyExecutor
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<DiscordUser>()
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.ClaimerId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.ClaimerId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<DiscordUser>()
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.AffinityId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.AffinityId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<WaifuItem>> GetItems(int waifuId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<WaifuItem>()
|
||||
.Where(x => x.WaifuInfoId == ctx.GetTable<WaifuInfo>()
|
||||
.Where(x => x.WaifuId == waifuId)
|
||||
.Select(x => x.Id)
|
||||
.FirstOrDefault())
|
||||
.ToListAsyncEF();
|
||||
.Where(x => x.WaifuInfoId
|
||||
== ctx.GetTable<WaifuInfo>()
|
||||
.Where(x => x.WaifuId == waifuId)
|
||||
.Select(x => x.Id)
|
||||
.FirstOrDefault())
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
public record class MultipleWaifuItems(int Count, WaifuItemModel Item);
|
@@ -0,0 +1,47 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
public partial class MultipleWaifuItemsTypeReader : NadekoTypeReader<MultipleWaifuItems>
|
||||
{
|
||||
private readonly WaifuService _service;
|
||||
|
||||
[GeneratedRegex(@"(?:(?<count>\d+)[x*])?(?<item>.+)")]
|
||||
private static partial Regex ItemRegex();
|
||||
|
||||
public MultipleWaifuItemsTypeReader(WaifuService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
public override ValueTask<TypeReaderResult<MultipleWaifuItems>> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
input = input.ToLowerInvariant();
|
||||
var match = ItemRegex().Match(input);
|
||||
if (!match.Success)
|
||||
{
|
||||
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid input."));
|
||||
}
|
||||
|
||||
var count = 1;
|
||||
if (match.Groups["count"].Success)
|
||||
{
|
||||
if (!int.TryParse(match.Groups["count"].Value, out count) || count < 1)
|
||||
{
|
||||
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid count."));
|
||||
}
|
||||
}
|
||||
|
||||
var itemName = match.Groups["item"].Value?.ToLowerInvariant();
|
||||
var allItems = _service.GetWaifuItems();
|
||||
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName);
|
||||
if (item is null)
|
||||
{
|
||||
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Waifu gift does not exist."));
|
||||
}
|
||||
|
||||
return new(Discord.Commands.TypeReaderResult.FromSuccess(new MultipleWaifuItems(count, item)));
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
using NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
@@ -58,18 +57,21 @@ public class ChatterBotService : IExecOnMessage
|
||||
|
||||
Log.Information("Cleverbot will not work as the api key is missing");
|
||||
return null;
|
||||
case ChatBotImplementation.Gpt:
|
||||
case ChatBotImplementation.OpenAi:
|
||||
var data = _gcs.Data;
|
||||
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
||||
return new OfficialGptSession(_creds.Gpt3ApiKey,
|
||||
_gcs.Data.ChatGpt.ModelName,
|
||||
_gcs.Data.ChatGpt.ChatHistory,
|
||||
_gcs.Data.ChatGpt.MaxTokens,
|
||||
_gcs.Data.ChatGpt.MinTokens,
|
||||
_gcs.Data.ChatGpt.PersonalityPrompt,
|
||||
return new OpenAiApiSession(
|
||||
data.ChatGpt.ApiUrl,
|
||||
_creds.Gpt3ApiKey,
|
||||
data.ChatGpt.ModelName,
|
||||
data.ChatGpt.ChatHistory,
|
||||
data.ChatGpt.MaxTokens,
|
||||
data.ChatGpt.MinTokens,
|
||||
data.ChatGpt.PersonalityPrompt,
|
||||
_client.CurrentUser.Username,
|
||||
_httpFactory);
|
||||
|
||||
Log.Information("Gpt3 will not work as the api key is missing");
|
||||
Log.Information("Openai Api will likely not work as the api key is missing");
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
@@ -96,6 +98,8 @@ public class ChatterBotService : IExecOnMessage
|
||||
message = msg.Content[normalMention.Length..].Trim();
|
||||
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
|
||||
message = msg.Content[nickMention.Length..].Trim();
|
||||
else if (msg.ReferencedMessage?.Author.Id == nadekoId)
|
||||
message = msg.Content;
|
||||
else
|
||||
return null;
|
||||
|
||||
@@ -165,7 +169,7 @@ public class ChatterBotService : IExecOnMessage
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("Error in chatterbot: {Error}", error);
|
||||
Log.Warning("Error in chatterbot: {Error}", error.Value);
|
||||
}
|
||||
|
||||
Log.Information("""
|
||||
|
9
src/NadekoBot/Modules/Games/ChatterBot/_common/Choice.cs
Normal file
9
src/NadekoBot/Modules/Games/ChatterBot/_common/Choice.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class Choice
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public Message Message { get; init; }
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiCompletionResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public Choice[] Choices { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public OpenAiUsageData Usage { get; set; }
|
||||
}
|
||||
|
||||
public class OpenAiUsageData
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
|
||||
public class Choice
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public Message Message { get; init; }
|
||||
}
|
||||
|
||||
public class Message {
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; init; }
|
||||
}
|
||||
|
||||
public class Gpt3ApiRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; init; }
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<GPTMessage> Messages { get; init; }
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
public int Temperature { get; init; }
|
||||
|
||||
[JsonPropertyName("max_tokens")]
|
||||
public int MaxTokens { get; init; }
|
||||
}
|
||||
|
||||
public class GPTMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role {get; init;}
|
||||
[JsonPropertyName("content")]
|
||||
public string Content {get; init;}
|
||||
[JsonPropertyName("name")]
|
||||
public string Name {get; init;}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class Message {
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; init; }
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiApiMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; init; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; }
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiApiRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; init; }
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<OpenAiApiMessage> Messages { get; init; }
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
public int Temperature { get; init; }
|
||||
|
||||
[JsonPropertyName("max_tokens")]
|
||||
public int MaxTokens { get; init; }
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiApiUsageData
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiCompletionResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public Choice[] Choices { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public OpenAiApiUsageData Usage { get; set; }
|
||||
}
|
@@ -1,16 +1,15 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using OneOf.Types;
|
||||
using System.Net.Http.Json;
|
||||
using SharpToken;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OfficialGptSession : IChatterBotSession
|
||||
public partial class OpenAiApiSession : IChatterBotSession
|
||||
{
|
||||
private string Uri
|
||||
=> $"https://api.openai.com/v1/chat/completions";
|
||||
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _apiKey;
|
||||
private readonly string _model;
|
||||
private readonly int _maxHistory;
|
||||
@@ -18,13 +17,14 @@ public class OfficialGptSession : IChatterBotSession
|
||||
private readonly int _minTokens;
|
||||
private readonly string _nadekoUsername;
|
||||
private readonly GptEncoding _encoding;
|
||||
private List<GPTMessage> messages = new();
|
||||
private List<OpenAiApiMessage> messages = new();
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
|
||||
public OfficialGptSession(
|
||||
public OpenAiApiSession(
|
||||
string url,
|
||||
string apiKey,
|
||||
ChatGptModel model,
|
||||
string model,
|
||||
int chatHistory,
|
||||
int maxTokens,
|
||||
int minTokens,
|
||||
@@ -32,37 +32,47 @@ public class OfficialGptSession : IChatterBotSession
|
||||
string nadekoUsername,
|
||||
IHttpClientFactory factory)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_httpFactory = factory;
|
||||
|
||||
_model = model switch
|
||||
if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
{
|
||||
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo",
|
||||
ChatGptModel.Gpt4o => "gpt-4o",
|
||||
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
|
||||
};
|
||||
throw new ArgumentException("Invalid OpenAi api url provided", nameof(url));
|
||||
}
|
||||
|
||||
_baseUrl = url.TrimEnd('/');
|
||||
|
||||
_apiKey = apiKey;
|
||||
_model = model;
|
||||
_httpFactory = factory;
|
||||
_maxHistory = chatHistory;
|
||||
_maxTokens = maxTokens;
|
||||
_minTokens = minTokens;
|
||||
_nadekoUsername = nadekoUsername;
|
||||
_encoding = GptEncoding.GetEncodingForModel(_model);
|
||||
messages.Add(new()
|
||||
_nadekoUsername = UsernameCleaner().Replace(nadekoUsername, "");
|
||||
_encoding = GptEncoding.GetEncodingForModel("gpt-4o");
|
||||
if (!string.IsNullOrWhiteSpace(personality))
|
||||
{
|
||||
Role = "system",
|
||||
Content = personality,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "system",
|
||||
Content = personality,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[GeneratedRegex("[^a-zA-Z0-9_-]")]
|
||||
private static partial Regex UsernameCleaner();
|
||||
|
||||
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username)
|
||||
{
|
||||
username = UsernameCleaner().Replace(username, "");
|
||||
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "user",
|
||||
Content = input,
|
||||
Name = username
|
||||
});
|
||||
|
||||
while (messages.Count > _maxHistory + 2)
|
||||
{
|
||||
messages.RemoveAt(1);
|
||||
@@ -83,26 +93,29 @@ public class OfficialGptSession : IChatterBotSession
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Error<string>("Token count exceeded, please increase the number of tokens in the bot config and restart.");
|
||||
return new Error<string>(
|
||||
"Token count exceeded, please increase the number of tokens in the bot config and restart.");
|
||||
}
|
||||
}
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
|
||||
|
||||
var data = await http.PostAsJsonAsync(Uri,
|
||||
new Gpt3ApiRequest()
|
||||
|
||||
var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions",
|
||||
new OpenAiApiRequest()
|
||||
{
|
||||
Model = _model,
|
||||
Messages = messages,
|
||||
MaxTokens = _maxTokens - tokensUsed,
|
||||
Temperature = 1,
|
||||
});
|
||||
|
||||
|
||||
var dataString = await data.Content.ReadAsStringAsync();
|
||||
try
|
||||
{
|
||||
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
|
||||
|
||||
// Log.Information("Received response: {Response} ", dataString);
|
||||
var res = response?.Choices?[0];
|
||||
var message = res?.Message?.Content;
|
||||
|
||||
@@ -110,14 +123,14 @@ public class OfficialGptSession : IChatterBotSession
|
||||
{
|
||||
return new Error<string>("ChatGpt: Received no response.");
|
||||
}
|
||||
|
||||
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = message,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
|
||||
|
||||
return new ThinkResult()
|
||||
{
|
||||
Text = message,
|
||||
@@ -131,11 +144,4 @@ public class OfficialGptSession : IChatterBotSession
|
||||
return new Error<string>("Unexpected response received");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ThinkResult
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public int TokensIn { get; set; }
|
||||
public int TokensOut { get; set; }
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
#nullable disable
|
||||
using System.CodeDom;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public sealed class ThinkResult
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public int TokensIn { get; set; }
|
||||
public int TokensOut { get; set; }
|
||||
}
|
@@ -8,7 +8,7 @@ namespace NadekoBot.Modules.Games.Common;
|
||||
public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 4;
|
||||
public int Version { get; set; } = 5;
|
||||
|
||||
[Comment("Hangman related settings (.hangman command)")]
|
||||
public HangmanConfig Hangman { get; set; } = new()
|
||||
@@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
}
|
||||
];
|
||||
|
||||
[Comment(@"Which chatbot API should bot use.
|
||||
'cleverbot' - bot will use Cleverbot API.
|
||||
'gpt' - bot will use GPT API")]
|
||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
|
||||
[Comment(
|
||||
"""
|
||||
Which chatbot API should bot use.
|
||||
'cleverbot' - bot will use Cleverbot API.
|
||||
'openai' - bot will use OpenAi API
|
||||
""")]
|
||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.OpenAi;
|
||||
|
||||
public ChatGptConfig ChatGpt { get; set; } = new();
|
||||
}
|
||||
@@ -114,19 +117,38 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
[Cloneable]
|
||||
public sealed partial class ChatGptConfig
|
||||
{
|
||||
[Comment(@"Which GPT Model should bot use.
|
||||
gpt35turbo - cheapest
|
||||
gpt4o - more expensive, higher quality
|
||||
")]
|
||||
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo;
|
||||
[Comment("""
|
||||
Url to any openai api compatible url.
|
||||
Make sure to modify the modelName appropriately
|
||||
DO NOT add /v1/chat/completions suffix to the url
|
||||
""")]
|
||||
public string ApiUrl { get; set; } = "https://api.openai.com";
|
||||
|
||||
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")]
|
||||
public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything.";
|
||||
[Comment("""
|
||||
Which GPT Model should bot use.
|
||||
gpt-3.5-turbo - cheapest
|
||||
gpt-4o - more expensive, higher quality
|
||||
|
||||
[Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")]
|
||||
If you are using another openai compatible api, you may use any of the models supported by that api
|
||||
""")]
|
||||
public string ModelName { get; set; } = "gpt-3.5-turbo";
|
||||
|
||||
[Comment("""
|
||||
How should the chatbot behave, what's its personality?
|
||||
This will be sent as a system message.
|
||||
Usage of this counts towards the max tokens.
|
||||
""")]
|
||||
public string PersonalityPrompt { get; set; } =
|
||||
"You are a chat bot willing to have a conversation with anyone about anything.";
|
||||
|
||||
[Comment(
|
||||
"""
|
||||
The maximum number of messages in a conversation that can be remembered.
|
||||
This will increase the number of tokens used.
|
||||
""")]
|
||||
public int ChatHistory { get; set; } = 5;
|
||||
|
||||
[Comment(@"The maximum number of tokens to use per GPT API call")]
|
||||
[Comment(@"The maximum number of tokens to use per OpenAi API call")]
|
||||
public int MaxTokens { get; set; } = 100;
|
||||
|
||||
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
|
||||
@@ -147,9 +169,9 @@ public sealed partial class TriviaConfig
|
||||
public long CurrencyReward { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Users won't be able to start trivia games which have
|
||||
a smaller win requirement than the one specified by this setting.
|
||||
""")]
|
||||
Users won't be able to start trivia games which have
|
||||
a smaller win requirement than the one specified by this setting.
|
||||
""")]
|
||||
public int MinimumWinReq { get; set; } = 1;
|
||||
}
|
||||
|
||||
@@ -163,18 +185,11 @@ public sealed partial class RaceAnimal
|
||||
public enum ChatBotImplementation
|
||||
{
|
||||
Cleverbot,
|
||||
OpenAi = 1,
|
||||
|
||||
[Obsolete]
|
||||
Gpt = 1,
|
||||
|
||||
[Obsolete]
|
||||
Gpt3 = 1,
|
||||
}
|
||||
|
||||
public enum ChatGptModel
|
||||
{
|
||||
[Obsolete]
|
||||
Gpt4,
|
||||
[Obsolete]
|
||||
Gpt432k,
|
||||
|
||||
Gpt35Turbo,
|
||||
Gpt4o,
|
||||
}
|
@@ -32,29 +32,21 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
gs => gs.ChatBot,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("gpt.apiUrl",
|
||||
gs => gs.ChatGpt.ApiUrl,
|
||||
ConfigParsers.String,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("gpt.modelName",
|
||||
gs => gs.ChatGpt.ModelName,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigParsers.String,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("gpt.personality",
|
||||
gs => gs.ChatGpt.PersonalityPrompt,
|
||||
ConfigParsers.String,
|
||||
ConfigPrinters.ToString);
|
||||
AddParsedProp("gpt.chathistory",
|
||||
gs => gs.ChatGpt.ChatHistory,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
val => val > 0);
|
||||
AddParsedProp("gpt.max_tokens",
|
||||
gs => gs.ChatGpt.MaxTokens,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
val => val > 0);
|
||||
AddParsedProp("gpt.min_tokens",
|
||||
gs => gs.ChatGpt.MinTokens,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
val => val > 0);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
@@ -78,7 +70,7 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 3;
|
||||
c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo;
|
||||
c.ChatGpt.ModelName = "gpt35turbo";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,11 +81,40 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
c.Version = 4;
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
c.ChatGpt.ModelName =
|
||||
c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k
|
||||
? ChatGptModel.Gpt4o
|
||||
: c.ChatGpt.ModelName;
|
||||
c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase)
|
||||
|| c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase)
|
||||
? "gpt-4o"
|
||||
: "gpt-3.5-turbo";
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 5)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 5;
|
||||
c.ChatBot = c.ChatBot == ChatBotImplementation.OpenAi
|
||||
? ChatBotImplementation.OpenAi
|
||||
: c.ChatBot;
|
||||
|
||||
if (c.ChatGpt.ModelName.Equals("gpt4o", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
c.ChatGpt.ModelName = "gpt-4o";
|
||||
}
|
||||
else if (c.ChatGpt.ModelName.Equals("gpt35turbo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
c.ChatGpt.ModelName = "gpt-3.5-turbo";
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning(
|
||||
"Unknown OpenAI api model name: {ModelName}. "
|
||||
+ "It will be reset to 'gpt-3.5-turbo' only this time",
|
||||
c.ChatGpt.ModelName);
|
||||
c.ChatGpt.ModelName = "gpt-3.5-turbo";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -519,31 +519,10 @@ public sealed partial class Help : NadekoModule<HelpService>
|
||||
"https://nadekobot.readthedocs.io/en/latest/"))
|
||||
.SendAsync();
|
||||
|
||||
|
||||
private Task SelfhostAction(SocketMessageComponent smc)
|
||||
=> smc.RespondConfirmAsync(_sender,
|
||||
"""
|
||||
- In case you don't want or cannot Donate to NadekoBot project, but you
|
||||
- NadekoBot is a free and [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own "selfhosted" instance on your computer.
|
||||
|
||||
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
|
||||
|
||||
- You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
|
||||
- If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)
|
||||
""",
|
||||
true);
|
||||
|
||||
[Cmd]
|
||||
[OnlyPublicBot]
|
||||
public async Task Donate()
|
||||
{
|
||||
var selfhostInter = _inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder(
|
||||
emote: new Emoji("🖥️"),
|
||||
customId: "donate:selfhosting",
|
||||
label: "Selfhosting"),
|
||||
SelfhostAction);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("Thank you for considering to donate to the NadekoBot project!");
|
||||
@@ -580,7 +559,6 @@ public sealed partial class Help : NadekoModule<HelpService>
|
||||
await Response()
|
||||
.Channel(await ctx.User.CreateDMChannelAsync())
|
||||
.Embed(eb)
|
||||
.Interaction(selfhostInter)
|
||||
.SendAsync();
|
||||
|
||||
_ = ctx.OkAsync();
|
||||
|
@@ -16,12 +16,12 @@ public partial class Searches
|
||||
_stocksService = stocksService;
|
||||
_stockDrawingService = stockDrawingService;
|
||||
}
|
||||
|
||||
|
||||
[Cmd]
|
||||
public async Task Stock([Leftover]string query)
|
||||
public async Task Stock([Leftover] string query)
|
||||
{
|
||||
using var typing = ctx.Channel.EnterTypingState();
|
||||
|
||||
|
||||
var stock = await _stocksService.GetStockDataAsync(query);
|
||||
|
||||
if (stock is null)
|
||||
@@ -36,9 +36,9 @@ public partial class Searches
|
||||
|
||||
var symbol = symbols.First();
|
||||
var promptEmbed = _sender.CreateEmbed()
|
||||
.WithDescription(symbol.Description)
|
||||
.WithTitle(GetText(strs.did_you_mean(symbol.Symbol)));
|
||||
|
||||
.WithDescription(symbol.Description)
|
||||
.WithTitle(GetText(strs.did_you_mean(symbol.Symbol)));
|
||||
|
||||
if (!await PromptUserConfirmAsync(promptEmbed))
|
||||
return;
|
||||
|
||||
@@ -54,7 +54,7 @@ public partial class Searches
|
||||
|
||||
var candles = await _stocksService.GetCandleDataAsync(query);
|
||||
var stockImageTask = _stockDrawingService.GenerateCombinedChartAsync(candles);
|
||||
|
||||
|
||||
var localCulture = (CultureInfo)Culture.Clone();
|
||||
localCulture.NumberFormat.CurrencySymbol = "$";
|
||||
|
||||
@@ -64,34 +64,34 @@ public partial class Searches
|
||||
|
||||
var change = (stock.Price - stock.Close).ToString("N2", Culture);
|
||||
var changePercent = (1 - (stock.Close / stock.Price)).ToString("P1", Culture);
|
||||
|
||||
|
||||
var sign50 = stock.Change50d >= 0
|
||||
? "\\🔼"
|
||||
: "\\🔻";
|
||||
|
||||
var change50 = (stock.Change50d).ToString("P1", Culture);
|
||||
|
||||
|
||||
var sign200 = stock.Change200d >= 0
|
||||
? "\\🔼"
|
||||
: "\\🔻";
|
||||
|
||||
|
||||
var change200 = (stock.Change200d).ToString("P1", Culture);
|
||||
|
||||
|
||||
var price = stock.Price.ToString("C2", localCulture);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(stock.Symbol)
|
||||
.WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}")
|
||||
.WithTitle(stock.Name)
|
||||
.AddField(GetText(strs.price), $"{sign} **{price}**", true)
|
||||
.AddField(GetText(strs.market_cap), stock.MarketCap, true)
|
||||
.AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true)
|
||||
.AddField("Change", $"{change} ({changePercent})", true)
|
||||
// .AddField("Change 50d", $"{sign50}{change50}", true)
|
||||
// .AddField("Change 200d", $"{sign200}{change200}", true)
|
||||
.WithFooter(stock.Exchange);
|
||||
|
||||
.WithOkColor()
|
||||
.WithAuthor(stock.Symbol)
|
||||
.WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}")
|
||||
.WithTitle(stock.Name)
|
||||
.AddField(GetText(strs.price), $"{sign} **{price}**", true)
|
||||
.AddField(GetText(strs.market_cap), stock.MarketCap, true)
|
||||
.AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true)
|
||||
.AddField("Change", $"{change} ({changePercent})", true)
|
||||
// .AddField("Change 50d", $"{sign50}{change50}", true)
|
||||
// .AddField("Change 200d", $"{sign200}{change200}", true)
|
||||
.WithFooter(stock.Exchange);
|
||||
|
||||
var message = await Response().Embed(eb).SendAsync();
|
||||
await using var imageData = await stockImageTask;
|
||||
if (imageData is null)
|
||||
@@ -105,15 +105,12 @@ public partial class Searches
|
||||
await message.ModifyAsync(mp =>
|
||||
{
|
||||
mp.Attachments =
|
||||
new(new[]
|
||||
{
|
||||
attachment
|
||||
});
|
||||
new(new[] { attachment });
|
||||
|
||||
mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Cmd]
|
||||
public async Task Crypto(string name)
|
||||
@@ -128,9 +125,9 @@ public partial class Searches
|
||||
if (nearest is not null)
|
||||
{
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithTitle(GetText(strs.crypto_not_found))
|
||||
.WithDescription(
|
||||
GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
|
||||
.WithTitle(GetText(strs.crypto_not_found))
|
||||
.WithDescription(
|
||||
GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
|
||||
|
||||
if (await PromptUserConfirmAsync(embed))
|
||||
crypto = nearest;
|
||||
@@ -146,7 +143,7 @@ public partial class Searches
|
||||
|
||||
var localCulture = (CultureInfo)Culture.Clone();
|
||||
localCulture.NumberFormat.CurrencySymbol = "$";
|
||||
|
||||
|
||||
var sevenDay = (usd.PercentChange7d / 100).ToString("P2", localCulture);
|
||||
var lastDay = (usd.PercentChange24h / 100).ToString("P2", localCulture);
|
||||
var price = usd.Price < 0.01
|
||||
@@ -159,28 +156,29 @@ public partial class Searches
|
||||
|
||||
await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0);
|
||||
var fileName = $"{crypto.Slug}_7d.png";
|
||||
|
||||
|
||||
var toSend = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor($"#{crypto.CmcRank}")
|
||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||
.AddField(GetText(strs.market_cap), marketCap, true)
|
||||
.AddField(GetText(strs.price), price, true)
|
||||
.AddField(GetText(strs.volume_24h), volume, true)
|
||||
.AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
|
||||
.AddField(GetText(strs.market_cap_dominance), dominance, true)
|
||||
.WithImageUrl($"attachment://{fileName}");
|
||||
.WithOkColor()
|
||||
.WithAuthor($"#{crypto.CmcRank}")
|
||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||
.WithThumbnailUrl(
|
||||
$"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||
.AddField(GetText(strs.market_cap), marketCap, true)
|
||||
.AddField(GetText(strs.price), price, true)
|
||||
.AddField(GetText(strs.volume_24h), volume, true)
|
||||
.AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
|
||||
.AddField(GetText(strs.market_cap_dominance), dominance, true)
|
||||
.WithImageUrl($"attachment://{fileName}");
|
||||
|
||||
if (crypto.CirculatingSupply is double cs)
|
||||
{
|
||||
var csStr = cs.ToString("N0", localCulture);
|
||||
|
||||
|
||||
if (crypto.MaxSupply is double ms)
|
||||
{
|
||||
var perc = (cs / ms).ToString("P1", localCulture);
|
||||
|
||||
|
||||
toSend.AddField(GetText(strs.circulating_supply), $"{csStr} ({perc})", true);
|
||||
}
|
||||
else
|
||||
@@ -192,5 +190,54 @@ public partial class Searches
|
||||
|
||||
await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Coins(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
if (page > 25)
|
||||
page = 25;
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(async (page) =>
|
||||
{
|
||||
var coins = await _service.GetTopCoins(page);
|
||||
return coins;
|
||||
})
|
||||
.PageSize(10)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithOkColor();
|
||||
|
||||
if (items.Count > 0)
|
||||
{
|
||||
foreach (var coin in items)
|
||||
{
|
||||
embed.AddField($"#{coin.MarketCapRank} {coin.Symbol} - {coin.Name}",
|
||||
$"""
|
||||
`Price:` {GetArrowEmoji(coin.PercentChange24h)} {coin.CurrentPrice.ToShortString()}$ ({GetSign(coin.PercentChange24h)}{Math.Round(coin.PercentChange24h, 2)}%)
|
||||
`MarketCap:` {coin.MarketCap.ToShortString()}$
|
||||
`Supply:` {(coin.CirculatingSupply?.ToShortString() ?? "?")} / {(coin.TotalSupply?.ToShortString() ?? "?")}
|
||||
""",
|
||||
inline: false);
|
||||
}
|
||||
}
|
||||
|
||||
return embed;
|
||||
})
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private static string GetArrowEmoji(decimal value)
|
||||
=> value > 0 ? "▲" : "▼";
|
||||
|
||||
private static string GetSign(decimal value)
|
||||
=> value >= 0 ? "+" : "-";
|
||||
}
|
||||
}
|
@@ -4,8 +4,10 @@ using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using StringExtensions = NadekoBot.Extensions.StringExtensions;
|
||||
@@ -212,4 +214,55 @@ public class CryptoService : INService
|
||||
var points = GetSparklinePointsFromSvgText(str);
|
||||
return points;
|
||||
}
|
||||
|
||||
private static TypedKey<IReadOnlyCollection<GeckoCoinsResult>> GetTopCoinsKey()
|
||||
=> new($"crypto:top_coins");
|
||||
|
||||
public async Task<IReadOnlyCollection<GeckoCoinsResult>?> GetTopCoins(int page)
|
||||
{
|
||||
if (page >= 25)
|
||||
page = 24;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
|
||||
http.AddFakeHeaders();
|
||||
|
||||
var result = await _cache.GetOrAddAsync<IReadOnlyCollection<GeckoCoinsResult>>(GetTopCoinsKey(),
|
||||
async () => await http.GetFromJsonAsync<List<GeckoCoinsResult>>(
|
||||
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250")
|
||||
?? [],
|
||||
expiry: TimeSpan.FromHours(1));
|
||||
|
||||
return result!.Skip(page * 10).Take(10).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GeckoCoinsResult
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("symbol")]
|
||||
public required string Symbol { get; init; }
|
||||
|
||||
[JsonPropertyName("current_price")]
|
||||
public required decimal CurrentPrice { get; init; }
|
||||
|
||||
[JsonPropertyName("price_change_percentage_24h")]
|
||||
public required decimal PercentChange24h { get; init; }
|
||||
|
||||
[JsonPropertyName("market_cap")]
|
||||
public required decimal MarketCap { get; init; }
|
||||
|
||||
[JsonPropertyName("circulating_supply")]
|
||||
public required decimal? CirculatingSupply { get; init; }
|
||||
|
||||
[JsonPropertyName("total_supply")]
|
||||
public required decimal? TotalSupply { get; init; }
|
||||
|
||||
[JsonPropertyName("market_cap_rank")]
|
||||
public required int MarketCapRank { get; init; }
|
||||
}
|
102
src/NadekoBot/Modules/Searches/ReligiousCommands.cs
Normal file
102
src/NadekoBot/Modules/Searches/ReligiousCommands.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public partial class Searches
|
||||
{
|
||||
public partial class ReligiousCommands : NadekoModule
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public ReligiousCommands(IHttpClientFactory httpFactory)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Bible(string book, string chapterAndVerse)
|
||||
{
|
||||
var obj = new BibleVerses();
|
||||
try
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
obj = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0)
|
||||
await Response().Error(obj.Error ?? "No verse found.").SendAsync();
|
||||
else
|
||||
{
|
||||
var v = obj.Verses[0];
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
||||
.WithDescription(v.Text))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Quran(string ayah)
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
|
||||
var obj = await http.GetFromJsonAsync<QuranResponse<QuranAyah>>($"https://api.alquran.cloud/v1/ayah/{Uri.EscapeDataString(ayah)}/editions/en.asad,ar.alafasy");
|
||||
if(obj is null or not { Code: 200 })
|
||||
{
|
||||
await Response().Error("No verse found.").SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var english = obj.Data[0];
|
||||
var arabic = obj.Data[1];
|
||||
|
||||
await using var audio = await http.GetStreamAsync(arabic.Audio);
|
||||
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Arabic", arabic.Text)
|
||||
.AddField("English", english.Text)
|
||||
.WithFooter(arabic.Number.ToString()))
|
||||
.File(audio, Uri.EscapeDataString(ayah) + ".mp3")
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class QuranResponse<T>
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public T[] Data { get; set; }
|
||||
}
|
||||
|
||||
public sealed class QuranAyah
|
||||
{
|
||||
[JsonPropertyName("number")]
|
||||
public int Number { get; set; }
|
||||
|
||||
[JsonPropertyName("audio")]
|
||||
public string Audio { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
|
||||
}
|
@@ -32,20 +32,21 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
var instanceUrl = GetRandomInstance();
|
||||
|
||||
Log.Information("Using {Instance} instance for web search...", instanceUrl);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
|
||||
using var http = _http.CreateClient();
|
||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||
+ $"?q={Uri.EscapeDataString(query)}"
|
||||
+ $"&format=json"
|
||||
+ $"&strict=2");
|
||||
|
||||
sw.Stop();
|
||||
var elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
|
||||
|
||||
if (dat is null)
|
||||
return new SearxSearchResult();
|
||||
|
||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
return dat;
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
var instanceUrl = GetRandomInstance();
|
||||
|
||||
Log.Information("Using {Instance} instance for img search...", instanceUrl);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
using var http = _http.CreateClient();
|
||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||
+ $"?q={Uri.EscapeDataString(query)}"
|
||||
@@ -64,13 +65,13 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
+ $"&category_images=on"
|
||||
+ $"&strict=2");
|
||||
|
||||
sw.Stop();
|
||||
var elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
|
||||
|
||||
if (dat is null)
|
||||
return new SearxImageSearchResult();
|
||||
|
||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
return dat;
|
||||
}
|
||||
}
|
@@ -528,34 +528,6 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Bible(string book, string chapterAndVerse)
|
||||
{
|
||||
var obj = new BibleVerses();
|
||||
try
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
obj = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0)
|
||||
await Response().Error(obj.Error ?? "No verse found.").SendAsync();
|
||||
else
|
||||
{
|
||||
var v = obj.Verses[0];
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
||||
.WithDescription(v.Text))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Steam([Leftover] string query)
|
||||
{
|
||||
|
@@ -251,7 +251,7 @@ public sealed class AiAssistantService
|
||||
return false;
|
||||
|
||||
await _cbs.RunChatterBot(sg, msg, channel, sess, query);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var commandString = GetCommandString(model);
|
||||
|
@@ -8,9 +8,9 @@ public sealed class CommandPromptResultModel
|
||||
public required string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("arguments")]
|
||||
public required Dictionary<string, string> Arguments { get; set; }
|
||||
|
||||
public Dictionary<string, string> Arguments { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("remaining")]
|
||||
[JsonConverter(typeof(NumberToStringConverter))]
|
||||
public required string Remaining { get; set; }
|
||||
public string Remaining { get; set; } = string.Empty;
|
||||
}
|
@@ -459,47 +459,82 @@ public partial class Utility : NadekoModule
|
||||
public async Task StickerAdd(string name = null, string description = null, params string[] tags)
|
||||
{
|
||||
string format;
|
||||
Stream stream;
|
||||
|
||||
if (ctx.Message.Stickers.Count is 1 && ctx.Message.Stickers.First() is SocketSticker ss)
|
||||
{
|
||||
name ??= ss.Name;
|
||||
description = ss.Description;
|
||||
tags = tags is null or { Length: 0 } ? ss.Tags.ToArray() : tags;
|
||||
format = FormatToExtension(ss.Format);
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
stream = await http.GetStreamAsync(ss.GetStickerUrl());
|
||||
}
|
||||
else
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
Stream stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (tags.Length == 0)
|
||||
tags = [name];
|
||||
if (ctx.Message.Stickers.Count is 1 && ctx.Message.Stickers.First() is SocketSticker ss)
|
||||
{
|
||||
name ??= ss.Name;
|
||||
description = ss.Description;
|
||||
tags = tags is null or { Length: 0 } ? ss.Tags.ToArray() : tags;
|
||||
format = FormatToExtension(ss.Format);
|
||||
|
||||
await ctx.Guild.CreateStickerAsync(
|
||||
name,
|
||||
stream,
|
||||
$"{name}.{format}",
|
||||
tags,
|
||||
string.IsNullOrWhiteSpace(description) ? "Missing description" : description
|
||||
);
|
||||
using var http = _httpFactory.CreateClient();
|
||||
stream = await http.GetStreamAsync(ss.GetStickerUrl());
|
||||
}
|
||||
else if (ctx.Message.Attachments.Count is 1 && name is not null)
|
||||
{
|
||||
if (tags.Length == 0)
|
||||
tags = [name];
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error occurred while adding a sticker: {Message}", ex.Message);
|
||||
await Response().Error(strs.error_occured).SendAsync();
|
||||
if (ctx.Message.Attachments.Count != 1)
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var attach = ctx.Message.Attachments.First();
|
||||
|
||||
|
||||
if (attach.Size > 512_000 || attach.Width != 300 || attach.Height != 300)
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
format = attach.Filename
|
||||
.Split('.')
|
||||
.Last()
|
||||
.ToLowerInvariant();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format) || (format != "png" && format != "apng"))
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
stream = await http.GetStreamAsync(attach.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ctx.Guild.CreateStickerAsync(
|
||||
name,
|
||||
stream,
|
||||
$"{name}.{format}",
|
||||
tags,
|
||||
string.IsNullOrWhiteSpace(description) ? "Missing description" : description
|
||||
);
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch
|
||||
(Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error occurred while adding a sticker: {Message}", ex.Message);
|
||||
await Response().Error(strs.error_occured).SendAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await stream.DisposeAsync();
|
||||
await (stream?.DisposeAsync() ?? ValueTask.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -123,6 +123,8 @@ public partial class Xp
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var rank = await _service.GetClubRankAsync(club.Id);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allUsers)
|
||||
@@ -135,6 +137,7 @@ public partial class Xp
|
||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||
.AddField(GetText(strs.desc),
|
||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||
.AddField(GetText(strs.rank), $"#{rank}", true)
|
||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||
.AddField(GetText(strs.members),
|
||||
|
@@ -23,22 +23,22 @@ public class ClubService : INService, IClubService
|
||||
{
|
||||
if (!CheckClubName(clubName))
|
||||
return ClubCreateResult.NameTooLong;
|
||||
|
||||
|
||||
//must be lvl 5 and must not be in a club already
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user);
|
||||
var xp = new LevelStats(du.TotalXp);
|
||||
|
||||
if (xp.Level < 5)
|
||||
|
||||
if (xp.Level < 5)
|
||||
return ClubCreateResult.InsufficientLevel;
|
||||
|
||||
|
||||
if (du.ClubId is not null)
|
||||
return ClubCreateResult.AlreadyInAClub;
|
||||
|
||||
if (await uow.Set<ClubInfo>().AnyAsyncEF(x => x.Name == clubName))
|
||||
return ClubCreateResult.NameTaken;
|
||||
|
||||
|
||||
du.IsClubAdmin = true;
|
||||
du.Club = new()
|
||||
{
|
||||
@@ -53,7 +53,7 @@ public class ClubService : INService, IClubService
|
||||
|
||||
return ClubCreateResult.Success;
|
||||
}
|
||||
|
||||
|
||||
public OneOf<ClubInfo, ClubTransferError> TransferClub(IUser from, IUser newOwner)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
@@ -62,7 +62,7 @@ public class ClubService : INService, IClubService
|
||||
|
||||
if (club is null || club.Owner.UserId != from.Id)
|
||||
return ClubTransferError.NotOwner;
|
||||
|
||||
|
||||
if (!club.Members.Contains(newOwnerUser))
|
||||
return ClubTransferError.TargetNotMember;
|
||||
|
||||
@@ -72,22 +72,22 @@ public class ClubService : INService, IClubService
|
||||
uow.SaveChanges();
|
||||
return club;
|
||||
}
|
||||
|
||||
|
||||
public async Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin)
|
||||
{
|
||||
if (owner.Id == toAdmin.Id)
|
||||
return ToggleAdminResult.CantTargetThyself;
|
||||
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(owner.Id);
|
||||
var adminUser = uow.GetOrCreateUser(toAdmin);
|
||||
|
||||
if (club is null)
|
||||
return ToggleAdminResult.NotOwner;
|
||||
|
||||
if(!club.Members.Contains(adminUser))
|
||||
|
||||
if (!club.Members.Contains(adminUser))
|
||||
return ToggleAdminResult.TargetNotMember;
|
||||
|
||||
|
||||
var newState = adminUser.IsClubAdmin = !adminUser.IsClubAdmin;
|
||||
await uow.SaveChangesAsync();
|
||||
return newState ? ToggleAdminResult.AddedAdmin : ToggleAdminResult.RemovedAdmin;
|
||||
@@ -99,17 +99,17 @@ public class ClubService : INService, IClubService
|
||||
var member = uow.Set<ClubInfo>().GetByMember(user.Id);
|
||||
return member;
|
||||
}
|
||||
|
||||
|
||||
public async Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string url)
|
||||
{
|
||||
if (url is not null)
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
using var temp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (!temp.IsImage())
|
||||
|
||||
if (!temp.IsImage())
|
||||
return SetClubIconResult.InvalidFileType;
|
||||
|
||||
|
||||
if (temp.GetContentLength() > 5.Megabytes())
|
||||
return SetClubIconResult.TooLarge;
|
||||
}
|
||||
@@ -134,6 +134,18 @@ public class ClubService : INService, IClubService
|
||||
return club is not null;
|
||||
}
|
||||
|
||||
public async Task<int> GetClubRankAsync(int clubId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var rank = await uow.Clubs
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.Xp > (uow.Clubs.First(c => c.Id == clubId).Xp))
|
||||
.CountAsyncLinqToDB();
|
||||
|
||||
return rank + 1;
|
||||
}
|
||||
|
||||
public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
@@ -144,10 +156,10 @@ public class ClubService : INService, IClubService
|
||||
// or doesn't min minumum level requirement, can't apply
|
||||
if (du.ClubId is not null)
|
||||
return ClubApplyResult.AlreadyInAClub;
|
||||
|
||||
|
||||
if (club.Bans.Any(x => x.UserId == du.Id))
|
||||
return ClubApplyResult.Banned;
|
||||
|
||||
|
||||
if (club.Applicants.Any(x => x.UserId == du.Id))
|
||||
return ClubApplyResult.AlreadyApplied;
|
||||
|
||||
@@ -162,7 +174,7 @@ public class ClubService : INService, IClubService
|
||||
return ClubApplyResult.Success;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
|
||||
{
|
||||
discordUser = null;
|
||||
@@ -188,7 +200,7 @@ public class ClubService : INService, IClubService
|
||||
uow.SaveChanges();
|
||||
return ClubAcceptResult.Accepted;
|
||||
}
|
||||
|
||||
|
||||
public ClubDenyResult RejectApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
|
||||
{
|
||||
discordUser = null;
|
||||
@@ -201,9 +213,9 @@ public class ClubService : INService, IClubService
|
||||
club.Applicants.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
|
||||
if (applicant is null)
|
||||
return ClubDenyResult.NoSuchApplicant;
|
||||
|
||||
|
||||
club.Applicants.Remove(applicant);
|
||||
|
||||
|
||||
discordUser = applicant.User;
|
||||
uow.SaveChanges();
|
||||
return ClubDenyResult.Rejected;
|
||||
@@ -220,7 +232,7 @@ public class ClubService : INService, IClubService
|
||||
using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user, x => x.Include(u => u.Club));
|
||||
if (du.Club is null)
|
||||
return ClubLeaveResult.NotInAClub;
|
||||
return ClubLeaveResult.NotInAClub;
|
||||
if (du.Club.OwnerId == du.Id)
|
||||
return ClubLeaveResult.OwnerCantLeave;
|
||||
|
||||
@@ -306,7 +318,7 @@ public class ClubService : INService, IClubService
|
||||
return ClubUnbanResult.Success;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
@@ -342,14 +354,14 @@ public class ClubService : INService, IClubService
|
||||
{
|
||||
if (!CheckClubName(clubName))
|
||||
return ClubRenameResult.NameTooLong;
|
||||
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
|
||||
var club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(userId);
|
||||
|
||||
|
||||
if (club is null)
|
||||
return ClubRenameResult.NotOwnerOrAdmin;
|
||||
|
||||
|
||||
if (await uow.Set<ClubInfo>().AnyAsyncEF(x => x.Name == clubName))
|
||||
return ClubRenameResult.NameTaken;
|
||||
|
||||
|
@@ -23,6 +23,7 @@ public interface IClubService
|
||||
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
|
||||
List<ClubInfo> GetClubLeaderboardPage(int page);
|
||||
Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName);
|
||||
Task<int> GetClubRankAsync(int clubId);
|
||||
}
|
||||
|
||||
public enum ClubApplyResult
|
||||
|
@@ -480,7 +480,8 @@ public partial class Xp : NadekoModule<XpService>
|
||||
ctx.User.Id,
|
||||
button,
|
||||
OnShopUse,
|
||||
(key, itemType));
|
||||
(key, itemType),
|
||||
clearAfter: false);
|
||||
|
||||
return inter;
|
||||
}
|
||||
@@ -494,7 +495,9 @@ public partial class Xp : NadekoModule<XpService>
|
||||
ctx.User.Id,
|
||||
button,
|
||||
OnShopBuy,
|
||||
(key, itemType));
|
||||
(key, itemType),
|
||||
singleUse: true,
|
||||
clearAfter: false);
|
||||
|
||||
return inter;
|
||||
}
|
||||
@@ -577,6 +580,10 @@ public partial class Xp : NadekoModule<XpService>
|
||||
{
|
||||
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)
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>5.1.0</Version>
|
||||
<Version>5.1.4</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
@@ -73,7 +73,7 @@
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/>
|
||||
<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="SharpToken" Version="2.0.3" />
|
||||
|
||||
|
@@ -133,52 +133,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||
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()
|
||||
{
|
||||
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))
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
if (creds.Version <= 7)
|
||||
if (creds.Version <= 8)
|
||||
{
|
||||
creds.Version = 8;
|
||||
creds.Version = 9;
|
||||
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
||||
}
|
||||
}
|
||||
|
@@ -147,4 +147,5 @@ public static class StringExtensions
|
||||
var newString = str.UnescapeUnicodeCodePoint();
|
||||
return newString;
|
||||
});
|
||||
|
||||
}
|
@@ -31,10 +31,10 @@ public sealed class Creds : IBotCredentials
|
||||
|
||||
[Comment("""
|
||||
Pledge 5$ or more on https://patreon.com/nadekobot and connect your discord account to Patreon.
|
||||
Go to https://dashy.nadeko.bot and login with your discord account
|
||||
Go to https://dashy.nadeko.bot/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
|
||||
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.
|
||||
""")]
|
||||
public string NadekoAiToken { get; set; }
|
||||
|
||||
@@ -156,7 +156,7 @@ public sealed class Creds : IBotCredentials
|
||||
|
||||
public Creds()
|
||||
{
|
||||
Version = 7;
|
||||
Version = 9;
|
||||
Token = string.Empty;
|
||||
UsePrivilegedIntents = true;
|
||||
OwnerIds = new List<ulong>();
|
||||
|
@@ -6,14 +6,16 @@ public interface INadekoInteractionService
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onTrigger,
|
||||
bool singleUse = true);
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true);
|
||||
|
||||
public NadekoInteractionBase Create<T>(
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, T, Task> onTrigger,
|
||||
in T state,
|
||||
bool singleUse = true);
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true);
|
||||
|
||||
NadekoInteractionBase Create(
|
||||
ulong userId,
|
||||
|
@@ -8,8 +8,9 @@ public sealed class NadekoButtonInteractionHandler : NadekoInteractionBase
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onAction,
|
||||
bool onlyAuthor,
|
||||
bool singleUse = true)
|
||||
: base(client, authorId, button.CustomId, onAction, onlyAuthor, singleUse)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true)
|
||||
: base(client, authorId, button.CustomId, onAction, onlyAuthor, singleUse, clearAfter)
|
||||
{
|
||||
Button = button;
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ public abstract class NadekoInteractionBase
|
||||
private IUserMessage message = null!;
|
||||
private readonly string _customId;
|
||||
private readonly bool _singleUse;
|
||||
private readonly bool _clearAfter;
|
||||
|
||||
public NadekoInteractionBase(
|
||||
DiscordSocketClient client,
|
||||
@@ -19,13 +20,16 @@ public abstract class NadekoInteractionBase
|
||||
string customId,
|
||||
Func<SocketMessageComponent, Task> onAction,
|
||||
bool onlyAuthor,
|
||||
bool singleUse = true)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true)
|
||||
{
|
||||
_authorId = authorId;
|
||||
_customId = customId;
|
||||
_onAction = onAction;
|
||||
_onlyAuthor = onlyAuthor;
|
||||
_singleUse = singleUse;
|
||||
_clearAfter = clearAfter;
|
||||
|
||||
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Client = client;
|
||||
@@ -36,13 +40,11 @@ public abstract class NadekoInteractionBase
|
||||
message = msg;
|
||||
|
||||
Client.InteractionCreated += OnInteraction;
|
||||
if (_singleUse)
|
||||
await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task);
|
||||
else
|
||||
await Task.Delay(30_000);
|
||||
await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task);
|
||||
Client.InteractionCreated -= OnInteraction;
|
||||
|
||||
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
|
||||
if (_clearAfter)
|
||||
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
|
||||
}
|
||||
|
||||
private Task OnInteraction(SocketInteraction arg)
|
||||
@@ -59,11 +61,15 @@ public abstract class NadekoInteractionBase
|
||||
if (smc.Data.CustomId != _customId)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_interactionCompletedSource.Task.IsCompleted)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_interactionCompletedSource.TrySetResult(true);
|
||||
if (_singleUse)
|
||||
_interactionCompletedSource.TrySetResult(true);
|
||||
await ExecuteOnActionAsync(smc);
|
||||
|
||||
if (!smc.HasResponded)
|
||||
|
@@ -13,25 +13,30 @@ public class NadekoInteractionService : INadekoInteractionService, INService
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onTrigger,
|
||||
bool singleUse = true)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true)
|
||||
=> new NadekoButtonInteractionHandler(_client,
|
||||
userId,
|
||||
button,
|
||||
onTrigger,
|
||||
onlyAuthor: true,
|
||||
singleUse: singleUse);
|
||||
singleUse: singleUse,
|
||||
clearAfter: clearAfter);
|
||||
|
||||
public NadekoInteractionBase Create<T>(
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, T, Task> onTrigger,
|
||||
in T state,
|
||||
bool singleUse = true)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true
|
||||
)
|
||||
=> Create(userId,
|
||||
button,
|
||||
((Func<T, Func<SocketMessageComponent, Task>>)((data)
|
||||
=> smc => onTrigger(smc, data)))(state),
|
||||
singleUse);
|
||||
singleUse,
|
||||
clearAfter);
|
||||
|
||||
public NadekoInteractionBase Create(
|
||||
ulong userId,
|
||||
|
@@ -1,45 +0,0 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,8 +12,8 @@ public partial class ResponseBuilder
|
||||
private readonly DiscordSocketClient _client;
|
||||
private int currentPage;
|
||||
|
||||
private NadekoButtonInteractionHandler left;
|
||||
private NadekoButtonInteractionHandler right;
|
||||
private NadekoButtonInteractionHandler? left;
|
||||
private NadekoButtonInteractionHandler? right;
|
||||
private NadekoInteractionBase? extra;
|
||||
|
||||
public PaginationSender(
|
||||
@@ -71,7 +71,8 @@ public partial class ResponseBuilder
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
true,
|
||||
singleUse: false);
|
||||
singleUse: false,
|
||||
clearAfter: false);
|
||||
|
||||
if (_paginationBuilder.InteractionFunc is not null)
|
||||
{
|
||||
@@ -106,7 +107,8 @@ public partial class ResponseBuilder
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
true,
|
||||
singleUse: false);
|
||||
singleUse: false,
|
||||
clearAfter: false);
|
||||
|
||||
return (leftBtnInter, maybeInter, rightBtnInter);
|
||||
}
|
||||
@@ -120,8 +122,8 @@ public partial class ResponseBuilder
|
||||
if (_paginationBuilder.AddPaginatedFooter)
|
||||
toSend.AddPaginatedFooter(currentPage, lastPage);
|
||||
|
||||
left.SetCompleted();
|
||||
right.SetCompleted();
|
||||
left?.SetCompleted();
|
||||
right?.SetCompleted();
|
||||
extra?.SetCompleted();
|
||||
(left, extra, right) = (await GetInteractions());
|
||||
|
||||
|
@@ -229,10 +229,4 @@ public static class Extensions
|
||||
public static IEnumerable<IRole> GetRoles(this IGuildUser user)
|
||||
=> 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();
|
||||
}
|
||||
}
|
@@ -1,7 +1,30 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class NumberExtensions
|
||||
{
|
||||
public static DateTimeOffset ToUnixTimestamp(this double number)
|
||||
=> new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number);
|
||||
|
||||
public static string ToShortString(this decimal value)
|
||||
{
|
||||
if (value <= 1_000)
|
||||
return Math.Round(value, 2).ToString(CultureInfo.InvariantCulture);
|
||||
if (value <= 1_000_000)
|
||||
return Math.Round(value, 1).ToString(CultureInfo.InvariantCulture);
|
||||
var tokens = " MBtq";
|
||||
var i = 2;
|
||||
while (true)
|
||||
{
|
||||
var num = (decimal)Math.Pow(1000, i);
|
||||
if (num > value)
|
||||
{
|
||||
var num2 = (decimal)Math.Pow(1000, i - 1);
|
||||
return $"{Math.Round((value / num2), 1)}{tokens[i - 1]}".Trim();
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
# DO NOT CHANGE
|
||||
version: 7
|
||||
version: 9
|
||||
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
|
||||
token: ''
|
||||
# List of Ids of the users who have bot owner permissions
|
||||
@@ -13,6 +13,12 @@ usePrivilegedIntents: true
|
||||
# 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 NadekoBot.Coordinator to start the bot, and it will correctly override this value.
|
||||
totalShards: 1
|
||||
# Pledge 5$ or more on https://patreon.com/nadekobot and connect your discord account to Patreon.
|
||||
# Go to https://dashy.nadeko.bot/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.
|
||||
nadekoAiToken:
|
||||
# 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.
|
||||
# Used only for Youtube Data Api (at the moment).
|
||||
@@ -56,7 +62,7 @@ patreon:
|
||||
botListToken: ''
|
||||
# Official cleverbot api key.
|
||||
cleverbotApiKey: ''
|
||||
# Official GPT-3 api key.
|
||||
# OpenAi api key.
|
||||
gpt3ApiKey: ''
|
||||
# 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.
|
||||
|
@@ -30,6 +30,8 @@ greettest:
|
||||
- greettest
|
||||
greetdmtest:
|
||||
- greetdmtest
|
||||
boosttest:
|
||||
- boosttest
|
||||
byetest:
|
||||
- byetest
|
||||
boost:
|
||||
@@ -1192,6 +1194,8 @@ xpreset:
|
||||
- xpreset
|
||||
bible:
|
||||
- bible
|
||||
quran:
|
||||
- quran
|
||||
edit:
|
||||
- edit
|
||||
delete:
|
||||
@@ -1400,4 +1404,10 @@ stickyroles:
|
||||
cleanupguilddata:
|
||||
- cleanupguilddata
|
||||
prompt:
|
||||
- prompt
|
||||
- prompt
|
||||
honeypot:
|
||||
- honeypot
|
||||
coins:
|
||||
- coins
|
||||
- crypto
|
||||
- cryptos
|
@@ -1,5 +1,5 @@
|
||||
# DO NOT CHANGE
|
||||
version: 4
|
||||
version: 5
|
||||
# Hangman related settings (.hangman command)
|
||||
hangman:
|
||||
# The amount of currency awarded to the winner of a hangman game
|
||||
@@ -56,19 +56,27 @@ raceAnimals:
|
||||
name: Unicorn
|
||||
# Which chatbot API should bot use.
|
||||
# 'cleverbot' - bot will use Cleverbot API.
|
||||
# 'gpt' - bot will use GPT API
|
||||
chatBot: Gpt
|
||||
# 'openai' - bot will use OpenAi API
|
||||
chatBot: OpenAi
|
||||
chatGpt:
|
||||
# Url to any openai api compatible url.
|
||||
# Make sure to modify the modelName appropriately
|
||||
# DO NOT add /v1/chat/completions suffix to the url
|
||||
apiUrl: https://api.openai.com
|
||||
# Which GPT Model should bot use.
|
||||
# gpt35turbo - cheapest
|
||||
# gpt4o - more expensive, higher quality
|
||||
# gpt-3.5-turbo - cheapest
|
||||
# gpt-4o - more expensive, higher quality
|
||||
#
|
||||
modelName: Gpt35Turbo
|
||||
# How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)
|
||||
# If you are using another openai compatible api, you may use any of the models supported by that api
|
||||
modelName: gpt-3.5-turbo
|
||||
# How should the chatbot behave, what's its personality?
|
||||
# This will be sent as a system message.
|
||||
# Usage of this counts towards the max tokens.
|
||||
personalityPrompt: You are a chat bot willing to have a conversation with anyone about anything.
|
||||
# The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)
|
||||
# The maximum number of messages in a conversation that can be remembered.
|
||||
# This will increase the number of tokens used.
|
||||
chatHistory: 5
|
||||
# The maximum number of tokens to use per GPT API call
|
||||
# The maximum number of tokens to use per OpenAi API call
|
||||
maxTokens: 100
|
||||
# The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.
|
||||
minTokens: 30
|
||||
|
@@ -112,6 +112,14 @@ greettest:
|
||||
params:
|
||||
- user:
|
||||
desc: "The user to impersonate when sending the greeting, defaulting to yourself if not specified."
|
||||
boosttest:
|
||||
desc: Sends the boost message in the current channel as if you just boosted the server. You can optionally specify a different user.
|
||||
ex:
|
||||
- ''
|
||||
- '@SomeoneElse'
|
||||
params:
|
||||
- user:
|
||||
desc: "The user to impersonate when sending the boost message, defaulting to yourself if not specified."
|
||||
greetdmtest:
|
||||
desc: Sends the greet direct message to you as if you just joined the server. You can optionally specify a different user.
|
||||
ex:
|
||||
@@ -835,7 +843,12 @@ setservericon:
|
||||
- img:
|
||||
desc: "The URL of the image file to be displayed as the bot's banner."
|
||||
send:
|
||||
desc: 'Sends a message to a channel or user. Channel or user can be '
|
||||
desc: |-
|
||||
Sends a message to a channel or user.
|
||||
You can write "channel" (literally word 'channel') first followed by the channel id or channel mention, or
|
||||
You can write "user" (literally word 'user') first followed by the user id or user mention.
|
||||
After either one of those, specify the message to be sent.
|
||||
This command can only be used by the Bot Owner.
|
||||
ex:
|
||||
- channel 123123123132312 Stop spamming commands plz
|
||||
- user 1231231232132 I can see in the console what you're doing.
|
||||
@@ -2289,7 +2302,11 @@ emojiremove:
|
||||
- emotes:
|
||||
desc: "The list of emojis to be removed from the server."
|
||||
stickeradd:
|
||||
desc: Adds the sticker from your message to this server. Send the sticker along with this command (in the same message).
|
||||
desc: |-
|
||||
Adds the sticker from your message to this server.
|
||||
Send the sticker along with this command (in the same message).
|
||||
Alternatively you can upload an image along with this command but you have to specify the name.
|
||||
The image must be 300x300 in .png or .apng format and up to 512KB in size.
|
||||
ex:
|
||||
- ''
|
||||
- name "description" tag1 tag2 tagN
|
||||
@@ -2743,9 +2760,11 @@ waifutransfer:
|
||||
newOwner:
|
||||
desc: "The user to whom ownership of the waifu is being transferred."
|
||||
waifugift:
|
||||
desc: -|
|
||||
Gift an item to someone.
|
||||
This will increase their waifu value by a percentage of the gift's value.
|
||||
desc: |-
|
||||
Gift an item to a waifu user.
|
||||
The waifu's value will be increased by the percentage of the gift's value.
|
||||
You can optionally prefix the gift with a multiplier to gift the item that many times.
|
||||
For example, 3xRose will give the waifu 3 roses, 10xBread will give the waifu 10 breads. Do not use plural forms.
|
||||
Negative gifts will not show up in waifuinfo.
|
||||
Provide no parameters to see a list of items that you can gift.
|
||||
ex:
|
||||
@@ -2753,9 +2772,9 @@ waifugift:
|
||||
- Rose @Himesama
|
||||
params:
|
||||
- page:
|
||||
desc: "The number of pages to display when listing available gifting options."
|
||||
- itemName:
|
||||
desc: "The name of an item to be gifted, which is used to determine the percentage increase in waifu value."
|
||||
desc: "The number of the page to display."
|
||||
- items:
|
||||
desc: "The name of an item to be gifted. With an optional multiplier prefix."
|
||||
waifu:
|
||||
desc: "The user who is receiving the gift."
|
||||
waifulb:
|
||||
@@ -3751,7 +3770,11 @@ expredit:
|
||||
message:
|
||||
desc: "The text that will replace the original response in the expression's output."
|
||||
say:
|
||||
desc: Bot will send the message you typed in the specified channel. If you omit the channel name, it will send the message in the current channel. Supports embeds.
|
||||
desc: |-
|
||||
Make the bot say something, or in other words, make the bot send the message.
|
||||
You can optionally specify the channel where the bot will send the message.
|
||||
If you omit the channel name, it will send the message in the current channel.
|
||||
Supports embeds.
|
||||
ex:
|
||||
- hi
|
||||
- '#chat hi'
|
||||
@@ -4076,6 +4099,16 @@ bible:
|
||||
desc: "The name of the biblical book being referenced."
|
||||
chapterAndVerse:
|
||||
desc: "The reference to a specific passage in the Bible, such as 'Genesis 3:15'"
|
||||
quran:
|
||||
desc: |-
|
||||
Shows the text of an ayah of the Quran, as well as the recitation by Alafasy.
|
||||
Supply surah:ayah, or ayah number. For instance, 262 or 2:255 will both get you Ayat Al Kursi
|
||||
ex:
|
||||
- 2:255
|
||||
- 262
|
||||
params:
|
||||
- ayah:
|
||||
desc: "The number of the ayah in the Quran, for example 2:255."
|
||||
edit:
|
||||
desc: Edits bot's message, you have to specify message ID and new text. You can optionally specify target channel. Supports embeds.
|
||||
ex:
|
||||
@@ -4242,8 +4275,10 @@ bankbalance:
|
||||
Shows how much currency is in your bank account.
|
||||
This differs from your cash amount, as the cash amount is publicly available, but only you have access to your bank balance.
|
||||
However, you have to withdraw it first in order to use it.
|
||||
Bot Owner can also check another user's bank balance.
|
||||
ex:
|
||||
- ''
|
||||
- '@User'
|
||||
params:
|
||||
- {}
|
||||
banktake:
|
||||
@@ -4523,4 +4558,23 @@ prompt:
|
||||
params:
|
||||
- query:
|
||||
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:
|
||||
- {}
|
||||
coins:
|
||||
desc: |-
|
||||
Shows a list of 10 crypto currencies ordered by market cap.
|
||||
Shows their price, change in the last24h, market cap and circulating and total supply.
|
||||
Paginated with 10 per page.
|
||||
ex:
|
||||
- ''
|
||||
- '2'
|
||||
params:
|
||||
- page:
|
||||
desc: "Page number to show. Starts at 1."
|
@@ -619,7 +619,7 @@
|
||||
"quote_deleted": "Quote #{0} deleted.",
|
||||
"quote_edited": "Quote Edited",
|
||||
"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_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.",
|
||||
@@ -889,6 +889,7 @@
|
||||
"club_kick_hierarchy": "Only club owner can kick club admins. Owner can't be kicked.",
|
||||
"club_renamed": "Club has been renamed to {0}",
|
||||
"club_name_taken": "A club with that name already exists.",
|
||||
"rank": "Rank",
|
||||
"template_reloaded": "Xp template has been reloaded.",
|
||||
"expr_edited": "Expression Edited",
|
||||
"self_assign_are_exclusive": "You can only choose 1 role from each group.",
|
||||
@@ -1039,6 +1040,7 @@
|
||||
"medusa_already_loaded": "Medusa {0} is already loaded",
|
||||
"medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid",
|
||||
"bank_balance": "You have {0} in your bank account.",
|
||||
"bank_balance_other": "User {0} has {1} in the bank.",
|
||||
"bank_deposited": "You deposited {0} to your bank account.",
|
||||
"bank_withdrew": "You withdrew {0} from your bank account.",
|
||||
"bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.",
|
||||
@@ -1067,8 +1069,7 @@
|
||||
"xpshop_already_owned": "You already own this item.",
|
||||
"xpshop_item_not_found": "An item with that key doesn't exist.",
|
||||
"xpshop_website": "You can see the list of all Xp Shop items here: <https://xpshop.nadeko.bot>",
|
||||
"sticker_invalid_size": "Stickers must be exactly 300x300 pixels.",
|
||||
"sticker_error": "You must either send a sticker along with this command, or upload a 300x300 .png or .apng image.",
|
||||
"sticker_error": "You must either send a sticker along with this command, or upload a 300x300 .png or .apng image. Up to 512KB in size.",
|
||||
"sticker_missing_name": "Please specify a name for the sticker.",
|
||||
"thread_deleted": "Thread Deleted",
|
||||
"thread_created": "Thread Created",
|
||||
@@ -1101,5 +1102,7 @@
|
||||
"todo_archived_list": "Archived Todo List",
|
||||
"search_results": "Search results",
|
||||
"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."
|
||||
}
|
||||
|
Reference in New Issue
Block a user