diff --git a/bot/start.ts b/bot/start.ts index fe7a1013..25984393 100644 --- a/bot/start.ts +++ b/bot/start.ts @@ -4,18 +4,17 @@ import { Message } from 'typegram' import { UserDocument } from '../models/user' import { FilterQuery } from 'mongoose'; const OrderEvents = require('./modules/events/orders'); - -const { limit } = require('@grammyjs/ratelimiter'); +import { limit } from "@grammyjs/ratelimiter" const schedule = require('node-schedule'); -const { +import { Order, User, PendingPayment, Community, Dispute, Config, -} = require('../models'); -const { getCurrenciesWithPrice, deleteOrderFromChannel, removeAtSymbol } = require('../util'); +} from '../models'; +import { getCurrenciesWithPrice, deleteOrderFromChannel, removeAtSymbol } from '../util'; const { commandArgsMiddleware, stageMiddleware, @@ -55,7 +54,7 @@ const { validateLightningAddress, } = require('./validations'); import * as messages from './messages'; -const { +import { attemptPendingPayments, cancelOrders, deleteOrders, @@ -63,8 +62,10 @@ const { attemptCommunitiesPendingPayments, deleteCommunity, nodeInfo, -} = require('../jobs'); -const { logger } = require('../logger'); +} from '../jobs'; +import { logger } from "../logger"; +import { ICommunity, IUsernameId } from '../models/community'; + export interface MainContext extends Context { match: Array | null; i18n: I18nContext; @@ -72,7 +73,7 @@ export interface MainContext extends Context { admin: UserDocument; } -interface OrderQuery { +export interface OrderQuery { status?: string; buyer_id?: string; seller_id?: string; @@ -80,7 +81,7 @@ interface OrderQuery { const askForConfirmation = async (user: UserDocument, command: string) => { try { - let orders = []; + let orders: any[] = []; if (command === '/cancel') { const where: FilterQuery = { $and: [ @@ -133,6 +134,7 @@ const askForConfirmation = async (user: UserDocument, command: string) => { return orders; } catch (error) { logger.error(error); + return null; } }; @@ -145,7 +147,7 @@ has the same condition. The problem mentioned above is similar to this issue: https://github.com/telegraf/telegraf/issues/1319#issuecomment-766360594 */ -const ctxUpdateAssertMsg = "ctx.update.message.text is not available."; +export const ctxUpdateAssertMsg = "ctx.update.message.text is not available."; const initialize = (botToken: string, options: Partial>): Telegraf => { const i18n = new I18n({ @@ -215,7 +217,7 @@ const initialize = (botToken: string, options: Partial'); if (!val) return; let config = await Config.findOne(); - if (!config) { + if (config === null) { config = new Config(); } config.maintenance = false; @@ -262,11 +264,11 @@ const initialize = (botToken: string, options: Partial el); + const [command, orderId] = params.filter((el: string) => el); if (!orderId) { const orders = await askForConfirmation(ctx.user, command); - if (!orders.length) return await ctx.reply(`${command} `); + if (orders === null || orders.length === 0) return await ctx.reply(`${command} `); return await messages.showConfirmationButtons(ctx, orders, command); } else if (!(await validateObjectId(ctx, orderId))) { @@ -325,7 +327,7 @@ const initialize = (botToken: string, options: Partial el); + const [command, orderId] = params.filter((el: string) => el); if (!orderId) { const orders = await askForConfirmation(ctx.user, command); - if (!orders.length) return await ctx.reply(`${command} `); + if (orders === null || orders.length === 0) return await ctx.reply(`${command} `); return await messages.showConfirmationButtons(ctx, orders, command); } else if (!(await validateObjectId(ctx, orderId))) { @@ -445,7 +448,7 @@ const initialize = (botToken: string, options: Partial el); + const [command, orderId] = params.filter((el: string) => el); if (!orderId) { const orders = await askForConfirmation(ctx.user, command); - if (!orders.length) return await ctx.reply(`${command} `); + if (orders === null || orders.length === 0) return await ctx.reply(`${command} `); return await messages.showConfirmationButtons(ctx, orders, command); } else if (!(await validateObjectId(ctx, orderId))) { @@ -637,7 +641,7 @@ const initialize = (botToken: string, options: Partial el.id !== user.id + if (community === null) throw Error("Community was not found in DB"); + community.banned_users = community.banned_users.toObject().filter( + (el: IUsernameId) => el.id !== user.id ); await community.save(); } else { @@ -739,7 +745,7 @@ const initialize = (botToken: string, options: Partial { try { const config = await Config.findOne({}); + if (config === null) throw Error("Config was not found in DB"); await messages.showInfoMessage(ctx, ctx.user, config); } catch (error) { logger.error(error); diff --git a/bot/validations.js b/bot/validations.ts similarity index 76% rename from bot/validations.js rename to bot/validations.ts index c9aab38c..28a0a55c 100644 --- a/bot/validations.js +++ b/bot/validations.ts @@ -1,19 +1,33 @@ +import { MainContext, OrderQuery, ctxUpdateAssertMsg } from "./start"; +import { ICommunity } from "../models/community"; +import { FilterQuery } from "mongoose"; +import { UserDocument } from "../models/user"; +import { IOrder } from "../models/order"; +import { Telegraf } from "telegraf"; + const { parsePaymentRequest } = require('invoices'); const { ObjectId } = require('mongoose').Types; -const messages = require('./messages'); -const { Order, User, Community } = require('../models'); -const { isIso4217, isDisputeSolver } = require('../util'); +import * as messages from './messages'; +import { Order, User, Community } from '../models'; +import { isIso4217, isDisputeSolver } from '../util'; const { existLightningAddress } = require('../lnurl/lnurl-pay'); -const { logger } = require('../logger'); -const { removeLightningPrefix } = require('../util/valitationUtils'); +import { logger } from '../logger'; +import { removeLightningPrefix } from '../util/valitationUtils'; // We look in database if the telegram user exists, // if not, it creates a new user -const validateUser = async (ctx, start) => { +const validateUser = async (ctx: MainContext, start: boolean) => { try { - const tgUser = ctx.update.callback_query - ? ctx.update.callback_query.from - : ctx.update.message.from; + let tgUser = null; + if (("callback_query" in ctx.update) && ctx.update.callback_query) { + tgUser = ctx.update.callback_query.from; + } + else if (("message" in ctx.update) && ctx.update.message) { + tgUser = ctx.update.message.from; + } + else { + throw new Error(ctxUpdateAssertMsg); + } // We need to make sure the user has a username if (!tgUser.username) { await ctx.telegram.sendMessage(tgUser.id, ctx.i18n.t('non_handle_error')); @@ -48,15 +62,18 @@ const validateUser = async (ctx, start) => { } }; -const validateSuperAdmin = async (ctx, id) => { +const validateSuperAdmin = async (ctx: MainContext, id?: string) => { try { + if (!('message' in ctx.update) || !('text' in ctx.update.message)) { + throw new Error(ctxUpdateAssertMsg); + } const tgUserId = id || ctx.update.message.from.id; const user = await User.findOne({ tg_id: tgUserId }); // If the user never started the bot we can't send messages // to that user, so we do nothing - if (!user) return; + if (user === null) return; - if (!user.admin) return await messages.notAuthorized(ctx, tgUserId); + if (!user.admin) return await messages.notAuthorized(ctx, tgUserId.toString()); return user; } catch (error) { @@ -65,22 +82,26 @@ const validateSuperAdmin = async (ctx, id) => { } }; -const validateAdmin = async (ctx, id) => { +const validateAdmin = async (ctx: MainContext, id?: string) => { try { + if (!('message' in ctx.update) || !('text' in ctx.update.message)) { + throw new Error(ctxUpdateAssertMsg); + } const tgUserId = id || ctx.update.message.from.id; const user = await User.findOne({ tg_id: tgUserId }); // If the user never started the bot we can't send messages // to that user, so we do nothing - if (!user) return; + if (user === null) return; let community = null; if (user.default_community_id) community = await Community.findOne({ _id: user.default_community_id }); + if (community === null) throw Error("Community was not found in DB"); const isSolver = isDisputeSolver(community, user); if (!user.admin && !isSolver) - return await messages.notAuthorized(ctx, tgUserId); + return await messages.notAuthorized(ctx, tgUserId.toString()); return user; } catch (error) { @@ -89,7 +110,7 @@ const validateAdmin = async (ctx, id) => { } }; -const processParameters = args => { +const processParameters = (args: string[]) => { const correctedArgs = []; let isGrouping = false; let groupedString = ''; @@ -121,7 +142,7 @@ const processParameters = args => { return correctedArgs; }; -const validateSellOrder = async ctx => { +const validateSellOrder = async (ctx: MainContext) => { try { let args = ctx.state.command.args; if (args.length < 4) { @@ -166,11 +187,11 @@ const validateSellOrder = async ctx => { return false; } - if (amount !== 0 && amount < process.env.MIN_PAYMENT_AMT) { + if (amount !== 0 && amount < Number(process.env.MIN_PAYMENT_AMT)) { await messages.mustBeGreatherEqThan( ctx, 'monto_en_sats', - process.env.MIN_PAYMENT_AMT + Number(process.env.MIN_PAYMENT_AMT) ); return false; } @@ -185,7 +206,7 @@ const validateSellOrder = async ctx => { return false; } - if (fiatAmount.some(x => x < 1)) { + if (fiatAmount.some((x: number) => x < 1)) { await messages.mustBeGreatherEqThan(ctx, 'monto_en_fiat', 1); return false; } @@ -210,7 +231,7 @@ const validateSellOrder = async ctx => { } }; -const validateBuyOrder = async ctx => { +const validateBuyOrder = async (ctx: MainContext) => { try { let args = ctx.state.command.args; if (args.length < 4) { @@ -254,11 +275,11 @@ const validateBuyOrder = async ctx => { return false; } - if (amount !== 0 && amount < process.env.MIN_PAYMENT_AMT) { + if (amount !== 0 && amount < Number(process.env.MIN_PAYMENT_AMT)) { await messages.mustBeGreatherEqThan( ctx, 'monto_en_sats', - process.env.MIN_PAYMENT_AMT + Number(process.env.MIN_PAYMENT_AMT) ); return false; } @@ -273,7 +294,7 @@ const validateBuyOrder = async ctx => { return false; } - if (fiatAmount.some(x => x < 1)) { + if (fiatAmount.some((x: number) => x < 1)) { await messages.mustBeGreatherEqThan(ctx, 'monto_en_fiat', 1); return false; } @@ -297,24 +318,23 @@ const validateBuyOrder = async ctx => { return false; } }; -const validateLightningAddress = async lightningAddress => { +const validateLightningAddress = async (lightningAddress: string) => { const pattern = /^[\w-.]+@(?:[\w-]+(? { +const validateInvoice = async (ctx: MainContext, lnInvoice: string) => { try { // ISSUE: 542 - const checkedPrefixlnInvoice = removeLightningPrefix(lnInvoice); const invoice = parsePaymentRequest({ request: checkedPrefixlnInvoice }); - const latestDate = new Date( - Date.now() + parseInt(process.env.INVOICE_EXPIRATION_WINDOW) - ).toISOString(); - if (!!invoice.tokens && invoice.tokens < process.env.MIN_PAYMENT_AMT) { + Date.now() + Number(process.env.INVOICE_EXPIRATION_WINDOW) + ); + if (!("MAIN_PAYMENT_AMT" in process.env)) throw Error("MIN_PAYMENT_AMT not found, please check .env file"); + if (!!invoice.tokens && invoice.tokens < Number(process.env.MIN_PAYMENT_AMT)) { await messages.minimunAmountInvoiceMessage(ctx); return false; } @@ -347,24 +367,22 @@ const validateInvoice = async (ctx, lnInvoice) => { } }; -const isValidInvoice = async (ctx, lnInvoice) => { +const isValidInvoice = async (ctx: MainContext, lnInvoice: string) => { try { // ISSUE: 542 - const checkedPrefixlnInvoice = removeLightningPrefix(lnInvoice); const invoice = parsePaymentRequest({ request: checkedPrefixlnInvoice }); - const latestDate = new Date( - Date.now() + parseInt(process.env.INVOICE_EXPIRATION_WINDOW) + Date.now() + Number(process.env.INVOICE_EXPIRATION_WINDOW) ).toISOString(); - if (!!invoice.tokens && invoice.tokens < process.env.MIN_PAYMENT_AMT) { + if (!!invoice.tokens && invoice.tokens < Number(process.env.MIN_PAYMENT_AMT)) { await messages.invoiceMustBeLargerMessage(ctx); return { success: false, }; } - if (new Date(invoice.expires_at) < latestDate) { + if (new Date(invoice.expires_at).toISOString() < latestDate) { await messages.invoiceExpiryTooShortMessage(ctx); return { success: false, @@ -404,7 +422,7 @@ const isValidInvoice = async (ctx, lnInvoice) => { } }; -const isOrderCreator = (user, order) => { +const isOrderCreator = (user: UserDocument, order: IOrder) => { try { return user._id == order.creator_id; } catch (error) { @@ -413,7 +431,7 @@ const isOrderCreator = (user, order) => { } }; -const validateTakeSellOrder = async (ctx, bot, user, order) => { +const validateTakeSellOrder = async (ctx: MainContext, bot: Telegraf, user: UserDocument, order: IOrder) => { try { if (!order) { await messages.invalidOrderMessage(ctx, bot, user); @@ -442,10 +460,10 @@ const validateTakeSellOrder = async (ctx, bot, user, order) => { } }; -const validateTakeBuyOrder = async (ctx, bot, user, order) => { +const validateTakeBuyOrder = async (ctx: MainContext, bot: Telegraf, user: UserDocument, order: IOrder) => { try { if (!order) { - await messages.invalidOrderMessage(bot, user); + await messages.invalidOrderMessage(ctx, bot, user); return false; } if (isOrderCreator(user, order) && process.env.NODE_ENV === 'production') { @@ -467,9 +485,9 @@ const validateTakeBuyOrder = async (ctx, bot, user, order) => { } }; -const validateReleaseOrder = async (ctx, user, orderId) => { +const validateReleaseOrder = async (ctx: MainContext, user: UserDocument, orderId: string) => { try { - let where = { + let where: FilterQuery = { seller_id: user._id, status: 'WAITING_BUYER_INVOICE', _id: orderId, @@ -498,7 +516,7 @@ const validateReleaseOrder = async (ctx, user, orderId) => { } order = await Order.findOne(where); - if (!order) { + if (order === null) { await messages.notActiveOrderMessage(ctx); return false; } @@ -510,7 +528,7 @@ const validateReleaseOrder = async (ctx, user, orderId) => { } }; -const validateDisputeOrder = async (ctx, user, orderId) => { +const validateDisputeOrder = async (ctx: MainContext, user: UserDocument, orderId: string) => { try { const where = { $and: [ @@ -522,7 +540,7 @@ const validateDisputeOrder = async (ctx, user, orderId) => { const order = await Order.findOne(where); - if (!order) { + if (order === null) { await messages.notActiveOrderMessage(ctx); return false; } @@ -534,9 +552,9 @@ const validateDisputeOrder = async (ctx, user, orderId) => { } }; -const validateFiatSentOrder = async (ctx, user, orderId) => { +const validateFiatSentOrder = async (ctx: MainContext, user: UserDocument, orderId: string) => { try { - const where = { + const where: FilterQuery = { $and: [ { buyer_id: user._id }, { $or: [{ status: 'ACTIVE' }, { status: 'PAID_HOLD_INVOICE' }] }, @@ -547,7 +565,7 @@ const validateFiatSentOrder = async (ctx, user, orderId) => { where._id = orderId; } const order = await Order.findOne(where); - if (!order) { + if (order === null) { await messages.notActiveOrderMessage(ctx); return false; } @@ -570,7 +588,7 @@ const validateFiatSentOrder = async (ctx, user, orderId) => { }; // If a seller have an order with status FIAT_SENT, return false -const validateSeller = async (ctx, user) => { +const validateSeller = async (ctx: MainContext, user: UserDocument) => { try { const where = { seller_id: user._id, @@ -591,10 +609,13 @@ const validateSeller = async (ctx, user) => { } }; -const validateParams = async (ctx, paramNumber, errOutputString) => { +const validateParams = async (ctx: MainContext, paramNumber: number, errOutputString: string): Promise> => { try { + if (!('message' in ctx.update) || !('text' in ctx.update.message)) { + throw new Error(ctxUpdateAssertMsg); + } const paramsArray = ctx.update.message.text.split(' '); - const params = paramsArray.filter(el => el !== ''); + const params = paramsArray.filter((el: string) => el !== ''); if (params.length !== paramNumber) { await messages.customMessage( ctx, @@ -607,11 +628,11 @@ const validateParams = async (ctx, paramNumber, errOutputString) => { return params.slice(1); } catch (error) { logger.error(error); - return false; + return null; } }; -const validateObjectId = async (ctx, id) => { +const validateObjectId = async (ctx: MainContext, id: string) => { try { if (!ObjectId.isValid(id)) { await messages.notValidIdMessage(ctx); @@ -625,10 +646,10 @@ const validateObjectId = async (ctx, id) => { } }; -const validateUserWaitingOrder = async (ctx, bot, user) => { +const validateUserWaitingOrder = async (ctx: MainContext, bot: Telegraf, user: UserDocument) => { try { // If is a seller - let where = { + let where: FilterQuery = { seller_id: user._id, status: 'WAITING_PAYMENT', }; @@ -655,12 +676,12 @@ const validateUserWaitingOrder = async (ctx, bot, user) => { }; // We check if the user is banned from the community in the order -const isBannedFromCommunity = async (user, communityId) => { +const isBannedFromCommunity = async (user: UserDocument, communityId: string) => { try { if (!communityId) return false; const community = await Community.findOne({ _id: communityId }); if (!community) return false; - return community.banned_users.some(buser => buser.id == user._id); + return community.banned_users.toObject().some((buser: ICommunity) => buser.id == user._id); } catch (error) { logger.error(error); return false; diff --git a/jobs/calculate_community_earnings.js b/jobs/calculate_community_earnings.ts similarity index 77% rename from jobs/calculate_community_earnings.js rename to jobs/calculate_community_earnings.ts index 87eb3e28..3ccc7b39 100644 --- a/jobs/calculate_community_earnings.js +++ b/jobs/calculate_community_earnings.ts @@ -1,5 +1,5 @@ -const { Order, Community } = require('../models'); -const { logger } = require('../logger'); +import { Order, Community } from '../models'; +import { logger } from "../logger"; const calculateEarnings = async () => { try { @@ -12,9 +12,9 @@ const calculateEarnings = async () => { for (const order of orders) { const amount = order.amount; const fee = order.fee; - const botFee = order.bot_fee || parseFloat(process.env.MAX_FEE); + const botFee = order.bot_fee || Number(process.env.MAX_FEE); const communityFeePercent = - order.community_fee || parseFloat(process.env.FEE_PERCENT); + order.community_fee || Number(process.env.FEE_PERCENT); const maxFee = amount * botFee; const communityFee = fee - maxFee * communityFeePercent; const earnings = earningsMap.get(order.community_id) || [0, 0]; @@ -27,6 +27,7 @@ const calculateEarnings = async () => { } for (const [communityId, earnings] of earningsMap) { const community = await Community.findById(communityId); + if (community === null) throw Error("Community was not found in DB"); const amount = Math.round(earnings[0]); community.earnings = community.earnings + amount; community.orders_to_redeem = community.orders_to_redeem + earnings[1]; @@ -36,9 +37,9 @@ const calculateEarnings = async () => { ); } } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`calculateEarnings catch error: ${message}`); } }; -module.exports = calculateEarnings; +export default calculateEarnings; diff --git a/jobs/cancel_orders.js b/jobs/cancel_orders.ts similarity index 86% rename from jobs/cancel_orders.js rename to jobs/cancel_orders.ts index 4cce5064..686b3dec 100644 --- a/jobs/cancel_orders.js +++ b/jobs/cancel_orders.ts @@ -1,16 +1,18 @@ -const { User, Order } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; +import { User, Order } from "../models"; const { cancelShowHoldInvoice, cancelAddInvoice } = require('../bot/commands'); -const messages = require('../bot/messages'); -const { getUserI18nContext, holdInvoiceExpirationInSecs } = require('../util'); -const { logger } = require('../logger'); +import * as messages from "../bot/messages"; +import { getUserI18nContext, holdInvoiceExpirationInSecs } from '../util'; +import { logger } from "../logger"; const OrderEvents = require('../bot/modules/events/orders'); -const cancelOrders = async bot => { +const cancelOrders = async (bot: Telegraf) => { try { const holdInvoiceTime = new Date(); holdInvoiceTime.setSeconds( holdInvoiceTime.getSeconds() - - parseInt(process.env.HOLD_INVOICE_EXPIRATION_WINDOW) + Number(process.env.HOLD_INVOICE_EXPIRATION_WINDOW) ); // We get the orders where the seller didn't pay the hold invoice before expired // or where the buyer didn't add the invoice @@ -54,6 +56,7 @@ const cancelOrders = async bot => { for (const order of activeOrders) { const buyerUser = await User.findOne({ _id: order.buyer_id }); const sellerUser = await User.findOne({ _id: order.seller_id }); + if (buyerUser === null || sellerUser === null) return; const i18nCtxBuyer = await getUserI18nContext(buyerUser); const i18nCtxSeller = await getUserI18nContext(sellerUser); // Instead of cancel this order we should send this to the admins @@ -79,7 +82,7 @@ const cancelOrders = async bot => { // Now we cancel orders expired // ============================== orderTime = new Date(); - let orderExpirationTime = parseInt( + let orderExpirationTime = Number( process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW ); orderExpirationTime = orderExpirationTime + orderExpirationTime * 0.2; @@ -106,4 +109,4 @@ const cancelOrders = async bot => { } }; -module.exports = cancelOrders; +export default cancelOrders; diff --git a/jobs/communities.js b/jobs/communities.ts similarity index 70% rename from jobs/communities.js rename to jobs/communities.ts index 4d28409e..a3599506 100644 --- a/jobs/communities.js +++ b/jobs/communities.ts @@ -1,12 +1,15 @@ -const { Order, Community } = require('../models'); -const { logger } = require('../logger'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; -const deleteCommunity = async bot => { +import { Order, Community } from '../models'; +import { logger } from "../logger"; + +const deleteCommunity = async (bot: Telegraf) => { try { const communities = await Community.find(); for (const community of communities) { // Delete communities with COMMUNITY_TTL days without a successful order - const days = 86400 * parseInt(process.env.COMMUNITY_TTL); + const days = 86400 * Number(process.env.COMMUNITY_TTL); const time = new Date(); time.setSeconds(time.getSeconds() - days); // If is a new community we don't do anything @@ -26,9 +29,9 @@ const deleteCommunity = async bot => { } } } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`deleteCommunity catch error: ${message}`); } }; -module.exports = deleteCommunity; +export default deleteCommunity; diff --git a/jobs/delete_published_orders.js b/jobs/delete_published_orders.ts similarity index 73% rename from jobs/delete_published_orders.js rename to jobs/delete_published_orders.ts index bec2af2f..4182840d 100644 --- a/jobs/delete_published_orders.js +++ b/jobs/delete_published_orders.ts @@ -1,13 +1,16 @@ -const { Order } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; + +import { Order } from '../models'; const { deleteOrderFromChannel } = require('../util'); -const { logger } = require('../logger'); +import { logger } from '../logger'; -const deleteOrders = async bot => { +const deleteOrders = async (bot: Telegraf) => { try { const windowTime = new Date(); windowTime.setSeconds( windowTime.getSeconds() - - parseInt(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW) + Number(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW) ); // We get the pending orders where time is expired const pendingOrders = await Order.find({ @@ -25,9 +28,9 @@ const deleteOrders = async bot => { await deleteOrderFromChannel(orderCloned, bot.telegram); } } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`deleteOrders catch error: ${message}`); } }; -module.exports = deleteOrders; +export default deleteOrders; diff --git a/jobs/index.js b/jobs/index.js deleted file mode 100644 index b784f178..00000000 --- a/jobs/index.js +++ /dev/null @@ -1,19 +0,0 @@ -const { - attemptPendingPayments, - attemptCommunitiesPendingPayments, -} = require('./pending_payments'); -const cancelOrders = require('./cancel_orders'); -const deleteOrders = require('./delete_published_orders'); -const calculateEarnings = require('./calculate_community_earnings'); -const deleteCommunity = require('./communities'); -const nodeInfo = require('./node_info'); - -module.exports = { - attemptPendingPayments, - cancelOrders, - deleteOrders, - calculateEarnings, - attemptCommunitiesPendingPayments, - deleteCommunity, - nodeInfo, -}; diff --git a/jobs/index.ts b/jobs/index.ts new file mode 100644 index 00000000..281f16f6 --- /dev/null +++ b/jobs/index.ts @@ -0,0 +1,19 @@ +import { + attemptPendingPayments, + attemptCommunitiesPendingPayments, +} from "./pending_payments"; +import cancelOrders from "./cancel_orders"; +import deleteOrders from "./delete_published_orders"; +import calculateEarnings from './calculate_community_earnings' +import deleteCommunity from './communities' +import nodeInfo from './node_info' + +export { + attemptPendingPayments, + cancelOrders, + deleteOrders, + calculateEarnings, + attemptCommunitiesPendingPayments, + deleteCommunity, + nodeInfo, +}; diff --git a/jobs/node_info.js b/jobs/node_info.ts similarity index 62% rename from jobs/node_info.js rename to jobs/node_info.ts index 94c994d0..22e82eb3 100644 --- a/jobs/node_info.js +++ b/jobs/node_info.ts @@ -1,11 +1,14 @@ -const { Config } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; + +import { Config } from '../models'; const { getInfo } = require('../ln'); const { logger } = require('../logger'); -const info = async bot => { +const info = async (bot: Telegraf) => { try { let config = await Config.findOne({}); - if (!config) { + if (config === null) { config = new Config(); } const info = await getInfo(); @@ -15,9 +18,9 @@ const info = async bot => { config.node_uri = info.uris[0]; await config.save(); } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`node info catch error: ${message}`); } }; -module.exports = info; +export default info; diff --git a/jobs/pending_payments.ts b/jobs/pending_payments.ts index 90bdfff7..b3b9a091 100644 --- a/jobs/pending_payments.ts +++ b/jobs/pending_payments.ts @@ -1,14 +1,14 @@ -const { payRequest, isPendingPayment } = require('../ln'); -const { PendingPayment, Order, User, Community } = require('../models'); +import { PendingPayment, Order, User, Community } from '../models'; import * as messages from '../bot/messages'; -const { getUserI18nContext } = require('../util'); -const { logger } = require('../logger'); +import { logger } from "../logger"; import { Telegraf } from 'telegraf'; import { I18nContext } from '@grammyjs/i18n'; import { MainContext } from '../bot/start'; +const { payRequest, isPendingPayment } = require('../ln'); +import { getUserI18nContext } from '../util'; const { orderUpdated } = require('../bot/modules/events/orders'); -exports.attemptPendingPayments = async (bot: Telegraf): Promise => { +export const attemptPendingPayments = async (bot: Telegraf): Promise => { const pendingPayments = await PendingPayment.find({ paid: false, attempts: { $lt: process.env.PAYMENT_ATTEMPTS }, @@ -18,6 +18,7 @@ exports.attemptPendingPayments = async (bot: Telegraf): Promise): Promise): Promise): Promise): Promise => { +export const attemptCommunitiesPendingPayments = async (bot: Telegraf): Promise => { const pendingPayments = await PendingPayment.find({ paid: false, attempts: { $lt: process.env.PAYMENT_ATTEMPTS }, @@ -136,6 +141,7 @@ exports.attemptCommunitiesPendingPayments = async (bot: Telegraf): request: pending.payment_request, }); const user = await User.findById(pending.user_id); + if (user === null) throw Error("User was not found in DB"); const i18nCtx: I18nContext = await getUserI18nContext(user); // If the buyer's invoice is expired we let it know and don't try to pay again if (!!payment && payment.is_expired) { @@ -147,9 +153,10 @@ exports.attemptCommunitiesPendingPayments = async (bot: Telegraf): } const community = await Community.findById(pending.community_id); + if (community === null) throw Error("Community was not found in DB"); if (!!payment && !!payment.confirmed_at) { pending.paid = true; - pending.paid_at = new Date().toISOString(); + pending.paid_at = new Date(); // Reset the community's values community.earnings = 0; diff --git a/lnurl/lnurl-pay.js b/lnurl/lnurl-pay.ts similarity index 76% rename from lnurl/lnurl-pay.js rename to lnurl/lnurl-pay.ts index bd0ada6a..96aa85d0 100644 --- a/lnurl/lnurl-pay.js +++ b/lnurl/lnurl-pay.ts @@ -1,11 +1,11 @@ -const axios = require('axios').default; -const { logger } = require('../logger'); +import axios from 'axios'; +import { logger } from "../logger"; // { // pr: String, // bech32-serialized lightning invoice // routes: [], // an empty array // } -const resolvLightningAddress = async (address, amountMsat) => { +const resolvLightningAddress = async (address: string, amountMsat: number) => { const [user, domain] = address.split('@'); const lnAddressQuery = `https://${domain}/.well-known/lnurlp/${user}`; @@ -17,7 +17,7 @@ const resolvLightningAddress = async (address, amountMsat) => { } if ( - (lnAddressRes.minSendable > amountMsat) | + (lnAddressRes.minSendable > amountMsat) || (lnAddressRes.maxSendable < amountMsat) ) { logger.info('lnAddress invalid amount'); @@ -31,7 +31,7 @@ const resolvLightningAddress = async (address, amountMsat) => { return res; }; -const existLightningAddress = async address => { +const existLightningAddress = async (address: string) => { const [user, domain] = address.split('@'); const lnAddressQuery = `https://${domain}/.well-known/lnurlp/${user}`; @@ -48,7 +48,4 @@ const existLightningAddress = async address => { } }; -module.exports = { - resolvLightningAddress, - existLightningAddress, -}; +export { resolvLightningAddress, existLightningAddress } diff --git a/package-lock.json b/package-lock.json index 139aee65..9b99bdb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@types/node": "^20.5.0", + "@types/node-schedule": "^2.1.0", "@types/qrcode": "^1.5.2", "chai": "^4.3.4", "chokidar": "^3.5.3", @@ -1773,6 +1774,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.0.tgz", "integrity": "sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==" }, + "node_modules/@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qrcode": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", diff --git a/package.json b/package.json index b622708c..7706079a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@types/node": "^20.5.0", "@types/qrcode": "^1.5.2", + "@types/node-schedule": "^2.1.0", "chai": "^4.3.4", "chokidar": "^3.5.3", "eslint": "^8.15.0", diff --git a/tsconfig.json b/tsconfig.json index f2c464b6..7b8d0663 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,5 +2,7 @@ "compilerOptions": { "strict": true, "esModuleInterop": true, + "resolveJsonModule": true, + "downlevelIteration": 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 70% rename from util/index.js rename to util/index.ts index 628b2117..8d0f2da5 100644 --- a/util/index.js +++ b/util/index.ts @@ -1,20 +1,29 @@ -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 { Telegram } from "telegraf"; +import axios from "axios"; +import fiatJson from './fiat.json'; +import languagesJson from './languages.json'; +import { Order, Community } from "../models"; +import { logger } from "../logger"; const { I18n } = require('@grammyjs/i18n'); -const currencies = require('./fiat.json'); // ISO 639-1 language codes -const languages = require('./languages.json'); -const { Order, Community } = require('../models'); -const { logger } = require('../logger'); -// ISO 4217, all ISO currency codes are 3 letters but users can trade shitcoins +const languages: ILanguages = languagesJson; +const currencies: IFiatCurrencies = fiatJson; -const isIso4217 = code => { +// ISO 4217, all ISO currency codes are 3 letters but users can trade shitcoins +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; } @@ -22,19 +31,16 @@ const isIso4217 = code => { }); }; -exports.isIso4217 = isIso4217; - -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; }; -exports.getCurrency = getCurrency; +const plural = (n: number): string => { -const plural = n => { if (n === 1) { return ''; } @@ -45,8 +51,7 @@ exports.plural = plural; // 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. - -exports.numberFormat = (code, number) => { +const numberFormat = (code: string, number: number) => { if (!isIso4217(code)) return false; if (!currencies[code]) return number; @@ -62,8 +67,7 @@ exports.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 - -exports.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({ @@ -74,7 +78,7 @@ exports.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; @@ -128,9 +132,10 @@ exports.handleReputationItems = async (buyer, seller, amount) => { } }; -exports.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); @@ -140,13 +145,13 @@ exports.getBtcFiatPrice = async (fiatCode, fiatAmount) => { } const sats = (fiatAmount / response.data.btc) * 100000000; - return parseInt(sats); + return Number(sats); } catch (error) { logger.error(error); } }; -exports.getBtcExchangePrice = (fiatAmount, satsAmount) => { +const getBtcExchangePrice = (fiatAmount: number, satsAmount: number) => { try { const satsPerBtc = 1e8; const feeRate = (satsPerBtc * fiatAmount) / satsAmount; @@ -157,8 +162,8 @@ exports.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]); @@ -167,20 +172,20 @@ const objectToArray = object => { exports.objectToArray = objectToArray; -exports.getCurrenciesWithPrice = () => { +const getCurrenciesWithPrice = () => { const currenciesArr = objectToArray(currencies); const withPrice = currenciesArr.filter(currency => currency.price); return withPrice; }; -exports.toKebabCase = string => +const toKebabCase = (string: string) => string .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .toLowerCase(); -const getEmojiRate = rate => { +const getEmojiRate = (rate: number) => { const star = '⭐'; const roundedRate = Math.round(rate); const output = []; @@ -189,12 +194,9 @@ const getEmojiRate = rate => { return output.join(''); }; -exports.getEmojiRate = getEmojiRate; - // 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); } @@ -205,32 +207,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)); }; -exports.decimalRound = decimalRound; - -exports.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, '\\'); }; -exports.sanitizeMD = sanitizeMD; - -exports.secondsToTime = secs => { +const secondsToTime = (secs: number) => { const hours = Math.floor(secs / (60 * 60)); const divisor = secs % (60 * 60); @@ -242,9 +241,9 @@ exports.secondsToTime = secs => { }; }; -exports.isGroupAdmin = async (groupId, user, telegram) => { +const isGroupAdmin = async (groupId: string, user: UserDocument, telegram: 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') @@ -268,12 +267,12 @@ exports.isGroupAdmin = async (groupId, user, telegram) => { logger.error(error); return { success: false, - message: error.toString(), + message: String(error), }; } }; -exports.deleteOrderFromChannel = async (order, telegram) => { +const deleteOrderFromChannel = async (order: IOrder, telegram: Telegram) => { try { let channel = process.env.CHANNEL; if (order.community_id) { @@ -291,13 +290,13 @@ exports.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); } }; -exports.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 }); @@ -307,7 +306,7 @@ exports.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; } @@ -318,10 +317,11 @@ exports.getOrderChannel = async order => { return channel; }; -exports.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; } @@ -333,8 +333,13 @@ exports.getDisputeChannel = async order => { * @param {*} user * @returns i18n context */ -exports.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, @@ -344,7 +349,7 @@ exports.getUserI18nContext = async user => { return i18n.createContext(user.lang); }; -exports.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 @@ -363,7 +368,7 @@ exports.getDetailedOrder = (i18n, order, buyer, seller) => { takenAt = sanitizeMD(takenAt); const previousDisputeStatus = sanitizeMD(order.previous_dispute_status); 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 buyerAge = getUserAge(buyer); @@ -397,7 +402,7 @@ exports.getDetailedOrder = (i18n, order, buyer, seller) => { }; // We need to know if this user is a dispute solver for this community -exports.isDisputeSolver = (community, user) => { +const isDisputeSolver = (community: ICommunity, user: UserDocument) => { if (!community || !user) { return false; } @@ -407,49 +412,45 @@ exports.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 -exports.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; }; -exports.itemsFromMessage = str => { +const itemsFromMessage = (str: string) => { return str .split(' ') .map(e => e.trim()) .filter(e => !!e); }; -// Check if a number is int -const isInt = n => parseInt(n) === n; - -exports.isInt = isInt; - // Check if a number is float -exports.isFloat = n => typeof n === 'number' && !isInt(n); +const isFloat = (n: number) => typeof n === 'number' && !Number.isInteger(n); // Returns an emoji flag for a language -exports.getLanguageFlag = code => { +const getLanguageFlag = (code: string): ILanguage => { return languages[code]; }; -exports.delay = time => { +const delay = (time: number) => { return new Promise(resolve => setTimeout(resolve, time)); }; // Returns the hold invoice expiration time in seconds, // and the hold invoice safety window in seconds -exports.holdInvoiceExpirationInSecs = () => { +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, @@ -457,7 +458,7 @@ exports.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( @@ -466,21 +467,19 @@ const getUserAge = user => { return ageInDays; }; -exports.getUserAge = getUserAge; - /** * Returns order expiration time text * @param {*} order order object * @param {*} i18n context * @returns String with the remaining time to expiration in format '1 hours 30 minutes' */ -exports.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'); @@ -494,13 +493,45 @@ exports.getTimeToExpirationOrder = (order, i18n) => { return `${hours} ${textHour} ${minutes} ${textMin}`; }; -exports.getStars = (rate, totalReviews) => { +export const getStars = (rate: number, totalReviews: number) => { const stars = getEmojiRate(rate); const roundedRating = decimalRound(rate, -1); return `${roundedRating} ${stars} (${totalReviews})`; }; -exports.removeAtSymbol = text => { +export const removeAtSymbol = (text: string) => { return text[0] === '@' ? text.slice(1) : text; +} + +export { + isIso4217, + plural, + getCurrency, + handleReputationItems, + getBtcFiatPrice, + getBtcExchangePrice, + getCurrenciesWithPrice, + getEmojiRate, + decimalRound, + extractId, + sanitizeMD, + secondsToTime, + isGroupAdmin, + deleteOrderFromChannel, + getOrderChannel, + getUserI18nContext, + numberFormat, + getDisputeChannel, + getDetailedOrder, + isDisputeSolver, + getFee, + itemsFromMessage, + isFloat, + getLanguageFlag, + delay, + holdInvoiceExpirationInSecs, + getUserAge, + getTimeToExpirationOrder, + toKebabCase }; 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; +}