Compare commits

..

21 Commits
4.2.1 ... 4.2.4

Author SHA1 Message Date
Kwoth
1a8c9a6cba [skip ci] Version upped to 4.2.4, updated CHANGELOG.md 2022-06-17 22:57:03 +02:00
Kwoth
9d2f251923 Fixed crypto deserialization issue 2022-06-17 14:04:22 +02:00
Kwoth
3744dd287c Revert "Fixed .crypto - some extra fields which were causing deserialization issues"
This reverts commit f65ba100af.
2022-06-17 14:03:30 +02:00
Kwoth
f65ba100af Fixed .crypto - some extra fields which were causing deserialization issues 2022-06-17 14:02:38 +02:00
Kwoth
cc52605c90 [skip ci] Upped version to 4.2.3 2022-06-17 04:42:30 +02:00
Kwoth
3d3dc532dc Made .timely use timestamp tags and fixed a bug 2022-06-17 04:37:08 +02:00
Kwoth
6c58a6a72d Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-16 21:28:16 +02:00
Kwoth
cefd81d810 [skip ci] Use shared coinmarket key instead of public bot's 2022-06-16 21:27:51 +02:00
Kwoth
34c96c697a Merge branch 'hokutochen-v4-patch-06658' into 'v4'
updating docs and some code

See merge request Kwoth/nadekobot!250
2022-06-16 15:56:30 +00:00
Hokuto Chen
1cc5e0e1d8 updating docs and some code 2022-06-16 15:56:30 +00:00
Kwoth
deaedce6c7 Renamed some of the classes which still had 'Cr' instead of 'Expr' in them 2022-06-16 04:03:59 +02:00
Kwoth
91e4d9dffc permission commands should now work for global expressions too 2022-06-16 03:59:45 +02:00
Kwoth
a826f4245f Fixed .streamrole not updating in real time, closes #345 2022-06-16 03:37:19 +02:00
Kwoth
780eec62b3 [ci skip] undoed .gencmdlist path, no effect 2022-06-16 00:56:31 +02:00
Kwoth
dbeb83561a Upped version, updated CHANGELOG.md 2022-06-15 21:39:42 +02:00
Kwoth
6c11d11645 Rss errors will now show error counter until deletion 2022-06-15 13:18:00 +02:00
Kwoth
e9923a7691 Fixed bugs when users update their patreon pledge. Updated some packages 2022-06-15 13:11:27 +02:00
Kwoth
5fbe93d898 Possible fix for pledge updates 2022-06-15 10:24:09 +02:00
Kwoth
65995bdca4 Added missing patron tiers. Servers whose owner is bot owner will get excluded from patron quota. Use discord slowmode or cmdcd + nadeko permissions to limit usage 2022-06-15 10:11:46 +02:00
Kwoth
f7c333b671 Ignoring error if creds_example.yml fails to generate, as this happens in docker containers 2022-06-15 07:48:57 +02:00
Kwoth
f9d18aa086 Added enabled property to .config patron just so it's not empty 2022-06-14 14:43:00 +02:00
33 changed files with 419 additions and 302 deletions

View File

@@ -3,6 +3,31 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [4.2.4] - 17.06.2022
### Fixed
- Fixed `.crypto`, you will still need coinmarketcapApiKey in `creds.yml` in order to make it run consistently as the key is shared
## [4.2.3] - 17.06.2022
### Fixed
- Fixed `.timely` nullref bug and made it nicer
- Fixed `.streamrole` not updating in real time!
- Disabling specific Global Expressions should now work with `.sc` (and other permission commands)
## [4.2.2] - 15.06.2022
### Fixed
- Added missing Patron Tiers and fixed Patron pledge update bugs
- Prevented creds_example.yml error in docker containers from crashing it
### Changed
- Rss feeds will now show error counter before deletion
## [4.2.1] - 14.06.2022 ## [4.2.1] - 14.06.2022
### Added ### Added
@@ -130,6 +155,7 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- `.rh` no longer needs quotes for multi word roles - `.rh` no longer needs quotes for multi word roles
- `.deletexp` will now properly delete server xp too - `.deletexp` will now properly delete server xp too
- Fixed `.crypto` sparklines
- [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum) - [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum)
- [dev] Fixed a bug in .gencmdlist - [dev] Fixed a bug in .gencmdlist
- [dev] small fixes to creds provider - [dev] small fixes to creds provider
@@ -139,15 +165,6 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- `.ddg` removed. - `.ddg` removed.
- [dev] removed some dead code and comments - [dev] removed some dead code and comments
### Obsolete
### Fixed
- Fixed `.crypto` sparklines
## [4.1.6] - 14.05.2022 ## [4.1.6] - 14.05.2022
### Fixed ### Fixed

View File

@@ -3,19 +3,19 @@
### Important ### Important
- For modifying **global** expressions, the ones which will work across all the servers your bot is connected to, you **must** be a Bot Owner. - For modifying **global** expressions, the ones which will work across all the servers your bot is connected to, you **must** be a Bot Owner.
You must also use the commands for adding, deleting and listing these reactions in a direct message with the bot. You must also use the commands for adding, deleting and listing these expressions in a direct message with the bot.
- For modifying **local** expressions, the ones which will only work on the server that they are added on, it is required to have the **Administrator** permission. - For modifying **local** expressions, the ones which will only work on the server that they are added on, it is required to have the **Administrator** permission.
You must also use the commands for adding, deleting and listing these reactions in the server you want the expressions to work on. You must also use the commands for adding, deleting and listing these expressions in the server you want the expressions to work on.
### Commands and Their Use ### Commands and Their Use
| Command Name | Description | Example | | Command Name | Description | Example |
| :----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | | :----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| `.exadd` | Add an expression with a trigger and a response. Running this command in a server requries the Administrator permission. Running this command in DM is Bot Owner only, and adds a new global expression. | `.exadd "hello" Hi there, %user%!` | | `.exa` | Add an expression with a trigger and a response. Running this command in a server requries the Administrator permission. Running this command in DM is Bot Owner only, and adds a new global expression. | `.exadd "hello" Hi there, %user%!` |
| `exl` | Lists a page of global or server expression(15 reactions / expressions per page). Running this command in a DM will list the global expression, while running it in a server will list that server's expression. | `.exl 1` | | `exl` | Lists a page of global or server expression(15 expressions per page). Running this command in a DM will list the global expression, while running it in a server will list that server's expression. | `.exl 1` |
| `.exd` | Deletes an expression based on the provided index. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only, and will delete a global expression. | `.exd 5` | | `.exd` | Deletes an expression based on the provided index. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only, and will delete a global expression. | `.exd 5` |
#### Now that we know the commands let's take a look at an example of adding a command with `.acr`, #### Now that we know the commands let's take a look at an example of adding a command with `.exa`,
`.exadd "Nice Weather" It sure is, %user%!` `.exadd "Nice Weather" It sure is, %user%!`
@@ -35,7 +35,7 @@ Now, if that command was ran in a server, anyone on that server can make the bot
If you want to disable a global expression which you do not like, and you do not want to remove it, or you are not the bot owner, you can do so by adding a new expression with the same trigger on your server, and set the response to `-`. If you want to disable a global expression which you do not like, and you do not want to remove it, or you are not the bot owner, you can do so by adding a new expression with the same trigger on your server, and set the response to `-`.
For example: For example:
`.acr /o/ -` `.exa /o/ -`
Now if you try to trigger `/o/`, it won't print anything even if there is a global expression with the same name. Now if you try to trigger `/o/`, it won't print anything even if there is a global expression with the same name.

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: ##### Compatible operating systems:
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10 - Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10 22.04
- Mint: 19, 20 - Mint: 19, 20
- Debian: 9, 10 - Debian: 9, 10
- CentOS: 7 - CentOS: 7
@@ -63,9 +63,20 @@ Open Terminal (if you're on an installation with a window manager) and navigate
4. Run the bot (type `3` and press enter) 4. Run the bot (type `3` and press enter)
5. 🎉 5. 🎉
## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
## Linux Release ## Linux Release
**⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠** ###### Prerequisites
1. Nadeko requires redis to function
- ubuntu installation command: `sudo apt-get install redis-server`
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
3. Make sure your python is version 3+ with `python --version`
- if it's not, you can install python 3 and make it the default with: `sudo apt-get install python3.8 python-is-python3`
*You can use nadeko bash script [prerequisites installer](https://gitlab.com/Kwoth/nadeko-bash-installer/-/blob/v4/n-prereq.sh) as a reference*
##### Installation Instructions ##### Installation Instructions
@@ -92,19 +103,6 @@ Open Terminal (if you're on an installation with a window manager) and navigate
##### Release Update Instructions ##### Release Update Instructions
###### Prerequisites
1. Nadeko requires redis to function
- ubuntu installation command: `sudo apt-get install redis-server`
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
3. Make sure your python is version 3+ with `python --version`
- if it's not, you can install python 3 and make it the default with: `sudo apt-get install python3.8 python-is-python3`
*You can use nadeko bash script [prerequisites installer](https://gitlab.com/Kwoth/nadeko-bash-installer/-/blob/v4/n-prereq.sh) as a reference*
###### Installation
1. Stop the bot 1. Stop the bot
2. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases> 2. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
- Look for the file called "x.x.x-linux-x64-build.tar" (where `X.X.X` is a version, for example 3.0.4) and download it - Look for the file called "x.x.x-linux-x64-build.tar" (where `X.X.X` is a version, for example 3.0.4) and download it

View File

@@ -63,9 +63,9 @@ You can still install them manually:
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, then just move the `ffmpeg.exe` file to NadekoBot/system - [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, then just move the `ffmpeg.exe` file to NadekoBot/system
- [youtube-dl] - Click to download the file. Then put `youtube-dl.exe` in a path that's in your PATH environment variable. If you don't know what that is, then just move the `youtube-dl.exe` file to NadekoBot/system - [youtube-dl] - Click to download the file. Then put `youtube-dl.exe` in a path that's in your PATH environment variable. If you don't know what that is, then just move the `youtube-dl.exe` file to NadekoBot/system
### Windows From Source ## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠ ### Windows From Source
##### Prerequisites ##### Prerequisites

View File

@@ -77,8 +77,8 @@ Say you want to only enable NSFW commands for a specific role, just do the follo
If you don't want server or global Expressions, just block the module that controls their usage: If you don't want server or global Expressions, just block the module that controls their usage:
1. `.sm Expressions disable` 1. `.sm ActualExpressions disable`
- Disables the ActualCustomReactions module from being used - Disables the ActualExpression module from being used
**Note**: The `Expressions` module controls the usage of Expressions. The `Expressions` module controls commands related to Expressions (such as `.acr`, `.lcr`, `.crca`, etc). **Note**: The `Expressions` module controls the usage of Expressions. The `Expressions` module controls commands related to Expressions (such as `.acr`, `.lcr`, `.crca`, etc).

View File

@@ -92,4 +92,3 @@ nav:
- medusa/snek-lifecycle.md - medusa/snek-lifecycle.md
- Contribution Guide: contribution-guide.md - Contribution Guide: contribution-guide.md
- Donate: donate.md - Donate: donate.md
- License: license.md

View File

@@ -0,0 +1,62 @@
using System.Threading.Channels;
namespace NadekoBot.Common;
public sealed class QueueRunner
{
private readonly Channel<Func<Task>> _channel;
private readonly int _delayMs;
public QueueRunner(int delayMs = 0, int maxCapacity = -1)
{
if (delayMs < 0)
throw new ArgumentOutOfRangeException(nameof(delayMs));
_delayMs = delayMs;
_channel = maxCapacity switch
{
0 or < -1 => throw new ArgumentOutOfRangeException(nameof(maxCapacity)),
-1 => Channel.CreateUnbounded<Func<Task>>(new UnboundedChannelOptions()
{
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = true,
}),
_ => Channel.CreateBounded<Func<Task>>(new BoundedChannelOptions(maxCapacity)
{
Capacity = maxCapacity,
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = true
})
};
}
public async Task RunAsync(CancellationToken cancel = default)
{
while (true)
{
var func = await _channel.Reader.ReadAsync(cancel);
try
{
await func();
}
catch (Exception ex)
{
Log.Warning(ex, "Exception executing a staggered func: {ErrorMessage}", ex.Message);
}
finally
{
if (_delayMs != 0)
{
await Task.Delay(_delayMs, cancel);
}
}
}
}
public ValueTask Enqueue(Func<Task> action)
=> _channel.Writer.WriteAsync(action);
}

View File

@@ -31,38 +31,38 @@ public sealed class CommandTypeReader : NadekoTypeReader<CommandInfo>
} }
} }
public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo> public sealed class CommandOrExprTypeReader : NadekoTypeReader<CommandOrExprInfo>
{ {
private readonly CommandService _cmds; private readonly CommandService _cmds;
private readonly CommandHandler _commandHandler; private readonly CommandHandler _commandHandler;
private readonly NadekoExpressionsService _exprs; private readonly NadekoExpressionsService _exprs;
public CommandOrCrTypeReader(CommandService cmds, NadekoExpressionsService exprs, CommandHandler commandHandler) public CommandOrExprTypeReader(CommandService cmds, NadekoExpressionsService exprs, CommandHandler commandHandler)
{ {
_cmds = cmds; _cmds = cmds;
_exprs = exprs; _exprs = exprs;
_commandHandler = commandHandler; _commandHandler = commandHandler;
} }
public override async ValueTask<TypeReaderResult<CommandOrCrInfo>> ReadAsync(ICommandContext ctx, string input) public override async ValueTask<TypeReaderResult<CommandOrExprInfo>> ReadAsync(ICommandContext ctx, string input)
{ {
input = input.ToUpperInvariant(); input = input.ToUpperInvariant();
if (_exprs.ExpressionExists(ctx.Guild?.Id, input)) if (_exprs.ExpressionExists(ctx.Guild?.Id, input) || _exprs.ExpressionExists(null, input))
return TypeReaderResult.FromSuccess(new CommandOrCrInfo(input, CommandOrCrInfo.Type.Custom)); return TypeReaderResult.FromSuccess(new CommandOrExprInfo(input, CommandOrExprInfo.Type.Custom));
var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(ctx, input); var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(ctx, input);
if (cmd.IsSuccess) if (cmd.IsSuccess)
{ {
return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name, return TypeReaderResult.FromSuccess(new CommandOrExprInfo(((CommandInfo)cmd.Values.First().Value).Name,
CommandOrCrInfo.Type.Normal)); CommandOrExprInfo.Type.Normal));
} }
return TypeReaderResult.FromError<CommandOrCrInfo>(CommandError.ParseFailed, "No such command or cr found."); return TypeReaderResult.FromError<CommandOrExprInfo>(CommandError.ParseFailed, "No such command or expression found.");
} }
} }
public class CommandOrCrInfo public class CommandOrExprInfo
{ {
public enum Type public enum Type
{ {
@@ -76,7 +76,7 @@ public class CommandOrCrInfo
public bool IsCustom public bool IsCustom
=> CmdType == Type.Custom; => CmdType == Type.Custom;
public CommandOrCrInfo(string input, Type type) public CommandOrExprInfo(string input, Type type)
{ {
Name = input; Name = input;
CmdType = type; CmdType = type;

View File

@@ -35,4 +35,14 @@ public class PatronUser
// Date Only component // Date Only component
public DateTime ValidThru { get; set; } public DateTime ValidThru { get; set; }
public PatronUser Clone()
=> new PatronUser()
{
UniquePlatformUserId = this.UniquePlatformUserId,
UserId = this.UserId,
AmountCents = this.AmountCents,
LastCharge = this.LastCharge,
ValidThru = this.ValidThru
};
} }

View File

@@ -606,4 +606,4 @@ public class GreetService : INService, IReadyExecutor
} }
#endregion #endregion
} }

View File

@@ -14,7 +14,7 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async partial Task DiscordPermOverride(CommandOrCrInfo cmd, params GuildPerm[] perms) public async partial Task DiscordPermOverride(CommandOrExprInfo cmd, params GuildPerm[] perms)
{ {
if (perms is null || perms.Length == 0) if (perms is null || perms.Length == 0)
{ {

View File

@@ -189,7 +189,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
continue; continue;
} }
// if CA is disabled, and CR has AllowTarget, then the // if CA is disabled, and expr has AllowTarget, then the
// content has to start with the trigger followed by a space // content has to start with the trigger followed by a space
if (expr.AllowTarget if (expr.AllowTarget
&& content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase) && content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase)

View File

@@ -75,19 +75,4 @@ public sealed class BankService : IBankService, INService
?.Balance ?.Balance
?? 0; ?? 0;
} }
public async Task<long> BurnAllAsync(ulong userId)
{
await using var ctx = _db.GetDbContext();
var output = await ctx.GetTable<BankUser>()
.Where(x => x.UserId == userId)
.UpdateWithOutputAsync(old => new()
{
Balance = 0
});
if (output.Length == 0)
return 0;
return output[0].Deleted.Balance;
}
} }

View File

@@ -5,5 +5,4 @@ public interface IBankService
Task<bool> DepositAsync(ulong userId, long amount); Task<bool> DepositAsync(ulong userId, long amount);
Task<bool> WithdrawAsync(ulong userId, long amount); Task<bool> WithdrawAsync(ulong userId, long amount);
Task<long> GetBalanceAsync(ulong userId); Task<long> GetBalanceAsync(ulong userId);
Task<long> BurnAllAsync(ulong userId);
} }

View File

@@ -126,13 +126,15 @@ public partial class Gambling : GamblingModule<GamblingService>
if (_cache.AddTimelyClaim(ctx.User.Id, period) is { } rem) if (_cache.AddTimelyClaim(ctx.User.Id, period) is { } rem)
{ {
await ReplyErrorLocalizedAsync(strs.timely_already_claimed(rem.ToString(@"dd\d\ hh\h\ mm\m\ ss\s"))); var now = DateTime.UtcNow;
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
await ReplyErrorLocalizedAsync(strs.timely_already_claimed(relativeTag));
return; return;
} }
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0); var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
val = (int)(val * (1 + (result.Quota * 0.01f))); val = (int)(val * (1 + (result.Quota! * 0.01f)));
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim")); await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));

View File

@@ -136,7 +136,7 @@ public class ChatterBotService : IExecOnMessage
var channel = (ITextChannel)usrMsg.Channel; var channel = (ITextChannel)usrMsg.Channel;
var conf = _ps.GetConfig(); var conf = _ps.GetConfig();
if (conf.IsEnabled) if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
{ {
var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0); var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0);

View File

@@ -145,7 +145,7 @@ public partial class Help : NadekoModule<HelpService>
return "❓"; return "❓";
case "administration": case "administration":
return "🛠️"; return "🛠️";
case "customreactions": case "expressions":
return "🗣️"; return "🗣️";
case "searches": case "searches":
return "🔍"; return "🔍";
@@ -402,7 +402,7 @@ public partial class Help : NadekoModule<HelpService>
ContentType = "application/json", ContentType = "application/json",
ContentBody = uploadData, ContentBody = uploadData,
// either use a path provided in the argument or the default one for public nadeko, other/cmds.json // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
Key = $"cmds/v4/{StatsService.BOT_VERSION}.json", Key = $"cmds/{StatsService.BOT_VERSION}.json",
CannedACL = S3CannedACL.PublicRead CannedACL = S3CannedACL.PublicRead
}); });
} }
@@ -414,7 +414,7 @@ public partial class Help : NadekoModule<HelpService>
using var oldVersionObject = await dlClient.GetObjectAsync(new() using var oldVersionObject = await dlClient.GetObjectAsync(new()
{ {
BucketName = "nadeko-pictures", BucketName = "nadeko-pictures",
Key = "cmds/v4/versions.json" Key = "cmds/versions.json"
}); });
await using var ms = new MemoryStream(); await using var ms = new MemoryStream();
@@ -445,7 +445,7 @@ public partial class Help : NadekoModule<HelpService>
ContentType = "application/json", ContentType = "application/json",
ContentBody = versionListString, ContentBody = versionListString,
// either use a path provided in the argument or the default one for public nadeko, other/cmds.json // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
Key = "cmds/v4/versions.json", Key = "cmds/versions.json",
CannedACL = S3CannedACL.PublicRead CannedACL = S3CannedACL.PublicRead
}); });
} }

View File

@@ -79,7 +79,7 @@ public partial class Permissions
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[Priority(1)] [Priority(1)]
public partial Task CmdCooldown(CommandOrCrInfo command, int secs) public partial Task CmdCooldown(CommandOrExprInfo command, int secs)
=> CmdCooldownInternal(command.Name, secs); => CmdCooldownInternal(command.Name, secs);
[Cmd] [Cmd]

View File

@@ -60,7 +60,7 @@ public partial class Permissions
[Cmd] [Cmd]
[OwnerOnly] [OwnerOnly]
public async partial Task GlobalCommand(CommandOrCrInfo cmd) public async partial Task GlobalCommand(CommandOrExprInfo cmd)
{ {
var commandName = cmd.Name.ToLowerInvariant(); var commandName = cmd.Name.ToLowerInvariant();
var added = _service.ToggleCommand(commandName); var added = _service.ToggleCommand(commandName);

View File

@@ -204,7 +204,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async partial Task SrvrCmd(CommandOrCrInfo command, PermissionAction action) public async partial Task SrvrCmd(CommandOrExprInfo command, PermissionAction action)
{ {
await _service.AddPermissions(ctx.Guild.Id, await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2 new Permissionv2
@@ -245,7 +245,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async partial Task UsrCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] IGuildUser user) public async partial Task UsrCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IGuildUser user)
{ {
await _service.AddPermissions(ctx.Guild.Id, await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2 new Permissionv2
@@ -302,7 +302,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async partial Task RoleCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] IRole role) public async partial Task RoleCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] IRole role)
{ {
if (role == role.Guild.EveryoneRole) if (role == role.Guild.EveryoneRole)
return; return;
@@ -366,7 +366,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async partial Task ChnlCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] ITextChannel chnl) public async partial Task ChnlCmd(CommandOrExprInfo command, PermissionAction action, [Leftover] ITextChannel chnl)
{ {
await _service.AddPermissions(ctx.Guild.Id, await _service.AddPermissions(ctx.Guild.Id,
new Permissionv2 new Permissionv2

View File

@@ -49,7 +49,7 @@ public class FeedsService : INService
private void ClearErrors(string url) private void ClearErrors(string url)
=> _errorCounters.Remove(url); => _errorCounters.Remove(url);
private async Task AddError(string url, List<int> ids) private async Task<uint> AddError(string url, List<int> ids)
{ {
try try
{ {
@@ -68,10 +68,13 @@ public class FeedsService : INService
// reset the error counter // reset the error counter
ClearErrors(url); ClearErrors(url);
} }
return newValue;
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Error adding rss errors..."); Log.Error(ex, "Error adding rss errors...");
return 0;
} }
} }
@@ -181,12 +184,13 @@ public class FeedsService : INService
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Warning("An error occured while getting rss stream {RssFeed}" var errorCount = await AddError(rssUrl, kvp.Value.Select(x => x.Id).ToList());
Log.Warning("An error occured while getting rss stream ({ErrorCount} / 100) {RssFeed}"
+ "\n {Message}", + "\n {Message}",
errorCount,
rssUrl, rssUrl,
$"[{ex.GetType().Name}]: {ex.Message}"); $"[{ex.GetType().Name}]: {ex.Message}");
await AddError(rssUrl, kvp.Value.Select(x => x.Id).ToList());
} }
} }

View File

@@ -16,11 +16,11 @@ public class CmcQuote
[JsonPropertyName("volume_24h")] [JsonPropertyName("volume_24h")]
public double Volume24h { get; set; } public double Volume24h { get; set; }
[JsonPropertyName("volume_change_24h")] // [JsonPropertyName("volume_change_24h")]
public double VolumeChange24h { get; set; } // public double VolumeChange24h { get; set; }
//
[JsonPropertyName("percent_change_1h")] // [JsonPropertyName("percent_change_1h")]
public double PercentChange1h { get; set; } // public double PercentChange1h { get; set; }
[JsonPropertyName("percent_change_24h")] [JsonPropertyName("percent_change_24h")]
public double PercentChange24h { get; set; } public double PercentChange24h { get; set; }
@@ -33,12 +33,6 @@ public class CmcQuote
[JsonPropertyName("market_cap_dominance")] [JsonPropertyName("market_cap_dominance")]
public double MarketCapDominance { get; set; } public double MarketCapDominance { get; set; }
[JsonPropertyName("fully_diluted_market_cap")]
public double FullyDilutedMarketCap { get; set; }
[JsonPropertyName("last_updated")]
public DateTime LastUpdated { get; set; }
} }
public class CmcResponseData public class CmcResponseData
@@ -58,9 +52,6 @@ public class CmcResponseData
[JsonPropertyName("cmc_rank")] [JsonPropertyName("cmc_rank")]
public int CmcRank { get; set; } public int CmcRank { get; set; }
[JsonPropertyName("num_market_pairs")]
public int NumMarketPairs { get; set; }
[JsonPropertyName("circulating_supply")] [JsonPropertyName("circulating_supply")]
public double? CirculatingSupply { get; set; } public double? CirculatingSupply { get; set; }
@@ -70,15 +61,6 @@ public class CmcResponseData
[JsonPropertyName("max_supply")] [JsonPropertyName("max_supply")]
public double? MaxSupply { get; set; } public double? MaxSupply { get; set; }
[JsonPropertyName("last_updated")]
public DateTime LastUpdated { get; set; }
[JsonPropertyName("date_added")]
public DateTime DateAdded { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; }
[JsonPropertyName("quote")] [JsonPropertyName("quote")]
public Dictionary<string, CmcQuote> Quote { get; set; } public Dictionary<string, CmcQuote> Quote { get; set; }
} }

View File

@@ -14,5 +14,9 @@ public class PatronageConfig : ConfigServiceBase<PatronConfigData>
public PatronageConfig(IConfigSeria serializer, IPubSub pubSub) : base(FILE_PATH, serializer, pubSub, _changeKey) public PatronageConfig(IConfigSeria serializer, IPubSub pubSub) : base(FILE_PATH, serializer, pubSub, _changeKey)
{ {
AddParsedProp("enabled",
x => x.IsEnabled,
bool.TryParse,
ConfigPrinters.ToString);
} }
} }

View File

@@ -50,67 +50,91 @@ public class CurrencyRewardService : INService, IDisposable
private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron) private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron)
{ {
if (oldPatron.Amount != newPatron.Amount) // if pledge was increased
if (oldPatron.Amount < newPatron.Amount)
{ {
var conf = _config.Data; var conf = _config.Data;
var newAmount = (long)(newPatron.Amount * conf.PatreonCurrencyPerCent);
var newAmount = (long)(Math.Max(newPatron.Amount, oldPatron.Amount) * conf.PatreonCurrencyPerCent); RewardedUser old;
UpdateOutput<RewardedUser>[] output;
await using (var ctx = _db.GetDbContext()) await using (var ctx = _db.GetDbContext())
{ {
output = await ctx.GetTable<RewardedUser>() old = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UnqiuePlatformUserId) .Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.UpdateWithOutputAsync(old => new() .FirstOrDefaultAsync();
if (old is null)
{
await OnNewPayment(newPatron);
return;
}
// no action as the amount is the same or lower
if (old.AmountRewardedThisMonth >= newAmount)
return;
var count = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.UpdateAsync(_ => new()
{ {
PlatformUserId = newPatron.UnqiuePlatformUserId, PlatformUserId = newPatron.UniquePlatformUserId,
UserId = newPatron.UserId, UserId = newPatron.UserId,
// amount before bonuses // amount before bonuses
AmountRewardedThisMonth = newAmount, AmountRewardedThisMonth = newAmount,
LastReward = newPatron.PaidAt LastReward = newPatron.PaidAt
}); });
// shouldn't ever happen
if (count == 0)
return;
} }
// if the user wasn't previously in the db for some reason, var oldAmount = old.AmountRewardedThisMonth;
// we will treat him as a new patron
if (output.Length == 0)
{
await OnNewPayment(newPatron);
return;
}
var oldAmount = output[0].Deleted.AmountRewardedThisMonth; var realNewAmount = GetRealCurrencyReward(
(int)(newAmount / conf.PatreonCurrencyPerCent),
var diff = newAmount - oldAmount; newAmount,
out var percentBonus);
var realOldAmount = GetRealCurrencyReward(
(int)(oldAmount / conf.PatreonCurrencyPerCent),
oldAmount,
out _);
var diff = realNewAmount - realOldAmount;
if (diff <= 0) if (diff <= 0)
return; // no action if new is lower return; // no action if new is lower
// if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars, // if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars,
// up to 100% // up to 100%
var realAmount = GetRealCurrencyReward(newPatron.Amount, diff, out var percentBonus); await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update"));
await _cs.AddAsync(newPatron.UserId, realAmount, new TxData("patron","update"));
_ = SendMessageToUser(newPatron.UserId, _ = SendMessageToUser(newPatron.UserId,
$"You've received an additional **{realAmount}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!"); $"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!");
} }
} }
private long GetRealCurrencyReward(int fullPledge, long currentAmount, out int percentBonus) private long GetRealCurrencyReward(int pledgeCents, long modifiedAmount, out int percentBonus)
{ {
// needs at least 5$ to be eligible for a bonus // needs at least 5$ to be eligible for a bonus
if (fullPledge < 500) if (pledgeCents < 500)
{ {
percentBonus = 0; percentBonus = 0;
return currentAmount; return modifiedAmount;
} }
var dollarValue = fullPledge / 100; var dollarValue = pledgeCents / 100;
percentBonus = dollarValue switch percentBonus = dollarValue switch
{ {
> 100 => 100, >= 100 => 100,
_ => dollarValue >= 50 => 50,
>= 20 => 20,
>= 10 => 10,
>= 5 => 5,
_ => 0
}; };
return (long)(currentAmount * (1 + (percentBonus / 100.0f))); return (long)(modifiedAmount * (1 + (percentBonus / 100.0f)));
} }
// on a new payment, always give the full amount. // on a new payment, always give the full amount.
@@ -121,7 +145,7 @@ public class CurrencyRewardService : INService, IDisposable
await ctx.GetTable<RewardedUser>() await ctx.GetTable<RewardedUser>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
PlatformUserId = patron.UnqiuePlatformUserId, PlatformUserId = patron.UniquePlatformUserId,
UserId = patron.UserId, UserId = patron.UserId,
AmountRewardedThisMonth = amount, AmountRewardedThisMonth = amount,
LastReward = patron.PaidAt, LastReward = patron.PaidAt,
@@ -134,7 +158,7 @@ public class CurrencyRewardService : INService, IDisposable
}, },
() => new() () => new()
{ {
PlatformUserId = patron.UnqiuePlatformUserId PlatformUserId = patron.UniquePlatformUserId
}); });
var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus); var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus);
@@ -167,24 +191,9 @@ public class CurrencyRewardService : INService, IDisposable
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
_ = await ctx.GetTable<RewardedUser>() _ = await ctx.GetTable<RewardedUser>()
.UpdateWithOutputAsync(old => new() .UpdateAsync(old => new()
{ {
AmountRewardedThisMonth = old.AmountRewardedThisMonth * 2 AmountRewardedThisMonth = old.AmountRewardedThisMonth * 2
}); });
// var toTake = old.Length == 0
// ? patron.Amount
// : old[0].Inserted.AmountRewardedThisMonth;
// if (toTake > 0)
// {
// Log.Warning("Wiping the wallet and bank of the user {UserId} due to a refund/fraud...",
// patron.UserId);
// await _cs.RemoveAsync(patron.UserId, patron.Amount, new("patreon", "refund"));
// await _bs.BurnAllAsync(patron.UserId);
// Log.Warning("Burned {Amount} currency from the bank of the user {UserId} due to a refund/fraud.",
// patron.Amount,
// patron.UserId);
// }
} }
} }

View File

@@ -5,10 +5,10 @@ public readonly struct Patron
/// <summary> /// <summary>
/// Unique id assigned to this patron by the payment platform /// Unique id assigned to this patron by the payment platform
/// </summary> /// </summary>
public string UnqiuePlatformUserId { get; init; } public string UniquePlatformUserId { get; init; }
/// <summary> /// <summary>
/// Discord UserId to which this <see cref="UnqiuePlatformUserId"/> is connected to /// Discord UserId to which this <see cref="UniquePlatformUserId"/> is connected to
/// </summary> /// </summary>
public ulong UserId { get; init; } public ulong UserId { get; init; }

View File

@@ -173,7 +173,7 @@ public sealed class PatronageService
var lastChargeUtc = subscriber.LastCharge.Value.ToUniversalTime(); var lastChargeUtc = subscriber.LastCharge.Value.ToUniversalTime();
var dateInOneMonth = lastChargeUtc.Date.AddMonths(1); var dateInOneMonth = lastChargeUtc.Date.AddMonths(1);
await using var tran = await ctx.Database.BeginTransactionAsync(); // await using var tran = await ctx.Database.BeginTransactionAsync();
try try
{ {
var dbPatron = await ctx.GetTable<PatronUser>() var dbPatron = await ctx.GetTable<PatronUser>()
@@ -193,7 +193,7 @@ public sealed class PatronageService
ValidThru = dateInOneMonth, ValidThru = dateInOneMonth,
}); });
await tran.CommitAsync(); // await tran.CommitAsync();
var newPatron = PatronUserToPatron(dbPatron); var newPatron = PatronUserToPatron(dbPatron);
_ = SendWelcomeMessage(newPatron); _ = SendWelcomeMessage(newPatron);
@@ -222,35 +222,38 @@ public sealed class PatronageService
// this should never happen // this should never happen
if (count == 0) if (count == 0)
{ {
await tran.RollbackAsync(); // await tran.RollbackAsync();
continue; continue;
} }
await tran.CommitAsync(); // await tran.CommitAsync();
await OnNewPatronPayment(PatronUserToPatron(dbPatron)); await OnNewPatronPayment(PatronUserToPatron(dbPatron));
} }
else if (dbPatron.AmountCents != subscriber.Cents // if user changed the amount else if (dbPatron.AmountCents != subscriber.Cents // if user changed the amount
|| dbPatron.UserId != subscriber.UserId) // if user updated user id) || dbPatron.UserId != subscriber.UserId) // if user updated user id)
{ {
var cents = subscriber.Cents;
// the user updated the pledge or changed the connected discord account // the user updated the pledge or changed the connected discord account
var newData = await ctx.GetTable<PatronUser>() await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
&& x.LastCharge < lastChargeUtc) .UpdateAsync(old => new()
.UpdateWithOutputAsync(old => new() {
{ UserId = subscriber.UserId,
UserId = subscriber.UserId, AmountCents = cents,
AmountCents = subscriber.Cents, LastCharge = lastChargeUtc,
LastCharge = lastChargeUtc, ValidThru = old.ValidThru,
ValidThru = old.ValidThru, });
});
await tran.CommitAsync(); var newPatron = dbPatron.Clone();
newPatron.AmountCents = cents;
// this should never happen newPatron.UserId = subscriber.UserId;
if (newData.Length == 0)
continue; // idk what's going on but UpdateWithOutputAsync doesn't work properly here
// nor does firstordefault after update. I'm not seeing something obvious
await OnPatronUpdated(PatronUserToPatron(dbPatron), PatronUserToPatron(newData[0].Inserted)); await OnPatronUpdated(
PatronUserToPatron(dbPatron),
PatronUserToPatron(newPatron));
} }
} }
} }
@@ -262,22 +265,26 @@ public sealed class PatronageService
} }
} }
var expiredDate = DateTime.MinValue;
foreach (var patron in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Refunded)) foreach (var patron in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Refunded))
{ {
var expiredDate = DateTime.MinValue;
// if the subscription is refunded, Disable user's valid thru // if the subscription is refunded, Disable user's valid thru
var output = await ctx.GetTable<PatronUser>() var changedCount = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId
&& x.ValidThru != expiredDate) && x.ValidThru != expiredDate)
.UpdateWithOutputAsync(old => new() .UpdateAsync(old => new()
{ {
ValidThru = expiredDate ValidThru = expiredDate
}); });
if (output.Length == 0) if (changedCount == 0)
continue; continue;
await OnPatronRefunded(PatronUserToPatron(output[0].Inserted)); var updated = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId)
.FirstAsync();
await OnPatronRefunded(PatronUserToPatron(updated));
} }
} }
@@ -641,7 +648,6 @@ public sealed class PatronageService
}; };
} }
// should i allow users to pay extra for more quota?
private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats( private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats(
PatronTier patronTier, PatronTier patronTier,
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict, IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
@@ -691,7 +697,7 @@ public sealed class PatronageService
return new() return new()
{ {
Name = key.PrettyName, Name = key.PrettyName,
Quota = default, Quota = defaultValue,
IsPatronLimit = false IsPatronLimit = false
}; };
@@ -732,7 +738,7 @@ public sealed class PatronageService
private Patron PatronUserToPatron(PatronUser user) private Patron PatronUserToPatron(PatronUser user)
=> new Patron() => new Patron()
{ {
UnqiuePlatformUserId = user.UniquePlatformUserId, UniquePlatformUserId = user.UniquePlatformUserId,
UserId = user.UserId, UserId = user.UserId,
Amount = user.AmountCents, Amount = user.AmountCents,
Tier = CalculateTier(user), Tier = CalculateTier(user),
@@ -747,6 +753,9 @@ public sealed class PatronageService
return user.AmountCents switch return user.AmountCents switch
{ {
>= 10_000 => PatronTier.C,
>= 5000 => PatronTier.L,
>= 2000 => PatronTier.XX,
>= 1000 => PatronTier.X, >= 1000 => PatronTier.X,
>= 500 => PatronTier.V, >= 500 => PatronTier.V,
>= 100 => PatronTier.I, >= 100 => PatronTier.I,
@@ -782,7 +791,7 @@ public sealed class PatronageService
*- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Nadeko's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.* *- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Nadeko's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.*
*- Permission guide can be found here if you're not familiar with it: <https://nadekobot.readthedocs.io/en/latest/permissions-system/>*", *- Permission guide can be found here if you're not familiar with it: <https://nadekobot.readthedocs.io/en/latest/permissions-system/>*",
isInline: false) isInline: false)
.WithFooter($"platform id: {patron.UnqiuePlatformUserId}"); .WithFooter($"platform id: {patron.UniquePlatformUserId}");
await user.EmbedAsync(eb); await user.EmbedAsync(eb);
} }

View File

@@ -1,17 +1,20 @@
#nullable disable #nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db; using NadekoBot.Db;
using NadekoBot.Modules.Utility.Common; using NadekoBot.Modules.Utility.Common;
using NadekoBot.Modules.Utility.Common.Exceptions; using NadekoBot.Modules.Utility.Common.Exceptions;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using System.Diagnostics;
using System.Net; using System.Net;
namespace NadekoBot.Modules.Utility.Services; namespace NadekoBot.Modules.Utility.Services;
public class StreamRoleService : INService public class StreamRoleService : IReadyExecutor, INService
{ {
private readonly DbService _db; private readonly DbService _db;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, StreamRoleSettings> _guildSettings; private readonly ConcurrentDictionary<ulong, StreamRoleSettings> _guildSettings;
private readonly QueueRunner _queueRunner;
public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot) public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot)
{ {
@@ -22,33 +25,35 @@ public class StreamRoleService : INService
.Where(x => x.Value is { Enabled: true }) .Where(x => x.Value is { Enabled: true })
.ToConcurrent(); .ToConcurrent();
_client.GuildMemberUpdated += Client_GuildMemberUpdated; _client.PresenceUpdated += OnPresenceUpdate;
_ = Task.Run(async () => _queueRunner = new QueueRunner();
{
try
{
await client.Guilds.Select(g => RescanUsers(g)).WhenAll();
}
catch
{
// ignored
}
});
} }
private Task Client_GuildMemberUpdated(Cacheable<SocketGuildUser, ulong> cacheable, SocketGuildUser after) private Task OnPresenceUpdate(SocketUser user, SocketPresence oldPresence, SocketPresence newPresence)
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
//if user wasn't streaming or didn't have a game status at all if (oldPresence.Activities.Count != newPresence.Activities.Count)
if (_guildSettings.TryGetValue(after.Guild.Id, out var setting)) {
await RescanUser(after, setting); var guildUsers = _client.Guilds
.Select(x => x.GetUser(user.Id));
foreach (var guildUser in guildUsers)
{
if (_guildSettings.TryGetValue(guildUser.Guild.Id, out var s))
await RescanUser(guildUser, s);
}
}
}); });
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task OnReadyAsync()
=> Task.WhenAll(_client.Guilds.Select(RescanUsers).WhenAll(), _queueRunner.RunAsync());
/// <summary> /// <summary>
/// Adds or removes a user from a blacklist or a whitelist in the specified guild. /// Adds or removes a user from a blacklist or a whitelist in the specified guild.
/// </summary> /// </summary>
@@ -135,7 +140,7 @@ public class StreamRoleService : INService
streamRoleSettings.Keyword = keyword; streamRoleSettings.Keyword = keyword;
UpdateCache(guild.Id, streamRoleSettings); UpdateCache(guild.Id, streamRoleSettings);
uow.SaveChanges(); await uow.SaveChangesAsync();
} }
await RescanUsers(guild); await RescanUsers(guild);
@@ -191,8 +196,7 @@ public class StreamRoleService : INService
foreach (var usr in await fromRole.GetMembersAsync()) foreach (var usr in await fromRole.GetMembersAsync())
{ {
if (usr is { } x) await RescanUser(usr, setting, addRole);
await RescanUser(x, setting, addRole);
} }
} }
@@ -216,7 +220,10 @@ public class StreamRoleService : INService
await RescanUsers(guild); await RescanUsers(guild);
} }
private async Task RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null) private async ValueTask RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
=> await _queueRunner.Enqueue(() => RescanUserInternal(user, setting, addRole));
private async Task RescanUserInternal(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
{ {
if (user.IsBot) if (user.IsBot)
return; return;
@@ -232,58 +239,77 @@ public class StreamRoleService : INService
&& setting.Blacklist.All(x => x.UserId != user.Id) && setting.Blacklist.All(x => x.UserId != user.Id)
&& user.RoleIds.Contains(setting.FromRoleId)) && user.RoleIds.Contains(setting.FromRoleId))
{ {
try await _queueRunner.Enqueue(async () =>
{ {
addRole ??= user.Guild.GetRole(setting.AddRoleId); try
if (addRole is null) {
addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
{
await StopStreamRole(user.Guild);
Log.Warning("Stream role in server {RoleId} no longer exists. Stopping", setting.AddRoleId);
return;
}
//check if he doesn't have addrole already, to avoid errors
if (!user.RoleIds.Contains(addRole.Id))
{
await user.AddRoleAsync(addRole);
Log.Information("Added stream role to user {User} in {Server} server",
user.ToString(),
user.Guild.ToString());
}
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{ {
await StopStreamRole(user.Guild); await StopStreamRole(user.Guild);
Log.Warning("Stream role in server {RoleId} no longer exists. Stopping", setting.AddRoleId); Log.Warning(ex, "Error adding stream role(s). Forcibly disabling stream role feature");
return; throw new StreamRolePermissionException();
} }
catch (Exception ex)
//check if he doesn't have addrole already, to avoid errors
if (!user.RoleIds.Contains(addRole.Id))
{ {
await user.AddRoleAsync(addRole); Log.Warning(ex, "Failed adding stream role");
Log.Information("Added stream role to user {User} in {Server} server",
user.ToString(),
user.Guild.ToString());
} }
} });
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{
await StopStreamRole(user.Guild);
Log.Warning(ex, "Error adding stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
catch (Exception ex)
{
Log.Warning(ex, "Failed adding stream role");
}
} }
else else
{ {
//check if user is in the addrole //check if user is in the addrole
if (user.RoleIds.Contains(setting.AddRoleId)) if (user.RoleIds.Contains(setting.AddRoleId))
{ {
try await _queueRunner.Enqueue(async () =>
{ {
addRole ??= user.Guild.GetRole(setting.AddRoleId); try
if (addRole is null) {
throw new StreamRoleNotFoundException(); addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
{
await StopStreamRole(user.Guild);
Log.Warning(
"Addrole doesn't exist in {GuildId} server. Forcibly disabling stream role feature",
user.Guild.Id);
return;
}
await user.RemoveRoleAsync(addRole); // need to check again in case queuer is taking too long to execute
Log.Information("Removed stream role from the user {User} in {Server} server", if (user.RoleIds.Contains(setting.AddRoleId))
user.ToString(), {
user.Guild.ToString()); await user.RemoveRoleAsync(addRole);
} }
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{ Log.Information("Removed stream role from the user {User} in {Server} server",
await StopStreamRole(user.Guild); user.ToString(),
Log.Warning(ex, "Error removing stream role(s). Forcibly disabling stream role feature"); user.Guild.ToString());
throw new StreamRolePermissionException(); }
} catch (HttpException ex)
{
if (ex.HttpCode == HttpStatusCode.Forbidden)
{
await StopStreamRole(user.Guild);
Log.Warning(ex, "Error removing stream role(s). Forcibly disabling stream role feature");
}
}
});
} }
} }
} }

View File

@@ -69,15 +69,15 @@
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<!-- Db-related packages --> <!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="linq2db.EntityFrameworkCore" Version="6.7.1" /> <PackageReference Include="linq2db.EntityFrameworkCore" Version="6.8.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />

View File

@@ -37,8 +37,15 @@ public sealed class BotCredsProvider : IBotCredsProvider
public BotCredsProvider(int? totalShards = null) public BotCredsProvider(int? totalShards = null)
{ {
_totalShards = totalShards; _totalShards = totalShards;
if (!File.Exists(CredsExamplePath)) try
File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds)); {
if (!File.Exists(CredsExamplePath))
File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds));
}
catch
{
// this can fail in docker containers
}
MigrateCredentials(); MigrateCredentials();
@@ -96,8 +103,10 @@ public sealed class BotCredsProvider : IBotCredsProvider
if (string.IsNullOrWhiteSpace(_creds.RedisOptions)) if (string.IsNullOrWhiteSpace(_creds.RedisOptions))
_creds.RedisOptions = "127.0.0.1,syncTimeout=3000"; _creds.RedisOptions = "127.0.0.1,syncTimeout=3000";
if (string.IsNullOrWhiteSpace(_creds.CoinmarketcapApiKey)) // replace the old generated key with the shared key
_creds.CoinmarketcapApiKey = "e79ec505-0913-439d-ae07-069e296a6079"; if (string.IsNullOrWhiteSpace(_creds.CoinmarketcapApiKey)
|| _creds.CoinmarketcapApiKey.StartsWith("e79ec505-0913"))
_creds.CoinmarketcapApiKey = "3077537c-7dfb-4d97-9a60-56fc9a9f5035";
_creds.TotalShards = _totalShards ?? _creds.TotalShards; _creds.TotalShards = _totalShards ?? _creds.TotalShards;
} }

View File

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

View File

@@ -221,7 +221,7 @@ public static class Extensions
public static void Lap(this Stopwatch sw, string checkpoint) public static void Lap(this Stopwatch sw, string checkpoint)
{ {
Log.Information("Checkpoint {CheckPoint}: {Time}", checkpoint, sw.Elapsed.TotalMilliseconds); Log.Information("Checkpoint {CheckPoint}: {Time}ms", checkpoint, sw.Elapsed.TotalMilliseconds);
sw.Restart(); sw.Restart();
} }
} }

View File

@@ -1,66 +1,68 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 1 version: 1
# Whether the patronage feature is enabled # Whether the patronage feature is enabled
isEnabled: false isEnabled: true
# List of patron only features and relevant quota data # List of patron only features and relevant quota data
quotas: quotas:
# Dictionary of feature names with their respective limits. Set to null for unlimited # Dictionary of feature names with their respective limits. Set to null for unlimited
features: features:
"timely:extra_percent": timely:extra_percent:
v: 10 V: 10
x: 22 X: 22
xx: 50 XX: 50
l: 150 L: 150
c: 350 C: 350
"rero:max_count": rero:max_count:
v: 25 V: 25
x: 50 X: 50
"cleverbot:response": cleverbot:response:
v: -20 V: -20
x: 5000 X: 5000
xx: 12000 XX: 12000
l: 35000 L: 35000
c: 100000 C: 100000
# Dictionary of commands with their respective quota data # Dictionary of commands with their respective quota data
commands: commands:
cleverbot:
V: null
prune: prune:
x: X:
perHour: 1 PerHour: 1
xx: XX:
perHour: 3 PerHour: 3
google: google:
v: V:
perDay: 15 PerDay: 15
x: X:
perDay: 30 PerDay: 30
xx: XX:
perDay: 60 PerDay: 60
l: L:
perDay: 150 PerDay: 150
c: C:
perDay: 300 PerDay: 300
image: image:
v: V:
perDay: 15 PerDay: 15
x: X:
perDay: 30 PerDay: 30
xx: XX:
perDay: 60 PerDay: 60
l: L:
perDay: 150 PerDay: 150
c: C:
perDay: 300 PerDay: 300
youtube: youtube:
v: V:
perDay: 25 PerDay: 25
x: X:
perDay: 50 PerDay: 50
xx: XX:
perDay: 100 PerDay: 100
l: L:
perDay: 250 PerDay: 250
c: C:
perDay: 500 PerDay: 500
# Dictionary of groups with their respective quota data # Dictionary of groups with their respective quota data
groups: {} groups: {}
# Dictionary of modules with their respective quota data # Dictionary of modules with their respective quota data

View File

@@ -878,7 +878,7 @@
"autodc_enable": "I will disconnect from the voice channel when there are no more tracks to play.", "autodc_enable": "I will disconnect from the voice channel when there are no more tracks to play.",
"autodc_disable": "I will no longer disconnect from the voice channel when there are no more tracks to play.", "autodc_disable": "I will no longer disconnect from the voice channel when there are no more tracks to play.",
"timely_none": "Bot owner didn't specify a timely reward.", "timely_none": "Bot owner didn't specify a timely reward.",
"timely_already_claimed": "You've already claimed your timely reward. You can get it again in {0}.", "timely_already_claimed": "You've already claimed your timely reward. You can get it again {0}.",
"timely": "You've claimed your {0}. You can claim again in {1}h", "timely": "You've claimed your {0}. You can claim again in {1}h",
"timely_set": "Users will be able to claim {0} every {1}h", "timely_set": "Users will be able to claim {0} every {1}h",
"timely_set_none": "Users will not be able to claim any timely currency.", "timely_set_none": "Users will not be able to claim any timely currency.",