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
## [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
### 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
- `.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] Fixed a bug in .gencmdlist
- [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.
- [dev] removed some dead code and comments
### Obsolete
### Fixed
- Fixed `.crypto` sparklines
## [4.1.6] - 14.05.2022
### Fixed

View File

@@ -3,19 +3,19 @@
### 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.
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.
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
| 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%!` |
| `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` |
| `.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 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` |
#### 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%!`
@@ -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 `-`.
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.

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
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10 22.04
- Mint: 19, 20
- Debian: 9, 10
- 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)
5. 🎉
## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
## 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
@@ -92,19 +103,6 @@ Open Terminal (if you're on an installation with a window manager) and navigate
##### 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
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

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
- [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

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:
1. `.sm Expressions disable`
- Disables the ActualCustomReactions module from being used
1. `.sm ActualExpressions disable`
- 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).

View File

@@ -92,4 +92,3 @@ nav:
- medusa/snek-lifecycle.md
- Contribution Guide: contribution-guide.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 CommandHandler _commandHandler;
private readonly NadekoExpressionsService _exprs;
public CommandOrCrTypeReader(CommandService cmds, NadekoExpressionsService exprs, CommandHandler commandHandler)
public CommandOrExprTypeReader(CommandService cmds, NadekoExpressionsService exprs, CommandHandler commandHandler)
{
_cmds = cmds;
_exprs = exprs;
_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();
if (_exprs.ExpressionExists(ctx.Guild?.Id, input))
return TypeReaderResult.FromSuccess(new CommandOrCrInfo(input, CommandOrCrInfo.Type.Custom));
if (_exprs.ExpressionExists(ctx.Guild?.Id, input) || _exprs.ExpressionExists(null, input))
return TypeReaderResult.FromSuccess(new CommandOrExprInfo(input, CommandOrExprInfo.Type.Custom));
var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(ctx, input);
if (cmd.IsSuccess)
{
return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name,
CommandOrCrInfo.Type.Normal));
return TypeReaderResult.FromSuccess(new CommandOrExprInfo(((CommandInfo)cmd.Values.First().Value).Name,
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
{
@@ -76,7 +76,7 @@ public class CommandOrCrInfo
public bool IsCustom
=> CmdType == Type.Custom;
public CommandOrCrInfo(string input, Type type)
public CommandOrExprInfo(string input, Type type)
{
Name = input;
CmdType = type;

View File

@@ -35,4 +35,14 @@ public class PatronUser
// Date Only component
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

@@ -14,7 +14,7 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[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)
{

View File

@@ -189,7 +189,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
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
if (expr.AllowTarget
&& content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase)

View File

@@ -75,19 +75,4 @@ public sealed class BankService : IBankService, INService
?.Balance
?? 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> WithdrawAsync(ulong userId, long amount);
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)
{
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;
}
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"));

View File

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

View File

@@ -145,7 +145,7 @@ public partial class Help : NadekoModule<HelpService>
return "❓";
case "administration":
return "🛠️";
case "customreactions":
case "expressions":
return "🗣️";
case "searches":
return "🔍";
@@ -402,7 +402,7 @@ public partial class Help : NadekoModule<HelpService>
ContentType = "application/json",
ContentBody = uploadData,
// 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
});
}
@@ -414,7 +414,7 @@ public partial class Help : NadekoModule<HelpService>
using var oldVersionObject = await dlClient.GetObjectAsync(new()
{
BucketName = "nadeko-pictures",
Key = "cmds/v4/versions.json"
Key = "cmds/versions.json"
});
await using var ms = new MemoryStream();
@@ -445,7 +445,7 @@ public partial class Help : NadekoModule<HelpService>
ContentType = "application/json",
ContentBody = versionListString,
// 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
});
}

View File

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

View File

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

View File

@@ -204,7 +204,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd]
[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,
new Permissionv2
@@ -245,7 +245,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd]
[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,
new Permissionv2
@@ -302,7 +302,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd]
[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)
return;
@@ -366,7 +366,7 @@ public partial class Permissions : NadekoModule<PermissionService>
[Cmd]
[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,
new Permissionv2

View File

@@ -49,7 +49,7 @@ public class FeedsService : INService
private void ClearErrors(string url)
=> _errorCounters.Remove(url);
private async Task AddError(string url, List<int> ids)
private async Task<uint> AddError(string url, List<int> ids)
{
try
{
@@ -68,10 +68,13 @@ public class FeedsService : INService
// reset the error counter
ClearErrors(url);
}
return newValue;
}
catch (Exception ex)
{
Log.Error(ex, "Error adding rss errors...");
return 0;
}
}
@@ -181,12 +184,13 @@ public class FeedsService : INService
}
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}",
errorCount,
rssUrl,
$"[{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")]
public double Volume24h { get; set; }
[JsonPropertyName("volume_change_24h")]
public double VolumeChange24h { get; set; }
[JsonPropertyName("percent_change_1h")]
public double PercentChange1h { get; set; }
// [JsonPropertyName("volume_change_24h")]
// public double VolumeChange24h { get; set; }
//
// [JsonPropertyName("percent_change_1h")]
// public double PercentChange1h { get; set; }
[JsonPropertyName("percent_change_24h")]
public double PercentChange24h { get; set; }
@@ -33,12 +33,6 @@ public class CmcQuote
[JsonPropertyName("market_cap_dominance")]
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
@@ -58,9 +52,6 @@ public class CmcResponseData
[JsonPropertyName("cmc_rank")]
public int CmcRank { get; set; }
[JsonPropertyName("num_market_pairs")]
public int NumMarketPairs { get; set; }
[JsonPropertyName("circulating_supply")]
public double? CirculatingSupply { get; set; }
@@ -70,15 +61,6 @@ public class CmcResponseData
[JsonPropertyName("max_supply")]
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")]
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)
{
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)
{
if (oldPatron.Amount != newPatron.Amount)
// if pledge was increased
if (oldPatron.Amount < newPatron.Amount)
{
var conf = _config.Data;
var newAmount = (long)(newPatron.Amount * conf.PatreonCurrencyPerCent);
var newAmount = (long)(Math.Max(newPatron.Amount, oldPatron.Amount) * conf.PatreonCurrencyPerCent);
UpdateOutput<RewardedUser>[] output;
RewardedUser old;
await using (var ctx = _db.GetDbContext())
{
output = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UnqiuePlatformUserId)
.UpdateWithOutputAsync(old => new()
{
PlatformUserId = newPatron.UnqiuePlatformUserId,
UserId = newPatron.UserId,
// amount before bonuses
AmountRewardedThisMonth = newAmount,
LastReward = newPatron.PaidAt
});
}
old = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.FirstOrDefaultAsync();
// if the user wasn't previously in the db for some reason,
// we will treat him as a new patron
if (output.Length == 0)
if (old is null)
{
await OnNewPayment(newPatron);
return;
}
var oldAmount = output[0].Deleted.AmountRewardedThisMonth;
// no action as the amount is the same or lower
if (old.AmountRewardedThisMonth >= newAmount)
return;
var diff = newAmount - oldAmount;
var count = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.UpdateAsync(_ => new()
{
PlatformUserId = newPatron.UniquePlatformUserId,
UserId = newPatron.UserId,
// amount before bonuses
AmountRewardedThisMonth = newAmount,
LastReward = newPatron.PaidAt
});
// shouldn't ever happen
if (count == 0)
return;
}
var oldAmount = old.AmountRewardedThisMonth;
var realNewAmount = GetRealCurrencyReward(
(int)(newAmount / conf.PatreonCurrencyPerCent),
newAmount,
out var percentBonus);
var realOldAmount = GetRealCurrencyReward(
(int)(oldAmount / conf.PatreonCurrencyPerCent),
oldAmount,
out _);
var diff = realNewAmount - realOldAmount;
if (diff <= 0)
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,
// up to 100%
var realAmount = GetRealCurrencyReward(newPatron.Amount, diff, out var percentBonus);
await _cs.AddAsync(newPatron.UserId, realAmount, new TxData("patron","update"));
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update"));
_ = 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
if (fullPledge < 500)
if (pledgeCents < 500)
{
percentBonus = 0;
return currentAmount;
return modifiedAmount;
}
var dollarValue = fullPledge / 100;
var dollarValue = pledgeCents / 100;
percentBonus = dollarValue switch
{
> 100 => 100,
_ => dollarValue
>= 100 => 100,
>= 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.
@@ -121,7 +145,7 @@ public class CurrencyRewardService : INService, IDisposable
await ctx.GetTable<RewardedUser>()
.InsertOrUpdateAsync(() => new()
{
PlatformUserId = patron.UnqiuePlatformUserId,
PlatformUserId = patron.UniquePlatformUserId,
UserId = patron.UserId,
AmountRewardedThisMonth = amount,
LastReward = patron.PaidAt,
@@ -134,7 +158,7 @@ public class CurrencyRewardService : INService, IDisposable
},
() => new()
{
PlatformUserId = patron.UnqiuePlatformUserId
PlatformUserId = patron.UniquePlatformUserId
});
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 ctx.GetTable<RewardedUser>()
.UpdateWithOutputAsync(old => new()
.UpdateAsync(old => new()
{
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>
/// Unique id assigned to this patron by the payment platform
/// </summary>
public string UnqiuePlatformUserId { get; init; }
public string UniquePlatformUserId { get; init; }
/// <summary>
/// Discord UserId to which this <see cref="UnqiuePlatformUserId"/> is connected to
/// Discord UserId to which this <see cref="UniquePlatformUserId"/> is connected to
/// </summary>
public ulong UserId { get; init; }

View File

@@ -173,7 +173,7 @@ public sealed class PatronageService
var lastChargeUtc = subscriber.LastCharge.Value.ToUniversalTime();
var dateInOneMonth = lastChargeUtc.Date.AddMonths(1);
await using var tran = await ctx.Database.BeginTransactionAsync();
// await using var tran = await ctx.Database.BeginTransactionAsync();
try
{
var dbPatron = await ctx.GetTable<PatronUser>()
@@ -193,7 +193,7 @@ public sealed class PatronageService
ValidThru = dateInOneMonth,
});
await tran.CommitAsync();
// await tran.CommitAsync();
var newPatron = PatronUserToPatron(dbPatron);
_ = SendWelcomeMessage(newPatron);
@@ -222,35 +222,38 @@ public sealed class PatronageService
// this should never happen
if (count == 0)
{
await tran.RollbackAsync();
// await tran.RollbackAsync();
continue;
}
await tran.CommitAsync();
// await tran.CommitAsync();
await OnNewPatronPayment(PatronUserToPatron(dbPatron));
}
else if (dbPatron.AmountCents != subscriber.Cents // if user changed the amount
|| dbPatron.UserId != subscriber.UserId) // if user updated user id)
{
var cents = subscriber.Cents;
// the user updated the pledge or changed the connected discord account
var newData = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId
&& x.LastCharge < lastChargeUtc)
.UpdateWithOutputAsync(old => new()
await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
.UpdateAsync(old => new()
{
UserId = subscriber.UserId,
AmountCents = subscriber.Cents,
AmountCents = cents,
LastCharge = lastChargeUtc,
ValidThru = old.ValidThru,
});
await tran.CommitAsync();
// this should never happen
if (newData.Length == 0)
continue;
var newPatron = dbPatron.Clone();
newPatron.AmountCents = cents;
newPatron.UserId = subscriber.UserId;
await OnPatronUpdated(PatronUserToPatron(dbPatron), PatronUserToPatron(newData[0].Inserted));
// 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(newPatron));
}
}
}
@@ -262,22 +265,26 @@ public sealed class PatronageService
}
}
var expiredDate = DateTime.MinValue;
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
var output = await ctx.GetTable<PatronUser>()
var changedCount = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId
&& x.ValidThru != expiredDate)
.UpdateWithOutputAsync(old => new()
.UpdateAsync(old => new()
{
ValidThru = expiredDate
});
if (output.Length == 0)
if (changedCount == 0)
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(
PatronTier patronTier,
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
@@ -691,7 +697,7 @@ public sealed class PatronageService
return new()
{
Name = key.PrettyName,
Quota = default,
Quota = defaultValue,
IsPatronLimit = false
};
@@ -732,7 +738,7 @@ public sealed class PatronageService
private Patron PatronUserToPatron(PatronUser user)
=> new Patron()
{
UnqiuePlatformUserId = user.UniquePlatformUserId,
UniquePlatformUserId = user.UniquePlatformUserId,
UserId = user.UserId,
Amount = user.AmountCents,
Tier = CalculateTier(user),
@@ -747,6 +753,9 @@ public sealed class PatronageService
return user.AmountCents switch
{
>= 10_000 => PatronTier.C,
>= 5000 => PatronTier.L,
>= 2000 => PatronTier.XX,
>= 1000 => PatronTier.X,
>= 500 => PatronTier.V,
>= 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.*
*- Permission guide can be found here if you're not familiar with it: <https://nadekobot.readthedocs.io/en/latest/permissions-system/>*",
isInline: false)
.WithFooter($"platform id: {patron.UnqiuePlatformUserId}");
.WithFooter($"platform id: {patron.UniquePlatformUserId}");
await user.EmbedAsync(eb);
}

View File

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

View File

@@ -69,15 +69,15 @@
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<!-- 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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />

View File

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

View File

@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
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
=> "Kwoth#2452";

View File

@@ -221,7 +221,7 @@ public static class Extensions
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();
}
}

View File

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