Compare commits

...

17 Commits
5.1.0 ... 5.1.3

Author SHA1 Message Date
Kwoth
ca13684c0d docs: Updated CHANGELOG.
dev: upped version to 5.1.3
2024-07-06 09:05:34 +00:00
Kwoth
0ad6b741e7 add: chatterbot, (but not prompting) will also trigger if the user replies to a bot's message 2024-07-05 04:55:33 +00:00
Kwoth
4ce756d760 fix: if prompt ai responds with chat, it will prevent further further execution of tthat input to prevent the same query running multiple times 2024-07-05 04:48:00 +00:00
Kwoth
5f2813d3af fix: clean the bot username too from invalid characters for openai 2024-07-05 03:36:04 +00:00
Kwoth
1b7458529c fix: bot should now trim the invalid characters from openai message usernames 2024-07-05 03:33:04 +00:00
Kwoth
9c9c8d7490 fix: fixed pipeline 2024-06-29 08:43:50 +00:00
Kwoth
2700bfdce8 fix: Fixed .stickeradd, it now properly supports 300x300 image uploads. closes #434 2024-06-29 08:42:47 +00:00
Kwoth
5498c5ce3f add: Added .quran command, which will show the provided ayah in english and arabic, including recitation by Alafasy 2024-06-29 07:14:13 +00:00
Kwoth
6f444a8da0 fix: fixed pipeline, no functional change 2024-06-29 01:36:38 +00:00
Kwoth
454c14eee1 docs: Updated changelog 2024-06-29 01:25:48 +00:00
Kwoth
30aa8e8186 fix: fixed honeypot not unbanning and pruning 2024-06-29 01:24:14 +00:00
Kwoth
2ca141810c docs: updated pagination, upped version to 5.1.1 2024-06-28 05:17:40 +00:00
Kwoth
9da8e4f1c1 fix: Fixed pagination, for real this time, closes #435,
dev: removed some old creds migration code, incremented creds version
2024-06-27 19:48:39 +00:00
Kwoth
ef471c32bb add: Added .honeypot command
fix: Fixed .betdraw not respecting max bet limit
2024-06-27 04:50:55 +00:00
Kwoth
49d557caec fix: removed an extra in remind string 2024-06-25 16:01:59 +00:00
Kwoth
4366f908f3 dev: Using new stopwatch, some cleanup. No functional change 2024-06-25 07:09:16 +00:00
Kwoth
237e66495b fix: Potential fix for the invalid syntax redis issue, unless the issue is caused by the older version of redis on the server itself 2024-06-25 03:32:21 +00:00
41 changed files with 11214 additions and 239 deletions

View File

@@ -2,6 +2,42 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [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 +46,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

View File

@@ -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())
@@ -161,8 +161,7 @@ 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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace 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");
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
// namespace NadekoBot.Modules.Administration;
//
// public partial class Administration
// {
// [Group]
// public partial class TicketCommands : NadekoModule
// {
// [Cmd]
// public async Task Ticket()
// {
//
// }
// }
// }

View File

@@ -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,7 +57,7 @@ public partial class Gambling
i.Dispose();
var eb = _sender.CreateEmbed()
.WithOkColor();
.WithOkColor();
var toSend = string.Empty;
if (cardObjects.Count == 5)
@@ -136,18 +137,28 @@ 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,
@@ -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();
@@ -192,6 +203,7 @@ public partial class Gambling
return $"{val} / {col}";
}
private string GetCardInfo(RegularCard card)
{
var val = (int)card.Value switch

View File

@@ -96,6 +96,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 +167,7 @@ public class ChatterBotService : IExecOnMessage
}
else
{
Log.Warning("Error in chatterbot: {Error}", error);
Log.Warning("Error in chatterbot: {Error}", error.Value);
}
Log.Information("""

View File

@@ -3,10 +3,12 @@ using Newtonsoft.Json;
using OneOf.Types;
using System.Net.Http.Json;
using SharpToken;
using System.CodeDom;
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Games.Common.ChatterBot;
public class OfficialGptSession : IChatterBotSession
public partial class OfficialGptSession : IChatterBotSession
{
private string Uri
=> $"https://api.openai.com/v1/chat/completions";
@@ -45,7 +47,7 @@ public class OfficialGptSession : IChatterBotSession
_maxHistory = chatHistory;
_maxTokens = maxTokens;
_minTokens = minTokens;
_nadekoUsername = nadekoUsername;
_nadekoUsername = UsernameCleaner().Replace(nadekoUsername, "");
_encoding = GptEncoding.GetEncodingForModel(_model);
messages.Add(new()
{
@@ -55,14 +57,21 @@ public class OfficialGptSession : IChatterBotSession
});
}
[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);
@@ -103,6 +112,8 @@ public class OfficialGptSession : IChatterBotSession
try
{
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
Log.Information("Received response: {response} ", dataString);
var res = response?.Choices?[0];
var message = res?.Message?.Content;

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

View File

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

View File

@@ -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)
{

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.0</Version>
<Version>5.1.3</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" />

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -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.

View File

@@ -1192,6 +1192,8 @@ xpreset:
- xpreset
bible:
- bible
quran:
- quran
edit:
- edit
delete:
@@ -1401,3 +1403,5 @@ cleanupguilddata:
- cleanupguilddata
prompt:
- prompt
honeypot:
- honeypot

View File

@@ -2289,7 +2289,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
@@ -4076,6 +4080,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:
@@ -4523,4 +4537,12 @@ 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:
- {}

View File

@@ -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.",
@@ -1067,8 +1067,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 +1100,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."
}