diff --git a/tsconfig.json b/tsconfig.json index 75cf960b..1e13115d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "strict": true, - "esModuleInterop": true + "esModuleInterop": true, + "resolveJsonModule": true } } diff --git a/util/fiatModel.ts b/util/fiatModel.ts index a722adc0..cc8d22e8 100644 --- a/util/fiatModel.ts +++ b/util/fiatModel.ts @@ -10,3 +10,7 @@ export interface IFiat { price?: boolean; locale?: string; } + +export interface IFiatCurrencies { + [key: string]: IFiat; +} diff --git a/util/index.js b/util/index.ts similarity index 71% rename from util/index.js rename to util/index.ts index 8cb13f63..2f1ab2e8 100644 --- a/util/index.js +++ b/util/index.ts @@ -1,18 +1,28 @@ -const axios = require('axios'); +import { I18nContext } from '@grammyjs/i18n'; +import { ICommunity, IOrderChannel } from '../models/community'; +import { IOrder } from '../models/order'; +import { UserDocument } from '../models/user'; +import { IFiatCurrencies, IFiat } from './fiatModel'; +import { ILanguage, ILanguages } from './languagesModel'; +import axios from 'axios'; +import fiatJson from './fiat.json'; +import languagesJson from './languages.json'; +import { Order, Community } from '../models'; +import logger from '../logger'; +import { MainContext } from '../bot/start'; const { I18n } = require('@grammyjs/i18n'); -const currencies = require('./fiat.json'); -const languages = require('./languages.json'); -const { Order, Community } = require('../models'); -const logger = require('../logger'); + +const languages: ILanguages = languagesJson; +const currencies: IFiatCurrencies = fiatJson; // ISO 4217, all ISO currency codes are 3 letters but users can trade shitcoins -const isIso4217 = code => { +const isIso4217 = (code: string): boolean => { if (code.length < 3 || code.length > 5) { return false; } const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); - code = code.toLowerCase().split(''); - return code.every(letter => { + code = code.toLowerCase() + return code.split('').every(letter => { if (alphabet.indexOf(letter) == -1) { return false; } @@ -20,15 +30,15 @@ const isIso4217 = code => { }); }; -const getCurrency = code => { - if (!isIso4217(code)) return false; +const getCurrency = (code: string): (IFiat | null) => { + if (!isIso4217(code)) return null; const currency = currencies[code]; - if (!currency) return false; + if (!currency) return null; return currency; }; -const plural = n => { +const plural = (n: number): string => { if (n === 1) { return ''; } @@ -37,7 +47,7 @@ const plural = n => { // This function formats a number to locale strings. // If Iso code or locale code doesn´t exist, the function will return a number without format. -const numberFormat = (code, number) => { +const numberFormat = (code: string, number: number) => { if (!isIso4217(code)) return false; if (!currencies[code]) return number; @@ -53,7 +63,7 @@ const numberFormat = (code, number) => { // This function checks if the current buyer and seller were doing circular operations // In order to increase their trades_completed and volume_traded. // If we found those trades in the last 24 hours we decrease both variables to both users -const handleReputationItems = async (buyer, seller, amount) => { +const handleReputationItems = async (buyer: UserDocument, seller: UserDocument, amount: number) => { try { const yesterday = new Date(Date.now() - 86400000).toISOString(); const orders = await Order.find({ @@ -64,7 +74,7 @@ const handleReputationItems = async (buyer, seller, amount) => { }); if (orders.length > 0) { let totalAmount = 0; - orders.forEach(order => { + orders.forEach((order: IOrder) => { totalAmount += order.amount; }); const lastAmount = orders[orders.length - 1].amount; @@ -118,9 +128,10 @@ const handleReputationItems = async (buyer, seller, amount) => { } }; -const getBtcFiatPrice = async (fiatCode, fiatAmount) => { +const getBtcFiatPrice = async (fiatCode: string, fiatAmount: number) => { try { const currency = getCurrency(fiatCode); + if (currency === null) throw Error("Currency not found"); if (!currency.price) return; // Before hit the endpoint we make sure the code have only 3 chars const code = currency.code.substring(0, 3); @@ -130,13 +141,13 @@ const getBtcFiatPrice = async (fiatCode, fiatAmount) => { } const sats = (fiatAmount / response.data.btc) * 100000000; - return parseInt(sats); + return Number(sats); } catch (error) { logger.error(error); } }; -const getBtcExchangePrice = (fiatAmount, satsAmount) => { +const getBtcExchangePrice = (fiatAmount: number, satsAmount: number) => { try { const satsPerBtc = 1e8; const feeRate = (satsPerBtc * fiatAmount) / satsAmount; @@ -147,8 +158,8 @@ const getBtcExchangePrice = (fiatAmount, satsAmount) => { } }; -const objectToArray = object => { - const array = []; +const objectToArray = (object: any): any[] => { + const array: any[] = []; for (const i in object) array.push(object[i]); @@ -162,7 +173,7 @@ const getCurrenciesWithPrice = () => { return withPrice; }; -const getEmojiRate = rate => { +const getEmojiRate = (rate: number): string => { const star = '⭐'; const roundedRate = Math.round(rate); const output = []; @@ -173,7 +184,7 @@ const getEmojiRate = rate => { // Round number to exp decimal digits // Source: https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Math/round#redondeo_decimal -const decimalRound = (value, exp) => { +const decimalRound = (value: number, exp: number): number => { if (typeof exp === 'undefined' || +exp === 0) { return Math.round(value); } @@ -184,27 +195,29 @@ const decimalRound = (value, exp) => { return NaN; } // Shift - value = value.toString().split('e'); - value = Math.round(+(value[0] + 'e' + (value[1] ? +value[1] - exp : -exp))); + let valueArr = value.toString().split('e'); + value = Math.round(+(valueArr[0] + 'e' + (valueArr[1] ? +valueArr[1] - exp : -exp))); // Shift back - value = value.toString().split('e'); - return +(value[0] + 'e' + (value[1] ? +value[1] + exp : exp)); + valueArr = value.toString().split('e'); + return +(valueArr[0] + 'e' + (valueArr[1] ? +valueArr[1] + exp : exp)); }; -const extractId = text => { +const extractId = (text: string): (string | null) => { const matches = text.match(/:([a-f0-9]{24}):$/); - - return matches[1]; + if (matches !== null){ + return matches?.[1]; + } + return null; }; // Clean strings that are going to be rendered with markdown -const sanitizeMD = text => { +const sanitizeMD = (text: any) => { if (!text) return ''; - return text.toString().replace(/(?=[|<>(){}[\]\-_!#.`=+])/g, '\\'); + return String(text).replace(/(?=[|<>(){}[\]\-_!#.`=+])/g, '\\'); }; -const secondsToTime = secs => { +const secondsToTime = (secs: number) => { const hours = Math.floor(secs / (60 * 60)); const divisor = secs % (60 * 60); @@ -216,9 +229,16 @@ const secondsToTime = secs => { }; }; -const isGroupAdmin = async (groupId, user, telegram) => { +const isGroupAdmin = async ( + groupId: string, + user: UserDocument, + telegram: MainContext['telegram'] +) => { try { - const member = await telegram.getChatMember(groupId, user.tg_id); + const member = await telegram.getChatMember( + groupId, + Number(user.tg_id), + ); if ( member && (member.status === 'creator' || member.status === 'administrator') @@ -242,15 +262,17 @@ const isGroupAdmin = async (groupId, user, telegram) => { logger.error(error); return { success: false, - message: error.toString(), + message: String(error), }; } }; -const deleteOrderFromChannel = async (order, telegram) => { +const deleteOrderFromChannel = async (order: IOrder, telegram: MainContext['telegram']) => { try { let channel = process.env.CHANNEL; - if (order.community_id) { + if (channel === undefined) throw Error("CHANNEL not found, please check .env file") + if (order.tg_channel_message1 === undefined) throw Error("order.tg_channel_message1 was not found in DB") + if (order.community_id !== undefined) { const community = await Community.findOne({ _id: order.community_id }); if (!community) { return channel; @@ -265,13 +287,16 @@ const deleteOrderFromChannel = async (order, telegram) => { } } } - await telegram.deleteMessage(channel, order.tg_channel_message1); + await telegram.deleteMessage( + channel, + Number(order.tg_channel_message1), + ); } catch (error) { logger.error(error); } }; -const getOrderChannel = async order => { +const getOrderChannel = async (order: IOrder) => { let channel = process.env.CHANNEL; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); @@ -281,7 +306,7 @@ const getOrderChannel = async order => { if (community.order_channels.length === 1) { channel = community.order_channels[0].name; } else { - community.order_channels.forEach(async c => { + community.order_channels.forEach(async (c: IOrderChannel) => { if (c.type === order.type) { channel = c.name; } @@ -292,10 +317,11 @@ const getOrderChannel = async order => { return channel; }; -const getDisputeChannel = async order => { +const getDisputeChannel = async (order: IOrder) => { let channel = process.env.DISPUTE_CHANNEL; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); + if (community === null) throw Error("Community was not found in DB"); channel = community.dispute_channel; } @@ -307,8 +333,13 @@ const getDisputeChannel = async order => { * @param {*} user * @returns i18n context */ -const getUserI18nContext = async user => { - const language = user.language || 'en'; +const getUserI18nContext = async (user: UserDocument) => { + let language = null; + if (!('language' in user)) { + language = 'en'; + } else { + language = user.language; + } const i18n = new I18n({ locale: language, defaultLanguageOnMissing: true, @@ -318,7 +349,7 @@ const getUserI18nContext = async user => { return i18n.createContext(user.lang); }; -const getDetailedOrder = (i18n, order, buyer, seller) => { +const getDetailedOrder = (i18n: I18nContext, order: IOrder, buyer: UserDocument, seller: UserDocument) => { try { const buyerUsername = buyer ? sanitizeMD(buyer.username) : ''; const buyerReputation = buyer @@ -336,7 +367,7 @@ const getDetailedOrder = (i18n, order, buyer, seller) => { createdAt = sanitizeMD(createdAt); takenAt = sanitizeMD(takenAt); const status = sanitizeMD(order.status); - const fee = order.fee ? parseInt(order.fee) : ''; + const fee = order.fee ? Number(order.fee) : ''; const creator = order.creator_id === buyerId ? buyerUsername : sellerUsername; const message = i18n.t('order_detail', { @@ -361,7 +392,7 @@ const getDetailedOrder = (i18n, order, buyer, seller) => { }; // We need to know if this user is a dispute solver for this community -const isDisputeSolver = (community, user) => { +const isDisputeSolver = (community: ICommunity, user: UserDocument) => { if (!community || !user) { return false; } @@ -371,19 +402,20 @@ const isDisputeSolver = (community, user) => { // Return the fee the bot will charge to the seller // this fee is a combination from the global bot fee and the community fee -const getFee = async (amount, communityId) => { - const maxFee = Math.round(amount * parseFloat(process.env.MAX_FEE)); +const getFee = async (amount: number, communityId: string) => { + const maxFee = Math.round(amount * Number(process.env.MAX_FEE)); if (!communityId) return maxFee; - const botFee = maxFee * parseFloat(process.env.FEE_PERCENT); + const botFee = maxFee * Number(process.env.FEE_PERCENT); let communityFee = Math.round(maxFee - botFee); const community = await Community.findOne({ _id: communityId }); + if (community === null) throw Error("Community was not found in DB"); communityFee = communityFee * (community.fee / 100); return botFee + communityFee; }; -const itemsFromMessage = str => { +const itemsFromMessage = (str: string) => { return str .split(' ') .map(e => e.trim()) @@ -391,17 +423,17 @@ const itemsFromMessage = str => { }; // Check if a number is int -const isInt = n => parseInt(n) === n; +const isInt = (n: number) => Number(n) === n; // Check if a number is float -const isFloat = n => typeof n === 'number' && !isInt(n); +const isFloat = (n: number) => typeof n === 'number' && !Number.isInteger(n); // Returns an emoji flag for a language -const getLanguageFlag = code => { +const getLanguageFlag = (code: string): ILanguage => { return languages[code]; }; -const delay = time => { +const delay = (time: number) => { return new Promise(resolve => setTimeout(resolve, time)); }; @@ -409,9 +441,9 @@ const delay = time => { // and the hold invoice safety window in seconds const holdInvoiceExpirationInSecs = () => { const expirationTimeInSecs = - parseInt(process.env.HOLD_INVOICE_CLTV_DELTA) * 10 * 60; + Number(process.env.HOLD_INVOICE_CLTV_DELTA) * 10 * 60; const safetyWindowInSecs = - parseInt(process.env.HOLD_INVOICE_CLTV_DELTA_SAFETY_WINDOW) * 10 * 60; + Number(process.env.HOLD_INVOICE_CLTV_DELTA_SAFETY_WINDOW) * 10 * 60; return { expirationTimeInSecs, safetyWindowInSecs, @@ -419,7 +451,7 @@ const holdInvoiceExpirationInSecs = () => { }; // Returns the user age in days -const getUserAge = user => { +const getUserAge = (user: UserDocument) => { const userCreationDate = new Date(user.created_at); const today = new Date(); const ageInDays = Math.floor( @@ -434,13 +466,13 @@ const getUserAge = user => { * @param {*} i18n context * @returns String with the remaining time to expiration in format '1 hours 30 minutes' */ -const getTimeToExpirationOrder = (order, i18n) => { +const getTimeToExpirationOrder = (order: IOrder, i18n: I18nContext) => { const initialDateObj = new Date(order.created_at); - const timeToExpire = parseInt(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW); + const timeToExpire = Number(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW); initialDateObj.setSeconds(initialDateObj.getSeconds() + timeToExpire); const currentDateObj = new Date(); - const timeDifferenceMs = initialDateObj - currentDateObj; + const timeDifferenceMs = initialDateObj.valueOf() - currentDateObj.valueOf(); const totalSecondsRemaining = Math.floor(timeDifferenceMs / 1000); const textHour = i18n.t('hours'); const textMin = i18n.t('minutes'); @@ -454,7 +486,7 @@ const getTimeToExpirationOrder = (order, i18n) => { return `${hours} ${textHour} ${minutes} ${textMin}`; }; -module.exports = { +export { isIso4217, plural, getCurrency, diff --git a/util/languagesModel.ts b/util/languagesModel.ts new file mode 100644 index 00000000..fa159815 --- /dev/null +++ b/util/languagesModel.ts @@ -0,0 +1,9 @@ +export interface ILanguage { + name: string; + emoji: string; + code: string; +} + +export interface ILanguages { + [key: string]: ILanguage; +}