Compare commits

...

26 Commits
4.2.0 ... 4.2.2

Author SHA1 Message Date
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
30 changed files with 873 additions and 832 deletions

View File

@@ -101,7 +101,7 @@ upload-windows-updater-release:
publish-medusa-package: publish-medusa-package:
stage: publish-medusa-package stage: publish-medusa-package
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script: script:
- LAST_TAG=$(git describe --tags --abbrev=0) - 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 - if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_SHA"; fi
@@ -132,6 +132,6 @@ docker-build:
- docker push "$CI_REGISTRY_IMAGE${tag}" - docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists # Run this job in a branch where a Dockerfile exists
rules: rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
exists: exists:
- Dockerfile - Dockerfile

View File

@@ -3,9 +3,28 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased ## [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 ## [4.2.0] - 14.06.2022
@@ -122,6 +141,7 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- `.rh` no longer needs quotes for multi word roles - `.rh` no longer needs quotes for multi word roles
- `.deletexp` will now properly delete server xp too - `.deletexp` will now properly delete server xp too
- Fixed `.crypto` sparklines
- [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum) - [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum)
- [dev] Fixed a bug in .gencmdlist - [dev] Fixed a bug in .gencmdlist
- [dev] small fixes to creds provider - [dev] small fixes to creds provider
@@ -131,15 +151,6 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- `.ddg` removed. - `.ddg` removed.
- [dev] removed some dead code and comments - [dev] removed some dead code and comments
### Obsolete
### Fixed
- Fixed `.crypto` sparklines
## [4.1.6] - 14.05.2022 ## [4.1.6] - 14.05.2022
### Fixed ### Fixed

View File

@@ -1,6 +1,6 @@
using SixLabors.ImageSharp.PixelFormats; #nullable disable warnings
using SixLabors.ImageSharp.PixelFormats;
#nullable disable
namespace NadekoBot; namespace NadekoBot;
public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase 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) : base(eb)
=> (PlainText, Color) = (plainText, eb.Color?.RawValue ?? 0); => (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); => new(eb, plainText);
protected override EmbedBuilder GetEmbedInternal() protected override EmbedBuilder GetEmbedInternal()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,8 +37,15 @@ public sealed class BotCredsProvider : IBotCredsProvider
public BotCredsProvider(int? totalShards = null) public BotCredsProvider(int? totalShards = null)
{ {
_totalShards = totalShards; _totalShards = totalShards;
try
{
if (!File.Exists(CredsExamplePath)) if (!File.Exists(CredsExamplePath))
File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds)); File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds));
}
catch
{
// this can fail in docker containers
}
MigrateCredentials(); MigrateCredentials();

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ public static class SocketMessageComponentExtensions
=> text switch => text switch
{ {
SmartEmbedText set => smc.RespondAsync(set.PlainText, SmartEmbedText set => smc.RespondAsync(set.PlainText,
set.GetEmbed().Build(), set.IsValid ? set.GetEmbed().Build() : null,
sanitizeAll: sanitizeAll, sanitizeAll: sanitizeAll,
ephemeral: ephemeral), ephemeral: ephemeral),
SmartPlainText st => smc.RespondAsync(st.Text, 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) public static async Task<Stream> ToStream(this string str)
{ {
var ms = new MemoryStream(); var ms = new MemoryStream();
await using var sw = new StreamWriter(ms); var sw = new StreamWriter(ms);
await sw.WriteAsync(str); await sw.WriteAsync(str);
await sw.FlushAsync(); await sw.FlushAsync();
ms.Position = 0; ms.Position = 0;

View File

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

View File

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

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

View File

@@ -301,7 +301,7 @@
"botid": "ID du bot", "botid": "ID du bot",
"channelid": "{0} de ce salon est {1}.", "channelid": "{0} de ce salon est {1}.",
"channel_topic": "Thème du salon", "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}", "convert": "{0} {1} est égal à {2} {3}",
"convertlist": "Unités qui peuvent être converties par le convertisseur", "convertlist": "Unités qui peuvent être converties par le convertisseur",
"convert_not_found": "Impossible de convertir {0} vers {1}: unités introuvables", "convert_not_found": "Impossible de convertir {0} vers {1}: unités introuvables",

View File

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

View File

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

View File

@@ -543,15 +543,6 @@
"bj_joined": "", "bj_joined": "",
"no_invites": "", "no_invites": "",
"invite_deleted": "", "invite_deleted": "",
"deleted": "",
"insuff_perms": "权限不足. 全局自定义反应需要机器人所有权,服务器自定义反应需要管理员.",
"custom_reactions": "",
"new_cust_react": "",
"no_found": "",
"no_found_id": "",
"cleared": "",
"crr_reset": "",
"crr_set": "",
"invalid_emojis": "", "invalid_emojis": "",
"aliases_cleared": "", "aliases_cleared": "",
"vcrole_not_found": "", "vcrole_not_found": "",
@@ -821,13 +812,11 @@
"global_leaderboard": "", "global_leaderboard": "",
"modified": "", "modified": "",
"template_reloaded": "", "template_reloaded": "",
"edited_cust_react": "",
"self_assign_group": "", "self_assign_group": "",
"started": "", "started": "",
"stopped": "", "stopped": "",
"restart_fail": "", "restart_fail": "",
"restarting": "", "restarting": "",
"edit_fail": "",
"streaming": "", "streaming": "",
"rafflecur": "", "rafflecur": "",
"rafflecur_joined": "", "rafflecur_joined": "",
@@ -976,5 +965,15 @@
"deleted_x_servers": "", "deleted_x_servers": "",
"curtr_gift": "", "curtr_gift": "",
"curtr_award": "", "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