Compare commits

...

41 Commits
4.2.0 ... 4.2.5

Author SHA1 Message Date
Kwoth
9103dd9fdb Upped version to 4.2.5 as ci didn't run for 4.2.4 2022-06-18 02:07:25 +02:00
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
Kwoth
571e1c801f Upped version to 4.2.1, updated CHANGELOG.md 2022-06-14 14:36:53 +02:00
Kwoth
f922543d33 Fixed .exexport, .savechat, and .quoteexport, closes #358 2022-06-14 14:31:43 +02:00
Kwoth
6bec67006c Fixed plaintext-only embeds. closes #360 2022-06-14 14:26:59 +02:00
Kwoth
050eaa48eb Fixed greet message footer not showing, closes #359 2022-06-14 13:57:10 +02:00
Kwoth
248ce8b3d2 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-14 13:55:26 +02:00
Kwoth
04a488cdf2 Updated .gitlab-ci.yml, to fix medusa versioning and docker builds. hopefully for the last time 2022-06-14 13:55:19 +02:00
Kwoth
6bc2fc88f9 Update responses.uk-UA.json (POEditor.com) 2022-06-14 11:34:10 +00:00
Kwoth
69b6ed6a49 Update responses.es-ES.json (POEditor.com) 2022-06-14 11:34:09 +00:00
Kwoth
e30b126726 Update responses.ru-RU.json (POEditor.com) 2022-06-14 11:34:08 +00:00
Kwoth
a5e2321c5b Update responses.pt-BR.json (POEditor.com) 2022-06-14 11:34:07 +00:00
Kwoth
322e9a329d Update responses.pl-PL.json (POEditor.com) 2022-06-14 11:34:06 +00:00
Kwoth
7ca6ab8562 Update responses.it-IT.json (POEditor.com) 2022-06-14 11:34:05 +00:00
Kwoth
8a27dcc481 Update responses.id-ID.json (POEditor.com) 2022-06-14 11:34:03 +00:00
Kwoth
bed61c521f Update responses.de-DE.json (POEditor.com) 2022-06-14 11:34:02 +00:00
Kwoth
46ea1698eb Update responses.fr-FR.json (POEditor.com) 2022-06-14 11:34:01 +00:00
Kwoth
c47417024d Update responses.nl-NL.json (POEditor.com) 2022-06-14 11:34:00 +00:00
Kwoth
eedc2d05ff Update responses.zh-TW.json (POEditor.com) 2022-06-14 11:33:59 +00:00
Kwoth
d24dba7ed0 Update responses.zh-CN.json (POEditor.com) 2022-06-14 11:33:58 +00:00
Kwoth
9bdf58ec27 Update responses.ar.json (POEditor.com) 2022-06-14 11:33:56 +00:00
47 changed files with 1096 additions and 970 deletions

View File

@@ -101,13 +101,13 @@ upload-windows-updater-release:
publish-medusa-package:
stage: publish-medusa-package
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script:
- LAST_TAG=$(git describe --tags --abbrev=0)
- if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_SHA"; fi
- cd src/Nadeko.Medusa/
- dotnet pack -c Release /p:Version=$MEDUSA_VERSION -o bin/Release/packed
- dotnet nuget push bin/Release/packed/ --source https://www.myget.org/F/nadeko/api/v2/package --api-key "$MYGET_API_KEY"
- dotnet nuget push bin/Release/packed/ --source https://www.myget.org/F/nadeko/api/v2/package --api-key "$MYGET_API_KEY"
docker-build:
# Use the official docker image.
@@ -132,6 +132,6 @@ docker-build:
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
exists:
- Dockerfile

View File

@@ -3,9 +3,42 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased
## [4.2.5] - 18.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
- Localized strings updated
### Fixed
- Fixed `.exexport`, `.savechat`, and `.quoteexport`
- Fixed plaintext-only embeds
- Fixed greet message footer not showing origin server
## [4.2.0] - 14.06.2022
@@ -122,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
@@ -131,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

@@ -1,6 +1,6 @@
using SixLabors.ImageSharp.PixelFormats;
#nullable disable warnings
using SixLabors.ImageSharp.PixelFormats;
#nullable disable
namespace NadekoBot;
public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase
@@ -37,11 +37,11 @@ public sealed record SmartEmbedText : SmartEmbedTextBase
{
}
private SmartEmbedText(IEmbed eb, string plainText = null)
private SmartEmbedText(IEmbed eb, string? plainText = null)
: base(eb)
=> (PlainText, Color) = (plainText, eb.Color?.RawValue ?? 0);
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
public static SmartEmbedText FromEmbed(IEmbed eb, string? plainText = null)
=> new(eb, plainText);
protected override EmbedBuilder GetEmbedInternal()

View File

@@ -14,7 +14,10 @@ public sealed record SmartEmbedTextArray : SmartText
if (Embeds is null)
return Array.Empty<EmbedBuilder>();
return Embeds.Map(em => em.GetEmbed());
return Embeds
.Where(x => x.IsValid)
.Select(em => em.GetEmbed())
.ToArray();
}
public void NormalizeFields()

View File

@@ -68,7 +68,7 @@ public abstract record SmartText
var obj = root.ToObject<SmartEmbedText>();
if (obj is null)
if (obj is null || !(obj.IsValid || !string.IsNullOrWhiteSpace(obj.PlainText)))
return new SmartPlainText(input);
obj.NormalizeFields();

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

@@ -259,7 +259,8 @@ public class GreetService : INService, IReadyExecutor
Description = pt.Text
};
}
else if (text is SmartEmbedText set)
if (text is SmartEmbedText set)
{
text = set with
{
@@ -605,4 +606,4 @@ public class GreetService : INService, IReadyExecutor
}
#endregion
}
}

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()
old = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.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,
// amount before bonuses
AmountRewardedThisMonth = newAmount,
LastReward = newPatron.PaidAt
});
// shouldn't ever happen
if (count == 0)
return;
}
// if the user wasn't previously in the db for some reason,
// we will treat him as a new patron
if (output.Length == 0)
{
await OnNewPayment(newPatron);
return;
}
var oldAmount = old.AmountRewardedThisMonth;
var oldAmount = output[0].Deleted.AmountRewardedThisMonth;
var diff = newAmount - oldAmount;
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()
{
UserId = subscriber.UserId,
AmountCents = subscriber.Cents,
LastCharge = lastChargeUtc,
ValidThru = old.ValidThru,
});
await tran.CommitAsync();
// this should never happen
if (newData.Length == 0)
continue;
await OnPatronUpdated(PatronUserToPatron(dbPatron), PatronUserToPatron(newData[0].Inserted));
await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
.UpdateAsync(old => new()
{
UserId = subscriber.UserId,
AmountCents = cents,
LastCharge = lastChargeUtc,
ValidThru = old.ValidThru,
});
var newPatron = dbPatron.Clone();
newPatron.AmountCents = cents;
newPatron.UserId = subscriber.UserId;
// 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;
_ = Task.Run(async () =>
{
try
{
await client.Guilds.Select(g => RescanUsers(g)).WhenAll();
}
catch
{
// ignored
}
});
_queueRunner = new QueueRunner();
}
private Task Client_GuildMemberUpdated(Cacheable<SocketGuildUser, ulong> cacheable, SocketGuildUser after)
private Task OnPresenceUpdate(SocketUser user, SocketPresence oldPresence, SocketPresence newPresence)
{
_ = 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 (oldPresence.Activities.Count != newPresence.Activities.Count)
{
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;
}
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;
@@ -232,58 +239,77 @@ public class StreamRoleService : INService
&& setting.Blacklist.All(x => x.UserId != user.Id)
&& user.RoleIds.Contains(setting.FromRoleId))
{
try
await _queueRunner.Enqueue(async () =>
{
addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
try
{
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);
Log.Warning("Stream role in server {RoleId} no longer exists. Stopping", setting.AddRoleId);
return;
Log.Warning(ex, "Error adding stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
//check if he doesn't have addrole already, to avoid errors
if (!user.RoleIds.Contains(addRole.Id))
catch (Exception ex)
{
await user.AddRoleAsync(addRole);
Log.Information("Added stream role to user {User} in {Server} server",
user.ToString(),
user.Guild.ToString());
Log.Warning(ex, "Failed adding stream role");
}
}
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
{
//check if user is in the addrole
if (user.RoleIds.Contains(setting.AddRoleId))
{
try
await _queueRunner.Enqueue(async () =>
{
addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
throw new StreamRoleNotFoundException();
try
{
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);
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)
{
await StopStreamRole(user.Guild);
Log.Warning(ex, "Error removing stream role(s). Forcibly disabling stream role feature");
throw new StreamRolePermissionException();
}
// 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)
{
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" />
<!-- 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;
if (!File.Exists(CredsExamplePath))
File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds));
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.0";
public const string BOT_VERSION = "4.2.5";
public string Author
=> "Kwoth#2452";

View File

@@ -21,7 +21,7 @@ public static class Extensions
{
SmartEmbedText set => msg.ModifyAsync(x =>
{
x.Embed = set.GetEmbed().Build();
x.Embed = set.IsValid ? set.GetEmbed().Build() : null;
x.Content = set.PlainText?.SanitizeMentions() ?? "";
}),
SmartEmbedTextArray set => msg.ModifyAsync(x =>
@@ -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

@@ -54,7 +54,7 @@ public static class MessageChannelExtensions
=> text switch
{
SmartEmbedText set => channel.SendAsync(set.PlainText,
set.GetEmbed().Build(),
set.IsValid ? set.GetEmbed().Build() : null,
sanitizeAll: sanitizeAll),
SmartPlainText st => channel.SendAsync(st.Text,
default(Embed),

View File

@@ -36,7 +36,7 @@ public static class SocketMessageComponentExtensions
=> text switch
{
SmartEmbedText set => smc.RespondAsync(set.PlainText,
set.GetEmbed().Build(),
set.IsValid ? set.GetEmbed().Build() : null,
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartPlainText st => smc.RespondAsync(st.Text,

View File

@@ -89,7 +89,7 @@ public static class StringExtensions
public static async Task<Stream> ToStream(this string str)
{
var ms = new MemoryStream();
await using var sw = new StreamWriter(ms);
var sw = new StreamWriter(ms);
await sw.WriteAsync(str);
await sw.FlushAsync();
ms.Position = 0;

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

@@ -79,10 +79,10 @@
"self_assign_success": "انت الان لديك منصب الـ{0}.",
"adsarm_enable": "",
"adsarm_disable": "",
"setrole": "",
"setrole": "نجحت أضاف وظيفة {0} ل{1}",
"set_channel_name": "لقد تجدد إسم القناة.",
"shutting_down": "اغلق",
"spam_ignore": "",
"spam_ignore": "{0} سيتجاهل هذه القناة\n",
"spam_not_ignore": "",
"spam_stats": "",
"undeafen": "",
@@ -107,7 +107,7 @@
"min_bet_limit": "",
"not_enough": "",
"raffled_user": "",
"slot_bet": "",
"slot_bet": "راهن",
"slot_jackpot": "رائع!!! تهنئه شديدة",
"slot_single": "",
"slot_three": "",
@@ -543,15 +543,6 @@
"bj_joined": "",
"no_invites": "",
"invite_deleted": "",
"deleted": "",
"insuff_perms": "",
"custom_reactions": "",
"new_cust_react": "",
"no_found": "",
"no_found_id": "",
"cleared": "",
"crr_reset": "",
"crr_set": "",
"invalid_emojis": "",
"aliases_cleared": "",
"vcrole_not_found": "",
@@ -821,13 +812,11 @@
"global_leaderboard": "",
"modified": "",
"template_reloaded": "",
"edited_cust_react": "",
"self_assign_group": "مجموعة {0}",
"started": "",
"stopped": "",
"restart_fail": "",
"restarting": "",
"edit_fail": "",
"streaming": "",
"rafflecur": "",
"rafflecur_joined": "",
@@ -976,5 +965,15 @@
"deleted_x_servers": "",
"curtr_gift": "",
"curtr_award": "",
"curtr_take": ""
"curtr_take": "",
"expr_deleted": "",
"expr_insuff_perms": "",
"expressions": "",
"expr_new": "",
"expr_no_found": "",
"expr_no_found_id": "",
"exprs_cleared": "",
"expr_reset": "",
"expr_set": "",
"expr_edited": ""
}

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.",

View File

@@ -10,7 +10,7 @@
"banned_user": "Usuario bloqueado",
"byedel_off": "La eliminación automática de los mensajes de despedida ha sido desactivada.",
"byedel_on": "Los mensajes de despedida se eliminarán luego de {0} segundos.",
"byemsg_cur": "Mensaje de despedida actual",
"byemsg_cur": "Mensaje de despedida actual: {0}",
"byemsg_enable": "Habilita los mensajes de despedida escribiendo {0}",
"byemsg_new": "Nuevo mensaje de despedida configurado.",
"bye_off": "Anuncios de despedidas desactivados.",
@@ -382,7 +382,7 @@
"warnlog_for": "Registro de advertencias para {0}",
"warnpl_none": "No hay castigos configurados.",
"warn_cleared_by": "Reiniciados por {0}.",
"warn_punish_list": "Lista de castigos de advertencia",
"warn_punish_list": "Lista de castigos activos",
"warn_punish_rem": "Tener {0} advertencias ya no ejecutará un castigo.",
"warn_punish_set": "Aplicaré el castigo {0} a los usuarios con {1} advertencias.",
"cant_apply_punishment": "No pude aplicar el castigo. Faltan permisos.",
@@ -946,34 +946,34 @@
"reminder_server_list": "Lista de recordatorios del servidor",
"imageonly_enable": "Este canal ahora es exclusivo para imágenes.",
"imageonly_disable": "Este canal ya no es exclusivo para imágenes.",
"transaction": "",
"finished_track": "",
"playing_track": "",
"queued_track": "",
"removed_track": "",
"autoplaying": "",
"music_autoplay_on": "",
"music_autoplay_off": "",
"track_moved": "",
"atl_not_enabled": "",
"channels": "",
"track_not_found": "",
"removed_track_error": "",
"market_cap_dominance": "",
"circulating_supply": "",
"module_description_expressions": "",
"deleted_x_servers": "",
"curtr_gift": "",
"curtr_award": "",
"curtr_take": "",
"expr_deleted": "",
"expr_insuff_perms": "",
"expressions": "",
"expr_new": "",
"expr_no_found": "",
"expr_no_found_id": "",
"exprs_cleared": "",
"expr_reset": "",
"expr_set": "",
"expr_edited": ""
"transaction": "Transacciones de moneda",
"finished_track": "Pista terminada",
"playing_track": "Reproduciendo pista #{0}",
"queued_track": "Pista en cola",
"removed_track": "Pista eliminada",
"autoplaying": "Añade automáticamente pistas relacionadas.",
"music_autoplay_on": "Reproducción automática activada. Pondré en cola automáticamente las pistas relacionadas después de que cada pista termine de reproducirse.",
"music_autoplay_off": "Reproducción automática desactivada.",
"track_moved": "Pista movida",
"atl_not_enabled": "La traducción automática no está activada en este canal o has proporcionado un idioma no válido.",
"channels": "Canales",
"track_not_found": "No encontré ninguna pista.",
"removed_track_error": "La pista en ese índice no existe",
"market_cap_dominance": "Dominio",
"circulating_supply": "Acciones en circulación",
"module_description_expressions": "Configura respuestas personalizadas del bot a ciertas palabras o frases",
"deleted_x_servers": "Se han eliminado {0} servidores.",
"curtr_gift": "Regalo de {0} [{1}]",
"curtr_award": "Regaladas por el dueño del bot {0} [{1}]",
"curtr_take": "Retiradas por el dueño del bot {0} [{1}]",
"expr_deleted": "Expresión eliminada",
"expr_insuff_perms": "Permisos insuficientes. Requiere ser dueño del bot para las expresiones globales, y de administrador para las expresiones del servidor.",
"expressions": "Expresiones",
"expr_new": "Nueva expresión",
"expr_no_found": "No encontré expresiones.",
"expr_no_found_id": "No existen expresiones con esa ID.",
"exprs_cleared": "Se han eliminado las {0} expresiones de este servidor.",
"expr_reset": "Las expresiones con la ID {0} ya no agregarán reacciones.",
"expr_set": "La expresión con la ID {0} añadirá las siguientes reacciones al mensaje de respuesta: {1}",
"expr_edited": "Expresión editada."
}

View File

@@ -301,7 +301,7 @@
"botid": "ID du bot",
"channelid": "{0} de ce salon est {1}.",
"channel_topic": "Thème du salon",
"commands_ran": "Commande exécutée",
"commands_ran": "Commandes exécutées",
"convert": "{0} {1} est égal à {2} {3}",
"convertlist": "Unités qui peuvent être converties par le convertisseur",
"convert_not_found": "Impossible de convertir {0} vers {1}: unités introuvables",

View File

@@ -952,13 +952,13 @@
"queued_track": "Mengantrikan trek",
"removed_track": "Trek terhapus",
"autoplaying": "automatis menambahkan trek yang terkait",
"music_autoplay_on": "",
"music_autoplay_off": "",
"track_moved": "",
"atl_not_enabled": "",
"channels": "",
"track_not_found": "",
"removed_track_error": "",
"music_autoplay_on": "Pemain musik auto dinyalakan. Saya akan automatis antre trek mirip setelah semua trek telah selesai bermain.",
"music_autoplay_off": "Pemain musik auto dimatikan.",
"track_moved": "Trek dipindah",
"atl_not_enabled": "translasi automatis tidak dibolehkan disaluran ini atau ada memberikan saya bahasa yang tidak valid.",
"channels": "Saluran",
"track_not_found": "Trek tidak ditemukan.",
"removed_track_error": "Trek dalam indeks tidak ada",
"market_cap_dominance": "",
"circulating_supply": "",
"module_description_expressions": "",

View File

@@ -5,7 +5,7 @@
"trigger": "Тригер",
"response": "Відповідь",
"fw_cleared": "Усі фільтровані слова і фільтровані слова налаштування каналу видалено.",
"aar_disabled": "\n**Авто-призначення ролі** на приєднання користувача зараз **вимкнено**.",
"aar_disabled": "\n**Автопризначення ролі** на приєднання користувача зараз **вимкнено**.",
"bandm": "Ви отримали заборону на приєднання до сервер {0}.\nПричина: {1}",
"banned_user": "Користувач заблокований",
"byedel_off": "Автоматичне видалення прощального повідомлення було вимкнено.",
@@ -28,7 +28,7 @@
"deltextchan": "Текстовий канал {0} видалено.",
"delvoich": "Голосовий канал {0} видалено.",
"fwall_start": "Я відправлятиму ПП усім власникам.",
"fwall_stop": "Я відправлятиму ПП лише першому власнику.",
"fwall_stop": "Я відправлятиму ПП лише власнику.",
"fwdm_start": "Відтепер я відправлятиму ПП.",
"fwdm_stop": "Відтепер я припиню відправляти ПП.",
"greetdel_off": "Автоматичне видалення привітального повідомлення було вимкнено.",
@@ -389,7 +389,7 @@
"shop": "Магазин",
"shop_item_add": "Предмет в магазин добавлений",
"shop_none": "Предмет в магазині на цій сторінці не знайдено.",
"shop_role": "Ви получите {0} роль.",
"shop_role": "Ви отримаєте {0} роль.",
"type": "Тип",
"gvc_disabled": "Можливість ігрового голосового каналу були вимкнена на цьому сервері.",
"gvc_enabled": "{0} тепер це Ігровий голосовоий канал.",
@@ -407,7 +407,7 @@
"shop_purchase": "Купівля на {0} сервері",
"shop_role_not_found": "Роль яка була продана більше не існує.",
"role_not_found": "Роль не знайдена: {0}",
"shop_role_purchase": "Ви успішно купили {0} роль.",
"shop_role_purchase": "Ви успішно придбали роль {0}.",
"shop_role_purchase_error": "Помилка присвоєння ролі. Вашу покупку повернуто.",
"unique_items_left": "{0} унікальних предметів осталось.",
"blocked_commands": "Заблоковані команди",
@@ -459,16 +459,16 @@
"stream_role_bl_add_fail": "Користувач {0} уже в чорному списку.",
"stream_role_bl_rem": "Користувач {0} більше не в чорному списку.",
"stream_role_bl_rem_fail": "Користувач {0} не в чорному списку.",
"stream_role_wl_add": "Користувач {0} може получити роль стрімера навіть якшо він не має ключеві слова в назві стріма.",
"stream_role_wl_add": "Користувач {0} може отримати роль стрімера навіть якщо він не має ключові слова в назві стріма.",
"stream_role_wl_add_fail": "Користувач {0} уже в білому списку.",
"stream_role_wl_rem": "Користувач {0} більше не в білому списку.",
"stream_role_wl_rem_fail": "Користувач {0} не в білому списку.",
"xp_role_reward_add_role": "Користувач який отримав рівень {0} получить роль {1}.",
"xp_role_reward_add_role": "Користувач який отримав рівень {0} отримає роль {1}.",
"xp_role_reward_remove_role": "Користувач який отримав рівень {0} втратить роль {1}.",
"cur_reward_cleared": "Отримання рівня {0} не буде більше винагороджувати {1}",
"cur_reward_added": "Користувачі що отримали рівень {0} отримають {1}.",
"level_up_rewards": "Нагороди за підвищення рівня",
"xp_receive_role": "Получив роль {0}.",
"xp_receive_role": "Отримує роль {0}.",
"xp_lose_role": "Втратив роль {0}.",
"club_create_error": "Невдача при створені клуба. Переконайтесь що ваш рівень вище 5 і Ви не учасник клубу.",
"club_created": "Клуб {0} успішно створенний!",
@@ -524,7 +524,7 @@
"change_7d_24h": "Зміна (7дн / 24год)",
"crypto_not_found": "Криптовалюта з такою назвою не знайдена.",
"did_you_mean": "Ви мали на увазі {0}?",
"self_assign_level_req": "Роль, що самостійно призначається, {0} тепер вимагає принаймні рівня сервера {1}.",
"self_assign_level_req": "Роль, що самостійно призначається, {0} тепер вимагає принаймні рівень {1} на сервері.",
"self_assign_not_level": "Для самовизначення ролі потрібен принаймні рівень сервера {0}.",
"invalid": "Недійсний / Не вдається знайти ({0})",
"mass_kill_in_progress": "Триває масове заборона та внесення у чорний список користувачів {0}...",
@@ -534,7 +534,7 @@
"reaction_roles_message": "** Ролі: ** {0}\n** Зміст: ** {1}",
"no_reaction_roles": "На цьому сервері немає функцій ReactionRole.",
"reaction_role_removed": "Повідомлення ReactionRole № {0} видалено",
"reaction_roles_full": "Ви досягли межі для повідомлень ReactionRole. Ви повинні видалити деякі.",
"reaction_roles_full": "Ви досягли межі для повідомлень ReactionRole. Деякі доведеться видалити.",
"reminder_list": "Список нагадувань",
"reminder_deleted": "Нагадування #{0} було видалене.",
"reminder_not_exist": "Нагадування з цим індексом не існує.",
@@ -549,7 +549,7 @@
"attachments": "Вкладення",
"avatar_changed": "Аватар зміненно",
"banmsg_disabled": "Сповіщення про бан вимкнено. Ви можете увімкнути його, встановивши banmsg на щось інше, ніж '-'.",
"banmsg_default": "Повідомлення про бан не встановлено. Буде використано поведінку за замовчуванням.",
"banmsg_default": "Повідомлення про бан не встановлено. Буде використано відповідь за замовчуванням.",
"banned_pl": "заблокований",
"bot_name": "Ім'я бота змінене на {0}",
"bot_status": "Статус бота змінений на {0}",
@@ -578,7 +578,7 @@
"muted_pl": "Заглушений",
"muted_sn": "Заглушений ",
"mute_role": "поточна роль-заборона {0}",
"mute_perms": "Ви не можете заборонити писати користувачу, роль якого вища вашої в ієрархії ролей, якщо ви не є власником сервера.",
"mute_perms": "Ви не можете заборонити писати користувачу, роль якого вища вашої в ієрархії ролей, або якщо ви не є власником сервера.",
"new_msg": "Нове повідомлення",
"new_nick": "Новий нік",
"new_topic": "Нова тема",
@@ -592,10 +592,10 @@
"prot_cant_use_time": "Ви не можете вказати тривалість цієї дії покарання.",
"rar_err": "Не вдалося видалити ролі. У мене недостатньо дозволів.",
"rc_perms": "Сталася помилка через неправильний колір або недостатні дозволи.",
"remrole": "Роль {0} успішно видалено з користувача {1}",
"remrole": "Роль {0} успішно видалено у користувача {1}",
"remrole_err": "Не вдалося видалити роль. У мене недостатньо дозволів.",
"renrole_err": "Не перейменувати видалити роль. У мене недостатньо дозволів.",
"renrole_perms": "Ви не можете редагувати ролі вище, ніж ваша, або моя найвища роль.",
"renrole_err": "Не вдалося перейменувати роль. У мене недостатньо дозволів.",
"renrole_perms": "Ви не можете редагувати роль вищу за вашу або мою.",
"ropl_added": "Додано.",
"ropl_disabled": "Статус обертання відтворення вимкнено.",
"ropl_enabled": "Статус обертання відтворення увімкнено.",
@@ -612,13 +612,13 @@
"text_chan_destroyed": "Текстовий канал знищений.",
"duration": "Тривалість",
"unmuted_sn": "Не заглушений",
"unable_to_dm_user": "ПП користувачу провалилось.",
"unable_to_dm_user": "Надіслано в ПП.",
"username_changed": "Ім'я користувача змінено",
"user_banned": "Користувач заблокований",
"user_joined": "Користувач приєднався",
"user_left": "Користувач покинув",
"user_role_add": "Роль користувача додана",
"user_role_rem": "Роль користувача вилучена",
"user_role_add": "Роль користувача додано",
"user_role_rem": "Роль користувача вилучено",
"user_status_change": "{0} тепер {1}",
"user_vjoined": "{0} приєднався до {1} голосового каналу.",
"user_vleft": "{0} покинув {1} голосовий канал.",
@@ -643,11 +643,11 @@
"take_fail": "не вдалося взяти {0} з {1}, оскільки у користувача немає стільки {2}!",
"commandlist_regen": "Список команд відновлено.",
"desc": "Опис",
"cant_dm": "Я не можу надіслати ПП. Переконайтеся, що Ваші ПП відкриті. Перейдіть до опцій ** (кнопка шестерні) -> Конфіденційність та безпека -> Дозволити прямі повідомлення від членів сервера **",
"cant_dm": "Я не можу надіслати ПП. Переконайтеся, що Ваші ПП відкриті. Перейдіть до опцій ** (кнопка \"налаштування\") -> Конфіденційність та безпека -> Дозволити повідомлення від членів сервера **",
"donate": "Ви можете підтримати проєкт NadekoBot на\nПатреон <{0}> або\nPaypal <{1}>\nНе забудьте залишити у повідомленні своє ім’я або ідентифікатор Discord.\n\n** Дякую ** ♥ ",
"guide": "** Список команд **: <{0}>\n** Посібники та документи щодо розміщення можна знайти тут **: <{1}>",
"list_of_modules": "Список модулів",
"autohentai_started": "Авто-хентай почався. Повторно публікуйте кожні {0} з одним із таких тегів:\n{1}",
"autohentai_started": "Авто-хентай розпочато. Повторно публікуйте кожні {0} з одним із таких тегів:\n{1}",
"tag": "Тег",
"animal_race_full": "Гонка повна! Починаєм негайно.",
"animal_race_join_bet": "{0} приєднався як {1} і зробив ставку {2}!",
@@ -804,8 +804,8 @@
"server_is_excluded": "Цей сервер виключений.",
"server_is_not_excluded": "Цей сервер не виключений.",
"level_up_channel": "Вітаємо, {0}, Ви досягли рівня {1}!",
"level_up_dm": "Вітаємо, {0}, Ви досягли рівня {1} на сервері {2}!",
"level_up_global": "Вітаємо, {0}, Ви досягли глобального рівня {1}!",
"level_up_dm": "Вітаємо, {0}, ви досягли рівня {1} на сервері {2}!",
"level_up_global": "Вітаємо, {0}, ви досягли глобального рівня {1}!",
"level_x": "Рівень {0}",
"no_level_up_rewards": "На цій сторінці немає винагороди за підвищення рівня.",
"server_leaderboard": "Таблиця ХР сервера",
@@ -855,13 +855,13 @@
"quote_id": "Цитата {0}",
"aar_none": "Коли вони приєднуються до цього сервера, користувачі не отримують жодних ролей.",
"aar_roles": "До користувачів, які приєднуються до цього сервера, застосовуються такі ролі: {0}",
"aar_role_removed": "Коли вони приєднаються до цього сервера, користувачі більше не отримуватимуть роль {0}.",
"aar_role_removed": "Коли нові учасники приєднуватимуться до цього сервера - вони більше не отримуватимуть роль {0}.",
"remove_roles_pl": "їх ролі вилучені",
"role_too_high": "Ви не можете використовувати цю команду з ролями, які вищі за вашу найвищу роль, якщо ви не адміністратор сервера.",
"role_too_high": "Ви не можете використовувати цю команду на ролях, які вищі за вашу роль, чи якщо ви не адміністратор сервера.",
"log_vc_joined": "{0} приєднався до {1} голосового каналу",
"log_vc_left": "{0} покинув {1} голосовий канал",
"protection_not_running": "Захист {0} не ввімкнено.",
"anti_alt_status": "Будь-який користувач, що приєднується до сервера з обліковим записом, не старшим за {0}, до нього застосовується така дія: {1}\nНаразі покараних: {2}",
"anti_alt_status": "Будь-який користувач, що приєднується до сервера з обліковим записом, не старшим за {0} - буде застосована дія: {1}\nНаразі покараних: {2}",
"mass_take": "Взято {0} від {1} користувачів у ролі {2}.",
"_8ball": "Магічна куля",
"repeating_none": "Плеєр зупиниться, коли буде досягнуто кінця черги.",
@@ -946,34 +946,34 @@
"reminder_server_list": "Список нагадувань серверу",
"imageonly_enable": "Цей канал доступний лише для зображень.",
"imageonly_disable": "Цей канал більше не є лише для зображень. ",
"transaction": "",
"finished_track": "",
"playing_track": "",
"queued_track": "",
"removed_track": "",
"autoplaying": "",
"music_autoplay_on": "",
"music_autoplay_off": "",
"track_moved": "",
"atl_not_enabled": "",
"channels": "",
"track_not_found": "",
"removed_track_error": "",
"market_cap_dominance": "",
"circulating_supply": "",
"module_description_expressions": "",
"deleted_x_servers": "",
"curtr_gift": "",
"curtr_award": "",
"curtr_take": "",
"expr_deleted": "",
"expr_insuff_perms": "",
"expressions": "",
"expr_new": "",
"expr_no_found": "",
"expr_no_found_id": "",
"exprs_cleared": "",
"expr_reset": "",
"expr_set": "",
"expr_edited": ""
"transaction": "Переказ валюти",
"finished_track": "Трек завершився",
"playing_track": "Відтворюється трек #{0}",
"queued_track": "Трек на черзі",
"removed_track": "Трек вилучено",
"autoplaying": "Автоматично додає відносні треки.",
"music_autoplay_on": "Музичне автовідтворення увімкнене. Я автоматично додам до черги відносні треки після того як всі треки завершать відтворення.",
"music_autoplay_off": "Музичне автовідтворення вимкнене.",
"track_moved": "Трек посунутий",
"atl_not_enabled": "Автоматичний переклад не увімкнений на цьому каналі або ви вказали не правильну мову.",
"channels": "Канали",
"track_not_found": "Трек не знайдено.",
"removed_track_error": "Трека на цьому індексі не існує",
"market_cap_dominance": "Домінування",
"circulating_supply": "Обігове постачання",
"module_description_expressions": "Налаштуйте спеціальні відповіді бота на певні слова чи фрази",
"deleted_x_servers": "Видалено {0} серверів.",
"curtr_gift": "Подарунок від {0} [{1}]",
"curtr_award": "Нагородженний власником бота {0} [{1}]",
"curtr_take": "Вилучено власником бота {0} [{1}]",
"expr_deleted": "Вираз видалено",
"expr_insuff_perms": "Недостатньо дозволів. Для глобальних виразів потрібне володіння ботом, а для серверних виразів — бути адміністратором.",
"expressions": "Вирази",
"expr_new": "Новий вираз",
"expr_no_found": "Виразів не знайдено.",
"expr_no_found_id": "Виразів з цим id не знайдено.",
"exprs_cleared": "Всі {0} вирази на цьому сервері були вилучені.",
"expr_reset": "Вираз з id {0} більше не получає реакцій.",
"expr_set": "Вираз з цим id {0} буде получати вказані реакції до відповіді: {1}",
"expr_edited": "Вираз змінено"
}

View File

@@ -543,15 +543,6 @@
"bj_joined": "",
"no_invites": "",
"invite_deleted": "",
"deleted": "",
"insuff_perms": "权限不足. 全局自定义反应需要机器人所有权,服务器自定义反应需要管理员.",
"custom_reactions": "",
"new_cust_react": "",
"no_found": "",
"no_found_id": "",
"cleared": "",
"crr_reset": "",
"crr_set": "",
"invalid_emojis": "",
"aliases_cleared": "",
"vcrole_not_found": "",
@@ -821,13 +812,11 @@
"global_leaderboard": "",
"modified": "",
"template_reloaded": "",
"edited_cust_react": "",
"self_assign_group": "",
"started": "",
"stopped": "",
"restart_fail": "",
"restarting": "",
"edit_fail": "",
"streaming": "",
"rafflecur": "",
"rafflecur_joined": "",
@@ -976,5 +965,15 @@
"deleted_x_servers": "",
"curtr_gift": "",
"curtr_award": "",
"curtr_take": ""
"curtr_take": "",
"expr_deleted": "",
"expr_insuff_perms": "",
"expressions": "",
"expr_new": "",
"expr_no_found": "",
"expr_no_found_id": "",
"exprs_cleared": "",
"expr_reset": "",
"expr_set": "",
"expr_edited": ""
}

File diff suppressed because it is too large Load Diff