mirror of
https://github.com/LBRYFoundation/lbry-wunderbot.git
synced 2025-08-23 17:47:27 +00:00
626 lines
18 KiB
JavaScript
626 lines
18 KiB
JavaScript
'use strict';
|
|
let jp = require('jsonpath');
|
|
let moment = require('moment');
|
|
let numeral = require('numeral');
|
|
let request = require('request');
|
|
let config = require('config');
|
|
let hasPriceBotChannels = require('../helpers.js').hasPriceBotChannels;
|
|
let inPrivate = require('../helpers.js').inPrivate;
|
|
let ChannelID = config.get('pricebot').mainchannel;
|
|
|
|
exports.commands = ['price'];
|
|
|
|
exports.price = {
|
|
usage: '<currency> <amount>',
|
|
description: 'displays price of lbc',
|
|
process: function(bot, msg, suffix) {
|
|
let options = {
|
|
defaultCurrency: 'BTC',
|
|
|
|
// supported currencies and api steps to arrive at the final value
|
|
currencies: {
|
|
USD: {
|
|
steps: ['LBCBTC', 'BTCUSD'],
|
|
format: '$0,0.00',
|
|
sign: 'USD '
|
|
},
|
|
GBP: {
|
|
steps: ['LBCBTC', 'BTCGBP'],
|
|
format: '£0,0.00',
|
|
sign: '£'
|
|
},
|
|
AUD: {
|
|
steps: ['LBCBTC', 'BTCAUD'],
|
|
format: '$0,0.00',
|
|
sign: 'AUD '
|
|
},
|
|
BRL: {
|
|
steps: ['LBCBTC', 'BTCBRL'],
|
|
format: 'R$0,0.00',
|
|
sign: 'R$'
|
|
},
|
|
CAD: {
|
|
steps: ['LBCBTC', 'BTCCAD'],
|
|
format: '$0,0.00',
|
|
sign: 'CAD '
|
|
},
|
|
CHF: {
|
|
steps: ['LBCBTC', 'BTCCHF'],
|
|
format: 'CHF 0,0.00',
|
|
sign: 'CHF'
|
|
},
|
|
CLP: {
|
|
steps: ['LBCBTC', 'BTCCLP'],
|
|
format: '$0,0.00',
|
|
sign: 'CLP '
|
|
},
|
|
CNY: {
|
|
steps: ['LBCBTC', 'BTCCNY'],
|
|
format: '¥0,0.00',
|
|
sign: '¥'
|
|
},
|
|
DKK: {
|
|
steps: ['LBCBTC', 'BTCDKK'],
|
|
format: 'kr 0,0.00',
|
|
sign: 'kr'
|
|
},
|
|
EUR: {
|
|
steps: ['LBCBTC', 'BTCEUR'],
|
|
format: '€0,0.00',
|
|
sign: '€'
|
|
},
|
|
HKD: {
|
|
steps: ['LBCBTC', 'BTCHKD'],
|
|
format: '$0,0.00',
|
|
sign: 'HKD '
|
|
},
|
|
INR: {
|
|
steps: ['LBCBTC', 'BTCINR'],
|
|
format: '₹0,0.00',
|
|
sign: '₹'
|
|
},
|
|
ISK: {
|
|
steps: ['LBCBTC', 'BTCISK'],
|
|
format: 'kr 0,0.00',
|
|
sign: 'kr'
|
|
},
|
|
JPY: {
|
|
steps: ['LBCBTC', 'BTCJPY'],
|
|
format: '¥0,0.00',
|
|
sign: '¥'
|
|
},
|
|
KRW: {
|
|
steps: ['LBCBTC', 'BTCKRW'],
|
|
format: '₩0,0.00',
|
|
sign: '₩'
|
|
},
|
|
NZD: {
|
|
steps: ['LBCBTC', 'BTCNZD'],
|
|
format: '$0,0.00',
|
|
sign: 'NZD '
|
|
},
|
|
RUB: {
|
|
steps: ['LBCBTC', 'BTCRUB'],
|
|
format: 'RUB 0,0.00',
|
|
sign: 'RUB'
|
|
},
|
|
SEK: {
|
|
steps: ['LBCBTC', 'BTCSEK'],
|
|
format: 'kr 0,0.00',
|
|
sign: 'kr'
|
|
},
|
|
SGD: {
|
|
steps: ['LBCBTC', 'BTCSGD'],
|
|
format: '$0,0.00',
|
|
sign: 'SGD '
|
|
},
|
|
THB: {
|
|
steps: ['LBCBTC', 'BTCTHB'],
|
|
format: '฿0,0.00',
|
|
sign: '฿'
|
|
},
|
|
TWD: {
|
|
steps: ['LBCBTC', 'BTCTWD'],
|
|
format: 'NT$0,0.00',
|
|
sign: 'NT$'
|
|
},
|
|
MYR: {
|
|
steps: ['LBCBTC', 'BTCMYR'],
|
|
format: 'RM0,0.00',
|
|
sign: 'RM'
|
|
},
|
|
IDR: {
|
|
steps: ['LBCBTC', 'BTCIDR'],
|
|
format: 'Rp0,0.00',
|
|
sign: 'Rp'
|
|
},
|
|
VND: {
|
|
steps: ['LBCBTC', 'BTCVND'],
|
|
format: '₫ 0,0.00',
|
|
sign: '₫'
|
|
},
|
|
PHP: {
|
|
steps: ['LBCBTC', 'BTCPHP'],
|
|
format: '₱ 0,0.00',
|
|
sign: '₱'
|
|
},
|
|
BND: {
|
|
steps: ['LBCBTC', 'BTCBND'],
|
|
format: 'B$ 0,0.00',
|
|
sign: 'B$'
|
|
},
|
|
SAR: {
|
|
steps: ['LBCBTC', 'BTCSAR'],
|
|
format: 'SR0,0.00',
|
|
sign: 'SR'
|
|
},
|
|
MXN: {
|
|
steps: ['LBCBTC', 'BTCMXN'],
|
|
format: 'Mex$ 0,0.00',
|
|
sign: 'Mex$'
|
|
},
|
|
TRY: {
|
|
steps: ['LBCBTC', 'BTCTRY'],
|
|
format: '₺ 0,0.00',
|
|
sign: '₺'
|
|
},
|
|
MMK: {
|
|
steps: ['LBCBTC', 'BTCMMK'],
|
|
format: 'K 0,0.00',
|
|
sign: 'K'
|
|
},
|
|
KHR: {
|
|
steps: ['LBCBTC', 'BTCKHR'],
|
|
format: '៛ 0,0.00',
|
|
sign: '៛'
|
|
},
|
|
AED: {
|
|
steps: ['LBCBTC', 'BTCAED'],
|
|
format: 'د.إ0,0.00',
|
|
sign: 'د.إ'
|
|
},
|
|
ZAR: {
|
|
steps: ['LBCBTC', 'BTCZAR'],
|
|
format: 'R 0,0.00',
|
|
sign: 'R'
|
|
},
|
|
PGK: {
|
|
steps: ['LBCBTC', 'BTCPGK'],
|
|
format: 'K 0,0.00',
|
|
sign: 'K'
|
|
},
|
|
EGP: {
|
|
steps: ['LBCBTC', 'BTCEGP'],
|
|
format: 'EGP 0,0.00',
|
|
sign: 'EGP'
|
|
},
|
|
NOK: {
|
|
steps: ['LBCBTC', 'BTCNOK'],
|
|
format: 'kr0,0.00',
|
|
sign: 'kr'
|
|
},
|
|
HUF: {
|
|
steps: ['LBCBTC', 'BTCHUF'],
|
|
format: 'ft0,0.00',
|
|
sign: 'ft'
|
|
},
|
|
ALL: {
|
|
steps: ['LBCBTC', 'BTCALL'],
|
|
format: 'L 0,0.00',
|
|
sign: 'L'
|
|
},
|
|
GEL: {
|
|
steps: ['LBCBTC', 'BTCGEL'],
|
|
format: 'GEL 0,0.00',
|
|
sign: 'GEL'
|
|
},
|
|
MDL: {
|
|
steps: ['LBCBTC', 'BTCMDL'],
|
|
format: 'lei 0,0.00',
|
|
sign: 'lei'
|
|
},
|
|
BAM: {
|
|
steps: ['LBCBTC', 'BTCBAM'],
|
|
format: 'KM 0,0.00',
|
|
sign: 'KM'
|
|
},
|
|
AZN: {
|
|
steps: ['LBCBTC', 'BTCAZN'],
|
|
format: '₼ 0,0.00',
|
|
sign: '₼'
|
|
},
|
|
AMD: {
|
|
steps: ['LBCBTC', 'BTCAMD'],
|
|
format: '֏ 0,0.00',
|
|
sign: '֏'
|
|
},
|
|
BYN: {
|
|
steps: ['LBCBTC', 'BTCBYN'],
|
|
format: 'Br 0,0.00',
|
|
sign: 'Br'
|
|
},
|
|
BTN: {
|
|
steps: ['LBCBTC', 'BTCBTN'],
|
|
format: 'Nu 0,0.00',
|
|
sign: 'Nu'
|
|
},
|
|
NPR: {
|
|
steps: ['LBCBTC', 'BTCNPR'],
|
|
format: 'रु 0,0.00',
|
|
sign: 'रु'
|
|
},
|
|
BDT: {
|
|
steps: ['LBCBTC', 'BTCBDT'],
|
|
format: '৳ 0,0.00',
|
|
sign: '৳'
|
|
},
|
|
PKR: {
|
|
steps: ['LBCBTC', 'BTCPKR'],
|
|
format: 'Rs 0,0.00',
|
|
sign: 'Rs'
|
|
},
|
|
ARS: {
|
|
steps: ['LBCBTC', 'BTCARS'],
|
|
format: '$ 0,0.00',
|
|
sign: '$'
|
|
},
|
|
PLN: {
|
|
steps: ['LBCBTC', 'BTCPLN'],
|
|
format: 'zł 0,0.00',
|
|
sign: 'zł'
|
|
},
|
|
HRK: {
|
|
steps: ['LBCBTC', 'BTCHRK'],
|
|
format: 'kn 0,0.00',
|
|
sign: 'kn'
|
|
},
|
|
BTC: {
|
|
steps: ['LBCBTC'],
|
|
format: '0,0[.][00000000] BTC',
|
|
sign: 'BTC'
|
|
}
|
|
},
|
|
|
|
// api steps
|
|
api: {
|
|
LBCBTC: {
|
|
url: 'https://bittrex.com/api/v1.1/public/getticker?market=BTC-LBC',
|
|
path: '$.result.Bid'
|
|
},
|
|
BTCUSD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.USD.buy'
|
|
},
|
|
BTCGBP: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.GBP.buy'
|
|
},
|
|
BTCAUD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.AUD.buy'
|
|
},
|
|
BTCBRL: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.BRL.buy'
|
|
},
|
|
BTCCAD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.CAD.buy'
|
|
},
|
|
BTCCHF: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.CHF.buy'
|
|
},
|
|
BTCMYR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=MYR',
|
|
path: '$.MYR'
|
|
},
|
|
BTCCLP: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.CLP.buy'
|
|
},
|
|
BTCCNY: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.CNY.buy'
|
|
},
|
|
BTCDKK: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.DKK.buy'
|
|
},
|
|
BTCEUR: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.EUR.buy'
|
|
},
|
|
BTCHKD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.HKD.buy'
|
|
},
|
|
BTCINR: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.INR.buy'
|
|
},
|
|
BTCISK: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.ISK.buy'
|
|
},
|
|
BTCJPY: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.JPY.buy'
|
|
},
|
|
BTCKRW: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.KRW.buy'
|
|
},
|
|
BTCNZD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.NZD.buy'
|
|
},
|
|
BTCRUB: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.RUB.buy'
|
|
},
|
|
BTCSEK: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.SEK.buy'
|
|
},
|
|
BTCSGD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.SGD.buy'
|
|
},
|
|
BTCTHB: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.THB.buy'
|
|
},
|
|
BTCTWD: {
|
|
url: 'https://blockchain.info/ticker',
|
|
path: '$.TWD.buy'
|
|
},
|
|
BTCBND: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=BND',
|
|
path: '$.BND'
|
|
},
|
|
BTCVND: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=VND',
|
|
path: '$.VND'
|
|
},
|
|
BTCPHP: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=PHP',
|
|
path: '$.PHP'
|
|
},
|
|
BTCSAR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=SAR',
|
|
path: '$.SAR'
|
|
},
|
|
BTCMXN: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=MXN',
|
|
path: '$.MXN'
|
|
},
|
|
BTCTRY: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=TRY',
|
|
path: '$.TRY'
|
|
},
|
|
BTCMMK: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=MMK',
|
|
path: '$.MMK'
|
|
},
|
|
BTCKHR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=KHR',
|
|
path: '$.KHR'
|
|
},
|
|
BTCAED: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=AED',
|
|
path: '$.AED'
|
|
},
|
|
BTCZAR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=ZAR',
|
|
path: '$.ZAR'
|
|
},
|
|
BTCPGK: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=PGK',
|
|
path: '$.PGK'
|
|
},
|
|
BTCNOK: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=NOK',
|
|
path: '$.NOK'
|
|
},
|
|
BTCHUF: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=HUF',
|
|
path: '$.HUF'
|
|
},
|
|
BTCALL: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=ALL',
|
|
path: '$.ALL'
|
|
},
|
|
BTCEGP: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=EGP',
|
|
path: '$.EGP'
|
|
},
|
|
BTCGEL: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=GEL',
|
|
path: '$.GEL'
|
|
},
|
|
BTCMDL: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=MDL',
|
|
path: '$.MDL'
|
|
},
|
|
BTCBAM: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=BAM',
|
|
path: '$.BAM'
|
|
},
|
|
BTCKZT: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=KZT',
|
|
path: '$.KZT'
|
|
},
|
|
BTCAZN: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=AZN',
|
|
path: '$.AZN'
|
|
},
|
|
BTCAMD: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=AMD',
|
|
path: '$.AMD'
|
|
},
|
|
BTCBYN: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=BYN',
|
|
path: '$.BYN'
|
|
},
|
|
BTCBTN: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=BTN',
|
|
path: '$.BTN'
|
|
},
|
|
BTCNPR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=NPR',
|
|
path: '$.NPR'
|
|
},
|
|
BTCBDT: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=BDT',
|
|
path: '$.BDT'
|
|
},
|
|
BTCPKR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=PKR',
|
|
path: '$.PKR'
|
|
},
|
|
BTCARS: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=ARS',
|
|
path: '$.ARS'
|
|
},
|
|
BTCPLN: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=PLN',
|
|
path: '$.PLN'
|
|
},
|
|
BTCHRK: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=HRK',
|
|
path: '$.HRK'
|
|
},
|
|
BTCIDR: {
|
|
url: 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=IDR',
|
|
path: '$.IDR'
|
|
}
|
|
},
|
|
|
|
// display date/time format
|
|
dtFormat: 'Do MMM YYYY h:mma [UTC]',
|
|
|
|
// refresh rate in milliseconds to retrieve a new price (default to 10 minutes)
|
|
refreshTime: 100000
|
|
};
|
|
let words = suffix
|
|
.trim()
|
|
.split(' ')
|
|
.filter(function(n) {
|
|
return n !== '';
|
|
});
|
|
|
|
let currency = words.length > 0 ? words[0].toUpperCase() : options.defaultCurrency;
|
|
let amount = words.length > 1 ? parseFloat(words[1], 10) : 1;
|
|
let showHelp = isNaN(amount) || Object.keys(options.currencies).indexOf(currency) === -1;
|
|
// store the last retrieved rate
|
|
let cachedRates = {};
|
|
let command = '!price';
|
|
|
|
let currencies = Object.keys(options.currencies);
|
|
for (let i = 0; i < currencies.length; i++) {
|
|
cachedRates[currencies[i]] = {
|
|
rate: 0,
|
|
time: null
|
|
};
|
|
}
|
|
if (showHelp) {
|
|
doHelp(bot, msg, suffix);
|
|
} else {
|
|
if (!hasPriceBotChannels(msg) && !inPrivate(msg)) {
|
|
msg.channel.send('Please use <#' + ChannelID + '> or DMs to talk to price bot.');
|
|
return;
|
|
}
|
|
doSteps(bot, currency, amount);
|
|
}
|
|
|
|
function doHelp(bot, msg, suffix) {
|
|
if (!hasPriceBotChannels(msg) && !inPrivate(msg)) {
|
|
msg.channel.send('Please use <#' + ChannelID + '> or DMs to talk to price bot.');
|
|
return;
|
|
}
|
|
let message = `**${command}**: show the price of 1 LBC in ${options.defaultCurrency}
|
|
**${command} help**: this message
|
|
**${command} CURRENCY**: show the price of 1 LBC in CURRENCY. Supported values for CURRENCY are Listed Below
|
|
**${command} CURRENCY AMOUNT**: show the price of AMOUNT LBC in CURRENCY
|
|
**Supported Currencies:** *usd*, *gbp*, *eur*, *aud*, *brl*, *cad*, *chf*, *clp*, *cny*, *dkk*, *hkd*, *inr*, *isk*, *jpy*, *krw*, *nzd*, *pln* ,*rub*, *sek*, *sgd*, *thb*, *twd*, *myr*, *bnd*,*vnd*, *php*, *sar*, *mxn*, *try*, *mmk*, *khr*, *aed*, *zar*, *pgk*, *egp*,*nok*, *hrk*, *huf*, *all*, *gel*, *mdl*, *bam* ,*kzt*, *azn*, *amd*, *byn*, *btn*, *npr*, *bdt*, *pkr*, *ars*, *pln*, *idr* and *btc* (case-insensitive)`;
|
|
msg.channel.send(message);
|
|
}
|
|
|
|
function formatMessage(amount, rate, option) {
|
|
let cur = option.sign;
|
|
let value = numeral(rate.rate * amount).format(option.format);
|
|
return `*${numeral(amount).format('0,0[.][00000000]')} LBC = ${cur} ${value}*
|
|
_last updated ${rate.time.utc().format(options.dtFormat)}_`;
|
|
}
|
|
|
|
function doSteps(bot, currency, amount) {
|
|
let option = options.currencies[currency];
|
|
let shouldReload = true;
|
|
if (cachedRates[currency]) {
|
|
let cache = cachedRates[currency];
|
|
shouldReload = cache.time === null || moment().diff(cache.time) >= options.refreshTime;
|
|
if (!shouldReload) {
|
|
let message = formatMessage(amount, cache, option);
|
|
msg.channel.send(message);
|
|
}
|
|
}
|
|
|
|
if (shouldReload) {
|
|
// copy the steps array
|
|
let steps = [];
|
|
for (let i = 0; i < option.steps.length; i++) {
|
|
steps.push(option.steps[i]);
|
|
}
|
|
|
|
processSteps(bot, currency, 0, amount, steps, option);
|
|
}
|
|
}
|
|
|
|
function processSteps(bot, currency, rate, amount, steps, option) {
|
|
if (steps.length > 0) {
|
|
let pairName = steps[0];
|
|
if (!options.api[pairName]) {
|
|
msg.channel.send(`There was a configuration error. ${pairName} pair was not found.`);
|
|
return;
|
|
}
|
|
|
|
let pair = options.api[pairName];
|
|
request.get(pair.url, function(error, response, body) {
|
|
if (error) {
|
|
msg.channel.send(err.message ? err.message : 'The request could not be completed at this time. Please try again later.');
|
|
return;
|
|
}
|
|
let pairRate = 0;
|
|
try {
|
|
pairRate = jp.query(JSON.parse(body), pair.path);
|
|
if (Array.isArray(pairRate) && pairRate.length > 0) {
|
|
pairRate = pairRate[0];
|
|
}
|
|
} catch (ignored) {
|
|
// invalid response or pair rate
|
|
}
|
|
|
|
if (pairRate > 0) {
|
|
rate = rate === 0 ? pairRate : rate * pairRate;
|
|
steps.shift();
|
|
if (steps.length > 0) {
|
|
processSteps(bot, currency, rate, amount, steps, option);
|
|
return;
|
|
}
|
|
|
|
// final step, cache and then response
|
|
let result = {
|
|
rate: rate,
|
|
time: moment()
|
|
};
|
|
cachedRates[currency] = result;
|
|
msg.channel.send(formatMessage(amount, result, option));
|
|
} else {
|
|
msg.channel.send(`The rate returned for the ${pairName} pair was invalid.`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|