Compare commits

...

13 Commits
4.3.7 ... 4.3.8

Author SHA1 Message Date
Kwoth
9ffa701742 Updated changelog.md 2022-10-02 15:45:28 +02:00
Kwoth
a4f7df8aee Merge branch 'cmdcd-and-cleanup' into 'v4'
Cmdcd and cleanup

See merge request Kwoth/nadekobot!269
2022-10-02 13:38:51 +00:00
Kwoth
15e6cff14a * Rewrote cmdcd service, prettified and paginated .cmdcds
* Cleaned up/improved some command handler code
* Fixed .yun when channel id has an underscore
2022-10-02 13:38:50 +00:00
Kwoth
e63ac07a52 Fixed typo 2022-10-01 08:54:11 +02:00
Kwoth
a294e3bf8f Fixed -c GlobalNadeko compilation 2022-10-01 08:52:29 +02:00
Kwoth
a5b2fac69c .curtrs may? run a little faster. Uses async, clarified GamblingConfig transactionLifetime field 2022-09-28 07:09:37 +02:00
Kwoth
a8e06a5ae4 Clarified .remind permission requirement 2022-09-26 14:20:26 +02:00
Kwoth
761bdd8610 Merge branch 'hokutochen-v4-patch-16984' into 'v4'
Added VPS hosting requirements

See merge request Kwoth/nadekobot!268
2022-09-24 09:50:20 +00:00
Hokuto Chen
b5904889b0 Added VPS hosting requirements 2022-09-24 09:50:20 +00:00
Kwoth
5b4517cf5c Updated changelog, removed the ability to bet 0 on betdraw 2022-09-23 23:01:11 +02:00
Kwoth
72158c0a2c Added -a / --after <messageid> option to .prune, converted the argument to proper options object 2022-09-17 13:51:03 +02:00
Kwoth
1ca6f6dc5c Added .autopublish command 2022-09-16 21:49:50 +02:00
Kwoth
005fd7b8c6 Possible fix for missing IGuildUser when using guild commands, closes #383 2022-09-15 17:02:53 +02:00
36 changed files with 10848 additions and 154 deletions

View File

@@ -2,7 +2,28 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [4.3.7]
## Unreleased
## [4.3.8] - 02.10.2022
### Added
- Added `.autopublish` command which will automatically publish messages posted in the channel.
- Added `--after <messageid>` option to prune which will make prune only delete messages after the specified message id.
### Changed
- `.prune` options `--after` and `--safe` are now proper command options, and will show in .h help
- `.cmdcd` code mostly rewritten, slight QoL improvements.
- Clarified `.remind` permission requirements in help text
- `.cmdcds` looks a little better, and is paginated
### Fixed
- Fixed trivia bugs
- Fixed `.yun` not working with channels with underscore in the name
## [4.3.7] - 14.09.2022
### Added

View File

@@ -17,7 +17,7 @@ It is recommended that you use **Ubuntu 20.04**, as there have been nearly no pr
##### Compatible operating systems:
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10 22.04
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10, 22.04
- Mint: 19, 20
- Debian: 9, 10
- CentOS: 7
@@ -316,6 +316,26 @@ This method is similar to the one above, but requires one extra step, with the a
If you want Nadeko to play music for you 24/7 without having to hosting it on your PC and want to keep it cheap, reliable and convenient as possible, you can try Nadeko on Linux Digital Ocean Droplet using the link [DigitalOcean](http://m.do.co/c/46b4d3d44795/) (by using this link, you will get **$10 credit** and also support Nadeko)
To set up the VPS, please select the options below
```
These are the min requirements you must follow:
OS: Any between Ubuntu, Fedora, and Debian
Plan: Basic
CPU options: regular with SSD
1 GB / 1 CPU
25 GB SSD Disk
1000 GB transfer
Note: You can select the cheapest option with 512 MB /1 CPU but this has been a hit or miss.
Datacenter region: Choose one depending on where you are located.
Authentication: Password or SSH
(Select SSH if you know what you are doing, otherwise choose password)
```
**Setting up NadekoBot**
Assuming you have followed the link above to setup an account and a Droplet with a 64-bit operational system on Digital Ocean and got the `IP address and root password (in your e-mail)` to login, it's time to get started.

View File

@@ -116,10 +116,6 @@ public sealed class Bot
// cache
.AddCache(_creds);
// admin
#if GLOBAL_NADEKO
svcs.AddSingleton<ILogCommandService, DummyLogCommandService>();
#endif
svcs.AddHttpClient();
svcs.AddHttpClient("memelist")

View File

@@ -0,0 +1,33 @@
namespace NadekoBot.Common.TypeReaders;
public sealed class GuildUserTypeReader : NadekoTypeReader<IGuildUser>
{
public override async ValueTask<TypeReaderResult<IGuildUser>> ReadAsync(ICommandContext ctx, string input)
{
if (ctx.Guild is null)
return TypeReaderResult.FromError<IGuildUser>(CommandError.Unsuccessful, "Must be in a guild.");
input = input.Trim();
IGuildUser? user = null;
if (MentionUtils.TryParseUser(input, out var id))
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
if (ulong.TryParse(input, out id))
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
if (user is null)
{
var users = await ctx.Guild.GetUsersAsync(CacheMode.CacheOnly);
user = users.FirstOrDefault(x => x.Username == input)
?? users.FirstOrDefault(x =>
string.Equals(x.ToString(), input, StringComparison.InvariantCultureIgnoreCase))
?? users.FirstOrDefault(x =>
string.Equals(x.Username, input, StringComparison.InvariantCultureIgnoreCase));
}
if (user is null)
return TypeReaderResult.FromError<IGuildUser>(CommandError.ObjectNotFound, "User not found.");
return TypeReaderResult.FromSuccess(user);
}
}

View File

@@ -1,4 +1,5 @@
#nullable disable
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services.Database.Models;
@@ -6,12 +7,14 @@ namespace NadekoBot.Db;
public static class CurrencyTransactionExtensions
{
public static List<CurrencyTransaction> GetPageFor(this DbSet<CurrencyTransaction> set, ulong userId, int page)
=> set.AsQueryable()
.AsNoTracking()
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.DateAdded)
.Skip(15 * page)
.Take(15)
.ToList();
public static Task<List<CurrencyTransaction>> GetPageFor(
this DbSet<CurrencyTransaction> set,
ulong userId,
int page)
=> set.ToLinqToDBTable()
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.DateAdded)
.Skip(15 * page)
.Take(15)
.ToListAsyncLinqToDB();
}

View File

@@ -0,0 +1,9 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Db.Models;
public class AutoPublishChannel : DbEntity
{
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
}

View File

@@ -464,6 +464,14 @@ public abstract class NadekoContext : DbContext
});
#endregion
#region AutoPublish
modelBuilder.Entity<AutoPublishChannel>(apc => apc
.HasIndex(x => x.GuildId)
.IsUnique());
#endregion
}
#if DEBUG

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class autopub : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "autopublishchannel",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_autopublishchannel", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_autopublishchannel_guildid",
table: "autopublishchannel",
column: "guildid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "autopublishchannel");
}
}
}

View File

@@ -19,6 +19,35 @@ namespace NadekoBot.Migrations.Mysql
.HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("NadekoBot.Db.Models.AutoPublishChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<ulong>("ChannelId")
.HasColumnType("bigint unsigned")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<ulong>("GuildId")
.HasColumnType("bigint unsigned")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_autopublishchannel");
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_autopublishchannel_guildid");
b.ToTable("autopublishchannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class autopub : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "rolerequirement",
table: "shopentry",
type: "numeric(20,0)",
nullable: true);
migrationBuilder.CreateTable(
name: "autopublishchannel",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_autopublishchannel", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_autopublishchannel_guildid",
table: "autopublishchannel",
column: "guildid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "autopublishchannel");
migrationBuilder.DropColumn(
name: "rolerequirement",
table: "shopentry");
}
}
}

View File

@@ -22,6 +22,37 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NadekoBot.Db.Models.AutoPublishChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_autopublishchannel");
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_autopublishchannel_guildid");
b.ToTable("autopublishchannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class autopub : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AutoPublishChannel",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AutoPublishChannel", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_AutoPublishChannel_GuildId",
table: "AutoPublishChannel",
column: "GuildId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AutoPublishChannel");
}
}
}

View File

@@ -17,6 +17,29 @@ namespace NadekoBot.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("NadekoBot.Db.Models.AutoPublishChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId")
.IsUnique();
b.ToTable("AutoPublishChannel");
});
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")

View File

@@ -33,9 +33,13 @@ public partial class Administration : NadekoModule<AdministrationService>
}
private readonly SomethingOnlyChannelService _somethingOnly;
private readonly AutoPublishService _autoPubService;
public Administration(SomethingOnlyChannelService somethingOnly)
=> _somethingOnly = somethingOnly;
public Administration(SomethingOnlyChannelService somethingOnly, AutoPublishService autoPubService)
{
_somethingOnly = somethingOnly;
_autoPubService = autoPubService;
}
[Cmd]
[RequireContext(ContextType.Guild)]
@@ -376,4 +380,26 @@ public partial class Administration : NadekoModule<AdministrationService>
await t.DeleteAsync();
await ctx.OkAsync();
}
[Cmd]
[UserPerm(ChannelPerm.ManageMessages)]
public async Task AutoPublish()
{
if (ctx.Channel.GetChannelType() != ChannelType.News)
{
await ReplyErrorLocalizedAsync(strs.req_announcement_channel);
return;
}
var newState = await _autoPubService.ToggleAutoPublish(ctx.Guild.Id, ctx.Channel.Id);
if (newState)
{
await ReplyConfirmLocalizedAsync(strs.autopublish_enable);
}
else
{
await ReplyConfirmLocalizedAsync(strs.autopublish_disable);
}
}
}

View File

@@ -1,6 +1,5 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Nadeko.Common;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;

View File

@@ -0,0 +1,87 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Administration.Services;
public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredsProvider _creds;
private ConcurrentDictionary<ulong, ulong> _enabled;
public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds)
{
_db = db;
_client = client;
_creds = creds;
}
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
if (guild is null)
return;
if (msg.Channel.GetChannelType() != ChannelType.News)
return;
if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id)
return;
await msg.CrosspostAsync(new RequestOptions()
{
RetryMode = RetryMode.AlwaysFail
});
}
public async Task OnReadyAsync()
{
var creds = _creds.GetCreds();
await using var ctx = _db.GetDbContext();
var items = await ctx.GetTable<AutoPublishChannel>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
_enabled = items
.ToDictionary(x => x.GuildId, x => x.ChannelId)
.ToConcurrent();
}
public async Task<bool> ToggleAutoPublish(ulong guildId, ulong channelId)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<AutoPublishChannel>()
.DeleteAsync(x => x.GuildId == guildId && x.ChannelId == channelId);
if (deleted != 0)
{
_enabled.TryRemove(guildId, out _);
return false;
}
await ctx.GetTable<AutoPublishChannel>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
old => new()
{
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
() => new()
{
GuildId = guildId
});
_enabled[guildId] = channelId;
return true;
}
}

View File

@@ -1,4 +1,5 @@
#nullable disable
using CommandLine;
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Administration;
@@ -10,17 +11,34 @@ public partial class Administration
{
private static readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
//delets her own messages, no perm required
public sealed class PruneOptions : INadekoCommandOptions
{
[Option(shortName: 's', longName: "safe", Default = false, HelpText = "Whether pinned messages should be deleted.", Required = false)]
public bool Safe { get; set; }
[Option(shortName: 'a', longName: "after", Default = null, HelpText = "Prune only messages after the specified message ID.", Required = false)]
public ulong? After { get; set; }
public void NormalizeOptions()
{
}
}
//deletes her own messages, no perm required
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Prune(string parameter = null)
[NadekoOptions(typeof(PruneOptions))]
public async Task Prune(params string[] args)
{
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
var user = await ctx.Guild.GetCurrentUserAsync();
if (parameter is "-s" or "--safe")
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned);
if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, opts.After);
else
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id);
await _service.PruneWhere((ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, opts.After);
ctx.Message.DeleteAfter(3);
}
@@ -29,19 +47,22 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[NadekoOptions(typeof(PruneOptions))]
[Priority(1)]
public async Task Prune(int count, string parameter = null)
public async Task Prune(int count, params string[] args)
{
count++;
if (count < 1)
return;
if (count > 1000)
count = 1000;
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
if (parameter is "-s" or "--safe")
await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned);
if (opts.Safe)
await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => !x.IsPinned, opts.After);
else
await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true);
await _service.PruneWhere((ITextChannel)ctx.Channel, count, _ => true, opts.After);
}
//prune @user [x]
@@ -49,17 +70,19 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[NadekoOptions(typeof(PruneOptions))]
[Priority(0)]
public Task Prune(IGuildUser user, int count = 100, string parameter = null)
=> Prune(user.Id, count, parameter);
public Task Prune(IGuildUser user, int count = 100, string args = null)
=> Prune(user.Id, count, args);
//prune userid [x]
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
[NadekoOptions(typeof(PruneOptions))]
[Priority(0)]
public async Task Prune(ulong userId, int count = 100, string parameter = null)
public async Task Prune(ulong userId, int count = 100, params string[] args)
{
if (userId == ctx.User.Id)
count++;
@@ -70,17 +93,21 @@ public partial class Administration
if (count > 1000)
count = 1000;
if (parameter is "-s" or "--safe")
var (opts, _) = OptionsParser.ParseFrom<PruneOptions>(new PruneOptions(), args);
if (opts.Safe)
{
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned);
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
opts.After);
}
else
{
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks);
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
opts.After);
}
}
}

View File

@@ -1,6 +1,4 @@
#nullable disable
using Nadeko.Common;
namespace NadekoBot.Modules.Administration.Services;
public class PruneService : INService
@@ -13,7 +11,7 @@ public class PruneService : INService
public PruneService(ILogCommandService logService)
=> _logService = logService;
public async Task PruneWhere(ITextChannel channel, int amount, Func<IMessage, bool> predicate)
public async Task PruneWhere(ITextChannel channel, int amount, Func<IMessage, bool> predicate, ulong? after = null)
{
ArgumentNullException.ThrowIfNull(channel, nameof(channel));
@@ -28,7 +26,14 @@ public class PruneService : INService
var now = DateTime.UtcNow;
IMessage[] msgs;
IMessage lastMessage = null;
msgs = (await channel.GetMessagesAsync(50).FlattenAsync()).Where(predicate).Take(amount).ToArray();
var dled = await channel.GetMessagesAsync(50).FlattenAsync();
msgs = dled
.Where(predicate)
.Where(x => after is ulong a ? x.Id > a : true)
.Take(amount)
.ToArray();
while (amount > 0 && msgs.Any())
{
lastMessage = msgs[^1];
@@ -62,10 +67,13 @@ public class PruneService : INService
amount -= 50;
if (amount > 0)
{
msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync())
.Where(predicate)
.Take(amount)
.ToArray();
dled = await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync();
msgs = dled
.Where(predicate)
.Where(x => after is ulong a ? x.Id > a : true)
.Take(amount)
.ToArray();
}
}
}

View File

@@ -3,6 +3,9 @@
namespace NadekoBot.Modules.Administration;
public sealed class DummyLogCommandService : ILogCommandService
#if GLOBAL_NADEKO
, INService
#endif
{
public void AddDeleteIgnore(ulong xId)
{

View File

@@ -145,6 +145,9 @@ public partial class Gambling
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
{
if (amount <= 0)
return;
var res = await _service.BetDrawAsync(ctx.User.Id,
amount,
(byte?)val,

View File

@@ -249,7 +249,7 @@ public partial class Gambling : GamblingModule<GamblingService>
List<CurrencyTransaction> trs;
await using (var uow = _db.GetDbContext())
{
trs = uow.CurrencyTransactions.GetPageFor(userId, page);
trs = await uow.CurrencyTransactions.GetPageFor(userId, page);
}
var embed = _eb.Create()

View File

@@ -78,7 +78,7 @@ public class CurrencyConfig
[Comment(@"What is the name of the currency")]
public string Name { get; set; } = "Nadeko Flower";
[Comment(@"For how long will the transactions be kept in the database (curtrs)
[Comment(@"For how long (in days) will the transactions be kept in the database (curtrs)
Set 0 to disable cleanup (keep transactions forever)")]
public int TransactionsLifetime { get; set; } = 0;
}

View File

@@ -112,25 +112,25 @@ public partial class Games
private void RegisterEvents(TriviaGame trivia)
{
trivia.OnQuestion += OnTriviaOnOnQuestion;
trivia.OnHint += OnTriviaOnOnHint;
trivia.OnGuess += OnTriviaOnOnGuess;
trivia.OnEnded += OnTriviaOnOnEnded;
trivia.OnStats += OnTriviaOnOnStats;
trivia.OnTimeout += OnTriviaOnOnTimeout;
trivia.OnQuestion += OnTriviaQuestion;
trivia.OnHint += OnTriviaHint;
trivia.OnGuess += OnTriviaGuess;
trivia.OnEnded += OnTriviaEnded;
trivia.OnStats += OnTriviaStats;
trivia.OnTimeout += OnTriviaTimeout;
}
private void UnregisterEvents(TriviaGame trivia)
{
trivia.OnQuestion -= OnTriviaOnOnQuestion;
trivia.OnHint -= OnTriviaOnOnHint;
trivia.OnGuess -= OnTriviaOnOnGuess;
trivia.OnEnded -= OnTriviaOnOnEnded;
trivia.OnStats -= OnTriviaOnOnStats;
trivia.OnTimeout -= OnTriviaOnOnTimeout;
trivia.OnQuestion -= OnTriviaQuestion;
trivia.OnHint -= OnTriviaHint;
trivia.OnGuess -= OnTriviaGuess;
trivia.OnEnded -= OnTriviaEnded;
trivia.OnStats -= OnTriviaStats;
trivia.OnTimeout -= OnTriviaTimeout;
}
private async Task OnTriviaOnOnHint(TriviaGame game, TriviaQuestion question)
private async Task OnTriviaHint(TriviaGame game, TriviaQuestion question)
{
try
{
@@ -150,11 +150,11 @@ public partial class Games
}
catch (Exception ex)
{
Log.Warning(ex, "Error editing triva message");
Log.Warning(ex, "Error editing trivia message");
}
}
private async Task OnTriviaOnOnQuestion(TriviaGame game, TriviaQuestion question)
private async Task OnTriviaQuestion(TriviaGame game, TriviaQuestion question)
{
try
{
@@ -173,19 +173,16 @@ public partial class Games
questionMessage = await ctx.Channel.EmbedAsync(questionEmbed);
}
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound or HttpStatusCode.Forbidden or HttpStatusCode.BadRequest)
catch (HttpException ex) when (ex.HttpCode is HttpStatusCode.NotFound or HttpStatusCode.Forbidden
or HttpStatusCode.BadRequest)
{
Log.Warning("Unable to send trivia questions. Stopping immediately");
game.Stop();
}
catch (Exception ex)
{
Log.Warning(ex, "Error sending trivia embed");
await Task.Delay(2000);
throw;
}
}
private async Task OnTriviaOnOnTimeout(TriviaGame _, TriviaQuestion question)
private async Task OnTriviaTimeout(TriviaGame _, TriviaQuestion question)
{
try
{
@@ -205,7 +202,7 @@ public partial class Games
}
}
private async Task OnTriviaOnOnStats(TriviaGame game)
private async Task OnTriviaStats(TriviaGame game)
{
try
{
@@ -217,7 +214,7 @@ public partial class Games
}
}
private async Task OnTriviaOnOnEnded(TriviaGame game)
private async Task OnTriviaEnded(TriviaGame game)
{
try
{
@@ -237,10 +234,9 @@ public partial class Games
}
UnregisterEvents(game);
await Task.Delay(1000);
}
private async Task OnTriviaOnOnGuess(TriviaGame _, TriviaUser user, TriviaQuestion question, bool isWin)
private async Task OnTriviaGuess(TriviaGame _, TriviaUser user, TriviaQuestion question, bool isWin)
{
try
{

View File

@@ -1,4 +1,5 @@
using System.Threading.Channels;
using Exception = System.Exception;
namespace NadekoBot.Modules.Games.Common.Trivia;
@@ -64,7 +65,6 @@ public sealed class TriviaGame
if (errorCount >= 5)
{
Log.Warning("Trivia errored 5 times and will quit");
await OnEnded(this);
break;
}
@@ -80,7 +80,7 @@ public sealed class TriviaGame
var maybeQuestion = await _questionPool.GetQuestionAsync();
if (!(maybeQuestion is TriviaQuestion question))
if (maybeQuestion is not { } question)
{
// if question is null (ran out of question, or other bugg ) - stop
break;
@@ -110,7 +110,8 @@ public sealed class TriviaGame
var guessed = false;
while (true)
{
var readTask = _inputs.Reader.ReadAsync().AsTask();
using var readCancel = new CancellationTokenSource();
var readTask = _inputs.Reader.ReadAsync(readCancel.Token).AsTask();
// wait for either someone to attempt to guess
// or for timeout
@@ -119,6 +120,8 @@ public sealed class TriviaGame
// if the task which completed is the timeout task
if (task == halfGuessTimerTask)
{
readCancel.Cancel();
// if hint is already sent, means time expired
// break (end the round)
if (hintSent)
@@ -130,7 +133,7 @@ public sealed class TriviaGame
halfGuessTimerTask = TimeOutFactory();
// send a hint out
await OnHint(this, question);
continue;
}
@@ -147,6 +150,7 @@ public sealed class TriviaGame
// reset inactivity counter
inactivity = 0;
errorCount = 0;
var isWin = false;
// if user won the game, tell the game to stop
@@ -174,9 +178,9 @@ public sealed class TriviaGame
}
}
}
catch
catch (Exception ex)
{
Log.Error(ex, "Fatal error in trivia game: {ErrorMessage}", ex.Message);
}
finally
{

View File

@@ -1,68 +1,119 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Permissions.Services;
public class CmdCdService : IExecPreCommand, INService
public sealed class CmdCdService : IExecPreCommand, IReadyExecutor, INService
{
public ConcurrentDictionary<ulong, ConcurrentHashSet<CommandCooldown>> CommandCooldowns { get; }
public ConcurrentDictionary<ulong, ConcurrentHashSet<ActiveCooldown>> ActiveCooldowns { get; } = new();
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, int>> _settings = new();
public int Priority { get; } = 0;
private readonly ConcurrentDictionary<(ulong, string), ConcurrentDictionary<ulong, DateTime>> _activeCooldowns =
new();
public int Priority => 0;
public CmdCdService(Bot bot)
=> CommandCooldowns = new(bot.AllGuildConfigs.ToDictionary(k => k.GuildId,
v => new ConcurrentHashSet<CommandCooldown>(v.CommandCooldowns)));
public Task<bool> TryBlock(IGuild guild, IUser user, string commandName)
{
if (guild is null)
return Task.FromResult(false);
_settings = bot
.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.CommandCooldowns
.ToDictionary(c => c.CommandName, c => c.Seconds)
.ToConcurrent())
.ToConcurrent();
}
var cmdcds = CommandCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet<CommandCooldown>());
CommandCooldown cdRule;
if ((cdRule = cmdcds.FirstOrDefault(cc => cc.CommandName == commandName)) is not null)
public Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
=> TryBlock(context.Guild, context.User, command.Name.ToLowerInvariant());
public async Task<bool> TryBlock(IGuild guild, IUser user, string commandName)
{
if (!_settings.TryGetValue(guild.Id, out var cooldownSettings))
return false;
if (!cooldownSettings.TryGetValue(commandName, out var cdSeconds))
return false;
var cooldowns = _activeCooldowns.GetOrAdd(
(guild.Id, commandName),
static _ => new());
// if user is not already on cooldown, add
if (cooldowns.TryAdd(user.Id, DateTime.UtcNow))
{
var activeCdsForGuild = ActiveCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet<ActiveCooldown>());
if (activeCdsForGuild.FirstOrDefault(ac => ac.UserId == user.Id && ac.Command == commandName) is not null)
return Task.FromResult(true);
activeCdsForGuild.Add(new()
{
UserId = user.Id,
Command = commandName
});
_ = Task.Run(async () =>
{
try
{
await Task.Delay(cdRule.Seconds * 1000);
activeCdsForGuild.RemoveWhere(ac => ac.Command == commandName && ac.UserId == user.Id);
}
catch
{
// ignored
}
});
return false;
}
return Task.FromResult(false);
// if there is an entry, maybe it expired. Try to check if it expired and don't fail if it did
// - just update
if (cooldowns.TryGetValue(user.Id, out var oldValue))
{
var diff = DateTime.UtcNow - oldValue;
if (diff.Seconds > cdSeconds)
{
if (cooldowns.TryUpdate(user.Id, DateTime.UtcNow, oldValue))
return false;
}
}
return true;
}
public Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
public async Task OnReadyAsync()
{
var guild = ctx.Guild;
var user = ctx.User;
var commandName = command.Name.ToLowerInvariant();
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
return TryBlock(guild, user, commandName);
while (await timer.WaitForNextTickAsync())
{
var now = DateTime.UtcNow;
// once per hour delete expired entries
foreach (var ((guildId, commandName), dict) in _activeCooldowns)
{
// if this pair no longer has associated config, that means it has been removed.
// remove all cooldowns
if (!_settings.TryGetValue(guildId, out var inner)
|| !inner.TryGetValue(commandName, out var cdSeconds))
{
_activeCooldowns.Remove((guildId, commandName), out _);
continue;
}
Cleanup(dict, cdSeconds);
}
}
}
}
public class ActiveCooldown
{
public string Command { get; set; }
public ulong UserId { get; set; }
private void Cleanup(ConcurrentDictionary<ulong, DateTime> dict, int cdSeconds)
{
var now = DateTime.UtcNow;
foreach (var (key, _) in dict.Where(x => (now - x.Value).Seconds > cdSeconds).ToArray())
{
dict.TryRemove(key, out _);
}
}
public void ClearCooldowns(ulong guildId, string cmdName)
{
if (_settings.TryGetValue(guildId, out var dict))
dict.TryRemove(cmdName, out _);
_activeCooldowns.TryRemove((guildId, cmdName), out _);
}
public void AddCooldown(ulong guildId, string name, int secs)
{
var sett = _settings.GetOrAdd(guildId, static _ => new());
sett[name] = secs;
// force cleanup
if (_activeCooldowns.TryGetValue((guildId, name), out var dict))
Cleanup(dict, secs);
}
public IReadOnlyCollection<(string CommandName, int Seconds)> GetCommandCooldowns(ulong guildId)
{
if (!_settings.TryGetValue(guildId, out var dict))
return Array.Empty<(string, int)>();
return dict.Select(x => (x.Key, x.Value)).ToArray();
}
}

View File

@@ -1,4 +1,5 @@
#nullable disable
using Humanizer.Localisation;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.TypeReaders;
using NadekoBot.Db;
@@ -12,12 +13,6 @@ public partial class Permissions
[Group]
public partial class CmdCdsCommands : NadekoModule
{
private ConcurrentDictionary<ulong, ConcurrentHashSet<CommandCooldown>> CommandCooldowns
=> _service.CommandCooldowns;
private ConcurrentDictionary<ulong, ConcurrentHashSet<ActiveCooldown>> ActiveCooldowns
=> _service.ActiveCooldowns;
private readonly DbService _db;
private readonly CmdCdService _service;
@@ -40,12 +35,10 @@ public partial class Permissions
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet<CommandCooldown>());
var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name);
if (toDelete is not null)
uow.Set<CommandCooldown>().Remove(toDelete);
localSet.RemoveWhere(cc => cc.CommandName == name);
if (secs != 0)
{
var cc = new CommandCooldown
@@ -54,7 +47,7 @@ public partial class Permissions
Seconds = secs
};
config.CommandCooldowns.Add(cc);
localSet.Add(cc);
_service.AddCooldown(channel.Guild.Id, name, secs);
}
await uow.SaveChangesAsync();
@@ -62,8 +55,7 @@ public partial class Permissions
if (secs == 0)
{
var activeCds = ActiveCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet<ActiveCooldown>());
activeCds.RemoveWhere(ac => ac.Command == name);
_service.ClearCooldowns(ctx.Guild.Id, cmdName);
await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name)));
}
else
@@ -84,19 +76,29 @@ public partial class Permissions
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task AllCmdCooldowns()
public async Task AllCmdCooldowns(int page = 1)
{
if (--page < 0)
return;
var channel = (ITextChannel)ctx.Channel;
var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet<CommandCooldown>());
var localSet = _service.GetCommandCooldowns(ctx.Guild.Id);
if (!localSet.Any())
await ReplyConfirmLocalizedAsync(strs.cmdcd_none);
else
{
await channel.SendTableAsync("",
localSet.Select(c => c.CommandName + ": " + c.Seconds + GetText(strs.sec)),
s => $"{s,-30}",
2);
await ctx.SendPaginatedConfirmAsync(page, curPage =>
{
var items = localSet.Skip(curPage * 15)
.Take(15)
.Select(x => $"{Format.Code(x.CommandName)}: {x.Seconds.Seconds().Humanize(maxUnit: TimeUnit.Second, culture: Culture)}");
return _eb.Create(ctx)
.WithOkColor()
.WithDescription(items.Join("\n"));
}, localSet.Count, 15);
}
}
}

View File

@@ -11,7 +11,7 @@ public partial class Searches
public partial class FeedCommands : NadekoModule<FeedsService>
{
private static readonly Regex _ytChannelRegex =
new(@"youtube\.com\/(?:c\/|channel\/|user\/)?(?<channelid>[a-zA-Z0-9\-]{1,})");
new(@"youtube\.com\/(?:c\/|channel\/|user\/)?(?<channelid>[a-zA-Z0-9\-_]{1,})");
[Cmd]
[RequireContext(ContextType.Guild)]

View File

@@ -286,8 +286,7 @@ public sealed class PatronageService
}
}
public async Task<bool> ExecPreCommandAsync(
ICommandContext ctx,
public async Task<bool> ExecPreCommandAsync(ICommandContext ctx,
string moduleName,
CommandInfo command)
{

View File

@@ -2,8 +2,6 @@
using NadekoBot.Common.Configs;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using System.Collections.Immutable;
using Nadeko.Common;
using ExecuteResult = Discord.Commands.ExecuteResult;
using PreconditionResult = Discord.Commands.PreconditionResult;
@@ -219,7 +217,7 @@ public class CommandHandler : INService, IReadyExecutor
try
{
#if !GLOBAL_NADEKO
// track how many messagges each user is sending
// track how many messages each user is sending
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (_, old) => ++old);
#endif
@@ -254,7 +252,7 @@ public class CommandHandler : INService, IReadyExecutor
var prefix = GetPrefix(guild?.Id);
var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase);
// execute the command and measure the time it took
if (messageContent.StartsWith(prefix, StringComparison.InvariantCulture) || isPrefixCommand)
if (isPrefixCommand || messageContent.StartsWith(prefix, StringComparison.InvariantCulture))
{
var context = new CommandContext(_client, usrMsg);
var (success, error, info) = await ExecuteCommandAsync(context,
@@ -262,6 +260,7 @@ public class CommandHandler : INService, IReadyExecutor
isPrefixCommand ? 1 : prefix.Length,
_services,
MultiMatchHandling.Best);
startTime = Environment.TickCount - startTime;
// if a command is found
@@ -348,11 +347,10 @@ public class CommandHandler : INService, IReadyExecutor
switch (multiMatchHandling)
{
case MultiMatchHandling.Best:
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First())
.ToImmutableArray();
argList = parseResult.ArgValues
.Map(x => x.Values.MaxBy(y => y.Score));
paramList = parseResult.ParamValues
.Select(x => x.Values.OrderByDescending(y => y.Score).First())
.ToImmutableArray();
.Map(x => x.Values.MaxBy(y => y.Score));
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}

View File

@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
public sealed class StatsService : IStatsService, IReadyExecutor, INService
{
public const string BOT_VERSION = "4.3.7";
public const string BOT_VERSION = "4.3.8";
public string Author
=> "Kwoth#2452";

View File

@@ -1371,4 +1371,6 @@ patronmessage:
- patronmessage
- patronmsg
eval:
- eval
- eval
autopublish:
- autopublish

View File

@@ -371,7 +371,7 @@ setchanlname:
args:
- "NewName"
prune:
desc: "`{0}prune` removes all Nadeko's messages in the last 100 messages. `{0}prune X` removes last `X` number of messages from the channel (up to 100). `{0}prune @Someone` removes all Someone's messages in the last 100 messages. `{0}prune @Someone X` removes last `X` number of 'Someone's' messages in the channel. You can use the `-s` / `--safe` parameter at the end to only prune messages that are not pinned."
desc: "`{0}prune` removes all Nadeko's messages in the last 100 messages. `{0}prune X` removes last `X` number of messages from the channel (up to 100). `{0}prune @Someone` removes all Someone's messages in the last 100 messages. `{0}prune @Someone X` removes last `X` number of 'Someone's' messages in the channel."
args:
- ""
- "-s"
@@ -414,7 +414,7 @@ savechat:
args:
- "150"
remind:
desc: "Sends a message to you or a channel after certain amount of time (max 2 months). First parameter is `me`/`here`/'channelname'. Second parameter is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third parameter is a (multiword) message."
desc: "Sends a message to you or a channel after certain amount of time (max 2 months). First parameter is `me`/`here`/'channelname'. Second parameter is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third parameter is a (multiword) message. Requires ManageMessages server permission if you're targeting a different channel."
args:
- "me 1d5h Do something"
- "#general 1m Start now!"
@@ -2329,4 +2329,8 @@ threadcreate:
threaddelete:
desc: "Delete a thread with the specified name in this channel. Case insensitive."
args:
- "Q&A"
- "Q&A"
autopublish:
desc: "Make the bot automatically publish all messages posted in the news channel this command was executed in."
args:
- ""

View File

@@ -23,6 +23,9 @@
"aar_role_removed": "Users will no longer receive {0} role when they join this server.",
"attachments": "Attachments",
"avatar_changed": "Avatar Changed",
"autopublish_enable": "All messages posted in this channel will be automatically published.",
"autopublish_disable": "Messages in this channel will no longer be automatically published.",
"req_announcement_channel": "This command must be executed in an Announcement channel.",
"bandm": "You have been banned from {0} server.\nReason: {1}",
"banmsg_disabled": "Ban messages are disabled. You can enable them by setting a banmsg to something other than '-'.",
"banmsg_default": "No ban message set. Default behavior will be used.",