mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9103dd9fdb | ||
|
1a8c9a6cba | ||
|
9d2f251923 | ||
|
3744dd287c | ||
|
f65ba100af | ||
|
cc52605c90 | ||
|
3d3dc532dc | ||
|
6c58a6a72d | ||
|
cefd81d810 | ||
|
34c96c697a | ||
|
1cc5e0e1d8 | ||
|
deaedce6c7 | ||
|
91e4d9dffc | ||
|
a826f4245f | ||
|
780eec62b3 | ||
|
dbeb83561a | ||
|
6c11d11645 | ||
|
e9923a7691 | ||
|
5fbe93d898 | ||
|
65995bdca4 | ||
|
f7c333b671 | ||
|
f9d18aa086 | ||
|
571e1c801f | ||
|
f922543d33 | ||
|
6bec67006c | ||
|
050eaa48eb | ||
|
248ce8b3d2 | ||
|
04a488cdf2 | ||
|
6bc2fc88f9 | ||
|
69b6ed6a49 | ||
|
e30b126726 | ||
|
a5e2321c5b | ||
|
322e9a329d | ||
|
7ca6ab8562 | ||
|
8a27dcc481 | ||
|
bed61c521f | ||
|
46ea1698eb | ||
|
c47417024d | ||
|
eedc2d05ff | ||
|
d24dba7ed0 | ||
|
9bdf58ec27 |
@@ -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
|
||||
|
47
CHANGELOG.md
47
CHANGELOG.md
@@ -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
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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).
|
||||
|
||||
|
@@ -92,4 +92,3 @@ nav:
|
||||
- medusa/snek-lifecycle.md
|
||||
- Contribution Guide: contribution-guide.md
|
||||
- Donate: donate.md
|
||||
- License: license.md
|
||||
|
62
src/NadekoBot/Common/QueueRunner.cs
Normal file
62
src/NadekoBot/Common/QueueRunner.cs
Normal 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);
|
||||
}
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
};
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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"));
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
|
@@ -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]
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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; }
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
// }
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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" />
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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";
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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),
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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": ""
|
||||
}
|
@@ -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.",
|
||||
|
@@ -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."
|
||||
}
|
@@ -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",
|
||||
|
@@ -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": "",
|
||||
|
@@ -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": "Вираз змінено"
|
||||
}
|
@@ -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
Reference in New Issue
Block a user