Compare commits

...

8 Commits
4.3.6 ... 4.3.7

23 changed files with 17689 additions and 376 deletions

View File

@@ -2,6 +2,22 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [4.3.7]
### Added
- Added `.exprdelserv` (.exds) to completement .exas. Deletes an expression on the current server and is susceptible to .dpo, unlike .exd
- Added `.shopreq` which lets you set role requirement for specific shop items
- Added `.shopbuy` alias to `.buy`
### Fixed
- Fixed `.convertlist` showing currencies twice (this may not apply to existing users and it may require you to manually remove all currencies from units.json)
### Removed
- Removed `Viewer` field from stream online notification as it is (almost?) always 0.
## [4.3.6] - 08.09.2022
### Added

View File

@@ -24,6 +24,7 @@ public class ShopEntry : DbEntity, IIndexed
//list
public HashSet<ShopEntryItem> Items { get; set; } = new();
public ulong? RoleRequirement { get; set; }
}
public class ShopEntryItem : DbEntity

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class shoprolereq : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<ulong>(
name: "rolerequirement",
table: "shopentry",
type: "bigint unsigned",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "rolerequirement",
table: "shopentry");
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class shoprolereq : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "rolerequirement",
table: "shopentry",
type: "numeric(20,0)",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "rolerequirement",
table: "shopentry");
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class shoprolereq : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<ulong>(
name: "RoleRequirement",
table: "ShopEntry",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RoleRequirement",
table: "ShopEntry");
}
}
}

View File

@@ -1745,6 +1745,9 @@ namespace NadekoBot.Migrations
b.Property<string>("RoleName")
.HasColumnType("TEXT");
b.Property<ulong?>("RoleRequirement")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");

View File

@@ -67,42 +67,52 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesList()
public async Task ReactionRolesList(int page = 1)
{
if (--page < 0)
return;
var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
var embed = _eb.Create(ctx)
.WithOkColor();
var content = string.Empty;
foreach (var g in reros.GroupBy(x => x.MessageId).OrderBy(x => x.Key))
await ctx.SendPaginatedConfirmAsync(page, curPage =>
{
var messageId = g.Key;
content +=
$"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
var embed = _eb.Create(ctx)
.WithOkColor();
var groupGroups = g.GroupBy(x => x.Group);
foreach (var ggs in groupGroups)
var content = string.Empty;
foreach (var g in reros.OrderBy(x => x.Group)
.Skip(curPage * 10)
.GroupBy(x => x.MessageId)
.OrderBy(x => x.Key))
{
content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
var messageId = g.Key;
content +=
$"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
foreach (var rero in ggs)
var groupGroups = g.GroupBy(x => x.Group);
foreach (var ggs in groupGroups)
{
content += $"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
if (rero.LevelReq > 0)
content += $" (lvl {rero.LevelReq}+)";
content += '\n';
content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
foreach (var rero in ggs)
{
content +=
$"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
if (rero.LevelReq > 0)
content += $" (lvl {rero.LevelReq}+)";
content += '\n';
}
}
}
}
embed.WithDescription(string.IsNullOrWhiteSpace(content)
? "There are no reaction roles on this server"
: content);
embed.WithDescription(string.IsNullOrWhiteSpace(content)
? "There are no reaction roles on this server"
: content);
await ctx.Channel.EmbedAsync(embed);
return embed;
}, reros.Count, 10);
}
[Cmd]

View File

@@ -160,15 +160,8 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
found.Response.TrimTo(1000).Replace("](", "]\\(")));
}
[Cmd]
public async Task ExprDelete(kwum id)
public async Task ExprDeleteInternalAsync(kwum id)
{
if (!AdminInGuildOrOwnerInDm())
{
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
return;
}
var ex = await _service.DeleteAsync(ctx.Guild?.Id, id);
if (ex is not null)
@@ -186,6 +179,24 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
}
}
[Cmd]
[UserPerm(GuildPerm.Administrator)]
[RequireContext(ContextType.Guild)]
public async Task ExprDeleteServer(kwum id)
=> await ExprDeleteInternalAsync(id);
[Cmd]
public async Task ExprDelete(kwum id)
{
if (!AdminInGuildOrOwnerInDm())
{
await ReplyErrorLocalizedAsync(strs.expr_insuff_perms);
return;
}
await ExprDeleteInternalAsync(id);
}
[Cmd]
public async Task ExprReact(kwum id, params string[] emojiStrs)
{

View File

@@ -38,4 +38,6 @@ public interface IShopService
/// <param name="toIndex">Destination index of the entry</param>
/// <returns>Whether swap was successful</returns>
Task<bool> MoveEntryAsync(ulong guildId, int fromIndex, int toIndex);
Task<bool> SetItemRoleRequirementAsync(ulong guildId, int index, ulong? roleId);
}

View File

@@ -98,6 +98,23 @@ public partial class Gambling
return;
}
if (entry.RoleRequirement is ulong reqRoleId)
{
var role = ctx.Guild.GetRole(reqRoleId);
if (role is null)
{
await ReplyErrorLocalizedAsync(strs.shop_item_req_role_not_found);
return;
}
var guser = (IGuildUser)ctx.User;
if (!guser.RoleIds.Contains(reqRoleId))
{
await ReplyErrorLocalizedAsync(strs.shop_item_req_role_unfulfilled(Format.Bold(role.ToString())));
return;
}
}
if (entry.Type == ShopEntryType.Role)
{
var guser = (IGuildUser)ctx.User;
@@ -412,6 +429,27 @@ public partial class Gambling
await ctx.ErrorAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopReq(int itemIndex, [Leftover] IRole role = null)
{
if (--itemIndex < 0)
return;
var succ = await _service.SetItemRoleRequirementAsync(ctx.Guild.Id, itemIndex, role?.Id);
if (!succ)
{
await ReplyErrorLocalizedAsync(strs.shop_item_not_found);
return;
}
if (role is null)
await ReplyConfirmLocalizedAsync(strs.shop_item_role_no_req(itemIndex));
else
await ReplyConfirmLocalizedAsync(strs.shop_item_role_req(itemIndex + 1, role));
}
public IEmbedBuilder EntryToEmbed(ShopEntry entry)
{
var embed = _eb.Create().WithOkColor();
@@ -443,11 +481,17 @@ public partial class Gambling
public string EntryToString(ShopEntry entry)
{
var prepend = string.Empty;
if (entry.RoleRequirement is not null)
prepend = Format.Italics(GetText(strs.shop_item_requires_role($"<@&{entry.RoleRequirement}>")))
+ Environment.NewLine;
if (entry.Type == ShopEntryType.Role)
return GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE")));
return prepend
+ GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE")));
if (entry.Type == ShopEntryType.List)
return GetText(strs.unique_items_left(entry.Items.Count)) + "\n" + entry.Name;
return "";
return prepend + GetText(strs.unique_items_left(entry.Items.Count)) + "\n" + entry.Name;
return prepend;
}
}
}

View File

@@ -94,4 +94,20 @@ public class ShopService : IShopService, INService
await uow.SaveChangesAsync();
return true;
}
public async Task<bool> SetItemRoleRequirementAsync(ulong guildId, int index, ulong? roleId)
{
await using var uow = _db.GetDbContext();
var entries = GetEntriesInternal(uow, guildId);
if (index >= entries.Count)
return false;
var entry = entries[index];
entry.RoleRequirement = roleId;
await uow.SaveChangesAsync();
return true;
}
}

View File

@@ -1,6 +1,4 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Nadeko.Common;
using NadekoBot.Common.ModuleBehaviors;
@@ -12,95 +10,6 @@ using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Searches.Services;
public sealed class StreamOnlineMessageDeleterService : INService, IReadyExecutor
{
private readonly StreamNotificationService _notifService;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IPubSub _pubSub;
public StreamOnlineMessageDeleterService(
StreamNotificationService notifService,
DbService db,
IPubSub pubSub,
DiscordSocketClient client)
{
_notifService = notifService;
_db = db;
_client = client;
_pubSub = pubSub;
}
public async Task OnReadyAsync()
{
_notifService.OnlineMessagesSent += OnOnlineMessagesSent;
if(_client.ShardId == 0)
await _pubSub.Sub(_notifService.StreamsOfflineKey, OnStreamsOffline);
}
private async Task OnOnlineMessagesSent(FollowedStream.FType type, string name, IReadOnlyCollection<(ulong, ulong)> pairs)
{
await using var ctx = _db.GetDbContext();
foreach (var (channelId, messageId) in pairs)
{
await ctx.GetTable<StreamOnlineMessage>()
.InsertAsync(() => new()
{
Name = name,
Type = type,
MessageId = messageId,
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
});
}
}
private async ValueTask OnStreamsOffline(List<StreamData> streamDatas)
{
if (_client.ShardId != 0)
return;
var pairs = await GetMessagesToDelete(streamDatas);
foreach (var (channelId, messageId) in pairs)
{
try
{
var textChannel = await _client.GetChannelAsync(channelId) as ITextChannel;
if (textChannel is null)
continue;
await textChannel.DeleteMessageAsync(messageId);
}
catch
{
continue;
}
}
}
private async Task<IEnumerable<(ulong, ulong)>> GetMessagesToDelete(List<StreamData> streamDatas)
{
await using var ctx = _db.GetDbContext();
var toReturn = new List<(ulong, ulong)>();
foreach (var sd in streamDatas)
{
var key = sd.CreateKey();
var toDelete = await ctx.GetTable<StreamOnlineMessage>()
.Where(x => (x.Type == key.Type && x.Name == key.Name)
|| Sql.DateDiff(Sql.DateParts.Day, x.DateAdded, DateTime.UtcNow) > 1)
.DeleteWithOutputAsync();
toReturn.AddRange(toDelete.Select(x => (x.ChannelId, x.MessageId)));
}
return toReturn;
}
}
public sealed class StreamNotificationService : INService, IReadyExecutor
{
private readonly DbService _db;
@@ -370,7 +279,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message);
var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream), message);
var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream, false), message);
// only cache the ids of channel/message pairs
if(_deleteOnOfflineServers.Contains(fs.GuildId))
@@ -563,18 +472,22 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
return data;
}
public IEmbedBuilder GetEmbed(ulong guildId, StreamData status)
public IEmbedBuilder GetEmbed(ulong guildId, StreamData status, bool showViewers = true)
{
var embed = _eb.Create()
.WithTitle(status.Name)
.WithUrl(status.StreamUrl)
.WithDescription(status.StreamUrl)
.AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true)
.AddField(GetText(guildId, strs.viewers),
status.Viewers == 0 && !status.IsLive
? "-"
: status.Viewers,
true);
.AddField(GetText(guildId, strs.status), status.IsLive ? "🟢 Online" : "🔴 Offline", true);
if (showViewers)
{
embed.AddField(GetText(guildId, strs.viewers),
status.Viewers == 0 && !status.IsLive
? "-"
: status.Viewers,
true);
}
if (status.IsLive)
embed = embed.WithOkColor();

View File

@@ -0,0 +1,99 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Searches.Common;
namespace NadekoBot.Modules.Searches.Services;
public sealed class StreamOnlineMessageDeleterService : INService, IReadyExecutor
{
private readonly StreamNotificationService _notifService;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IPubSub _pubSub;
public StreamOnlineMessageDeleterService(
StreamNotificationService notifService,
DbService db,
IPubSub pubSub,
DiscordSocketClient client)
{
_notifService = notifService;
_db = db;
_client = client;
_pubSub = pubSub;
}
public async Task OnReadyAsync()
{
_notifService.OnlineMessagesSent += OnOnlineMessagesSent;
if (_client.ShardId == 0)
await _pubSub.Sub(_notifService.StreamsOfflineKey, OnStreamsOffline);
}
private async Task OnOnlineMessagesSent(
FollowedStream.FType type,
string name,
IReadOnlyCollection<(ulong, ulong)> pairs)
{
await using var ctx = _db.GetDbContext();
foreach (var (channelId, messageId) in pairs)
{
await ctx.GetTable<StreamOnlineMessage>()
.InsertAsync(() => new()
{
Name = name,
Type = type,
MessageId = messageId,
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
});
}
}
private async ValueTask OnStreamsOffline(List<StreamData> streamDatas)
{
if (_client.ShardId != 0)
return;
var pairs = await GetMessagesToDelete(streamDatas);
foreach (var (channelId, messageId) in pairs)
{
try
{
var textChannel = await _client.GetChannelAsync(channelId) as ITextChannel;
if (textChannel is null)
continue;
await textChannel.DeleteMessageAsync(messageId);
}
catch
{
continue;
}
}
}
private async Task<IEnumerable<(ulong, ulong)>> GetMessagesToDelete(List<StreamData> streamDatas)
{
await using var ctx = _db.GetDbContext();
var toReturn = new List<(ulong, ulong)>();
foreach (var sd in streamDatas)
{
var key = sd.CreateKey();
var toDelete = await ctx.GetTable<StreamOnlineMessage>()
.Where(x => (x.Type == key.Type && x.Name == key.Name)
|| Sql.DateDiff(Sql.DateParts.Day, x.DateAdded, DateTime.UtcNow) > 1)
.DeleteWithOutputAsync();
toReturn.AddRange(toDelete.Select(x => (x.ChannelId, x.MessageId)));
}
return toReturn;
}
}

View File

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

View File

@@ -1003,7 +1003,10 @@ shopswap:
shopmove:
- shopmove
buy:
- shopbuy
- buy
shopreq:
- shopreq
gamevoicechannel:
- gamevoicechannel
- gvc
@@ -1309,6 +1312,10 @@ exprdelete:
- exd
- exdel
- dcr
exprdeleteserver:
- exprdelserv
- exds
- exdelserv
exprclear:
- exprclear
- exc

View File

@@ -27,7 +27,7 @@ greetdel:
- "0"
- "30"
greet:
desc: "Toggles anouncements on the current channel when someone joins the server."
desc: "Toggles announcements on the current channel when someone joins the server."
args:
- ""
greetmsg:
@@ -40,7 +40,7 @@ greetmsg:
args:
- "Welcome, %user.mention%."
bye:
desc: "Toggles anouncements on the current channel when someone leaves the server."
desc: "Toggles announcements on the current channel when someone leaves the server."
args:
- ""
byemsg:
@@ -77,7 +77,7 @@ byetest:
- ""
- "@SomeoneElse"
boost:
desc: "Toggles anouncements on the current channel when someone boosts the server."
desc: "Toggles announcements on the current channel when someone boosts the server."
args:
- ""
boostmsg:
@@ -217,13 +217,17 @@ exprlist:
- "1"
- "all"
exprshow:
desc: "Shows a expression's response on a given ID."
desc: "Shows an expression's response on a given ID."
args:
- "1"
exprdelete:
desc: "Deletes a expression on a specific index. If ran in DM, it is bot owner only and deletes a global expression. If ran in a server, it requires Administration privileges and removes server expression."
desc: "Deletes an expression on a specific index. If ran in DM, it is bot owner only and deletes a global expression. If ran in a server, it requires Administration privileges and removes server expression."
args:
- "5"
exprdeleteserver:
desc: "Deletes an expression on a specific index on this server."
args:
- "5c"
exprclear:
desc: "Deletes all expression on this server."
args:
@@ -906,7 +910,7 @@ playlists:
args:
- "1"
playlistshow:
desc: "Lists all songs in a playlist spepcified by its id. Paginated, 20 per page."
desc: "Lists all songs in a playlist specified by its id. Paginated, 20 per page."
args:
- "1"
deleteplaylist:
@@ -962,7 +966,7 @@ convertlist:
args:
- ""
wowjoke:
desc: "Get one of Kwoth's penultimate WoW jokes."
desc: "Get one of penultimate WoW jokes."
args:
- ""
calculate:
@@ -992,7 +996,7 @@ pokemonability:
args:
- "overgrow"
memelist:
desc: "Shows a list of template keys (and their repspective names) used for `{0}memegen`."
desc: "Shows a list of template keys (and their respective names) used for `{0}memegen`."
args:
- ""
memegen:
@@ -1097,7 +1101,7 @@ wiki:
args:
- "query"
color:
desc: "Shows you pictures of colors which correspond to the inputed hex values. Max 10."
desc: "Shows you pictures of colors which correspond to the inputted hex values. Max 10."
args:
- "00ff00"
- "f00 0f0 00f"
@@ -1692,7 +1696,7 @@ banprune:
args:
- "3"
wait:
desc: "Used only as a startup command. Waits a certain number of miliseconds before continuing the execution of the following startup commands."
desc: "Used only as a startup command. Waits a certain number of milliseconds before continuing the execution of the following startup commands."
args:
- "3000"
warnexpire:
@@ -1738,6 +1742,11 @@ shopremove:
desc: "Removes an item from the shop by its ID."
args:
- "1"
shopreq:
desc: "Sets a role which will be required to buy the item on the specified index. Specify only index to remove the requirement."
args:
- "2 Gamers"
- "2"
shopchangename:
desc: "Change the name of a shop entry at the specified index. Only works for non-role items"
args:

View File

@@ -725,6 +725,11 @@
"shop_role_already_bought": "You already bought this role.",
"shop_role_purchase": "You've successfully purchased {0} role.",
"shop_role_purchase_error": "Error assigning role. Your purchase has been refunded.",
"shop_item_role_req": "Shop item #{0} will now require users to have a {1} role in order to purchase it.",
"shop_item_req_role_unfulfilled": "You don't have the required role '{0}' in order to buy this shop item.",
"shop_item_req_role_not_found": "The required role for this item doesn't exist anymore. Please contact server administrator.",
"shop_item_requires_role": "Requires {0} role to purchase",
"shop_item_role_no_req": "Shop item #{0} will no longer require a role.",
"unique_items_left": "{0} unique items left.",
"blocked_commands": "Blocked Commands",
"blocked_modules": "Blocked Modules",

View File

@@ -729,223 +729,6 @@
"UnitType": "time",
"Modifier": 31536000.0
},
{
"Triggers": [
"AUD"
],
"UnitType": "currency",
"Modifier": 1.4787
},
{
"Triggers": [
"BGN"
],
"UnitType": "currency",
"Modifier": 1.9558
},
{
"Triggers": [
"BRL"
],
"UnitType": "currency",
"Modifier": 3.5991
},
{
"Triggers": [
"CAD"
],
"UnitType": "currency",
"Modifier": 1.4562
},
{
"Triggers": [
"CHF"
],
"UnitType": "currency",
"Modifier": 1.0951
},
{
"Triggers": [
"CNY"
],
"UnitType": "currency",
"Modifier": 7.4565
},
{
"Triggers": [
"CZK"
],
"UnitType": "currency",
"Modifier": 27.025
},
{
"Triggers": [
"DKK"
],
"UnitType": "currency",
"Modifier": 7.4448
},
{
"Triggers": [
"GBP"
],
"UnitType": "currency",
"Modifier": 0.8517
},
{
"Triggers": [
"HKD"
],
"UnitType": "currency",
"Modifier": 8.6631
},
{
"Triggers": [
"HRK"
],
"UnitType": "currency",
"Modifier": 7.4846
},
{
"Triggers": [
"HUF"
],
"UnitType": "currency",
"Modifier": 308.97
},
{
"Triggers": [
"IDR"
],
"UnitType": "currency",
"Modifier": 14814.35
},
{
"Triggers": [
"ILS"
],
"UnitType": "currency",
"Modifier": 4.2241
},
{
"Triggers": [
"INR"
],
"UnitType": "currency",
"Modifier": 74.8703
},
{
"Triggers": [
"JPY"
],
"UnitType": "currency",
"Modifier": 114.27
},
{
"Triggers": [
"KRW"
],
"UnitType": "currency",
"Modifier": 1244.47
},
{
"Triggers": [
"MXN"
],
"UnitType": "currency",
"Modifier": 20.7542
},
{
"Triggers": [
"MYR"
],
"UnitType": "currency",
"Modifier": 4.5205
},
{
"Triggers": [
"NOK"
],
"UnitType": "currency",
"Modifier": 9.2873
},
{
"Triggers": [
"NZD"
],
"UnitType": "currency",
"Modifier": 1.5427
},
{
"Triggers": [
"PHP"
],
"UnitType": "currency",
"Modifier": 51.797
},
{
"Triggers": [
"PLN"
],
"UnitType": "currency",
"Modifier": 4.3436
},
{
"Triggers": [
"RON"
],
"UnitType": "currency",
"Modifier": 4.4505
},
{
"Triggers": [
"RUB"
],
"UnitType": "currency",
"Modifier": 72.4564
},
{
"Triggers": [
"SEK"
],
"UnitType": "currency",
"Modifier": 9.5008
},
{
"Triggers": [
"SGD"
],
"UnitType": "currency",
"Modifier": 1.5196
},
{
"Triggers": [
"THB"
],
"UnitType": "currency",
"Modifier": 38.608
},
{
"Triggers": [
"TRY"
],
"UnitType": "currency",
"Modifier": 3.2977
},
{
"Triggers": [
"USD"
],
"UnitType": "currency",
"Modifier": 1.1168
},
{
"Triggers": [
"ZAR"
],
"UnitType": "currency",
"Modifier": 16.0537
},
{
"Triggers": [
"K",
@@ -970,19 +753,5 @@
],
"UnitType": "temperature",
"Modifier": 0.00
},
{
"Triggers": [
"EUR"
],
"UnitType": "currency",
"Modifier": 1.0
},
{
"Triggers": [
"SKK"
],
"UnitType": "currency",
"Modifier": 30.13
}
]