diff --git a/app/app.js b/app/app.js index 76c8552..c4b945d 100644 --- a/app/app.js +++ b/app/app.js @@ -1,15 +1,29 @@ class App { #db; #mongoose; + #log; + #marketsService; + #bot; constructor(options) { this.#mongoose = options.mongoose; this.#db = options.db; + this.#log = options.logger.debug(this.constructor.name); + this.#marketsService = options.marketsService; + this.#bot = options.bot; + + this.#log('Initializing...'); } async start() { - this.#mongoose.connection.once('open', () => { - console.log('Connected to database!'); + this.#mongoose.connection.once('open', async () => { + this.#log('Connected to database!'); + + await this.#marketsService.watch(changed => { + this.#log(changed.length); + }); + + this.#bot.start(); }); await this.#db(); diff --git a/app/bot/TelegramBot.js b/app/bot/TelegramBot.js new file mode 100644 index 0000000..1be5dc0 --- /dev/null +++ b/app/bot/TelegramBot.js @@ -0,0 +1,44 @@ +const { I18n } = require("@grammyjs/i18n"); +const { Bot } = require("grammy"); + +const i18n = new I18n({ + defaultLocale: "en", + directory: "app/bot/locales" +}); + +class TelegramBot { + #log; + bot; + + constructor(options) { + this.#log = options.logger.debug(this.constructor.name); + this.bot = new Bot(options.config.telegramBotToken); + + this.#log('Initializing...'); + this.bot.catch(this.errorHandler); + this.bot.use(options.logger.middleware(this.constructor.name)); + this.bot.use(i18n); + this.bot.command('start', async ctx => { + await ctx.reply('Test'); + }); + } + + errorHandler({ ctx, error }) { + this.#log('Error while handling update %s:', ctx.update.update_id); + + if (error instanceof GrammyError) { + this.#log('Error in request: %s', error.description); + } else if (error instanceof HttpError) { + this.#log('Could not contact Telegram:', error); + } else { + this.#log("Unknown error:", error); + } + } + + start() { + this.#log('Started.'); + this.bot.start(); + } +} + +module.exports = TelegramBot; \ No newline at end of file diff --git a/bot/locales/en.ftl b/app/bot/locales/en.ftl similarity index 100% rename from bot/locales/en.ftl rename to app/bot/locales/en.ftl diff --git a/bot/locales/ru.ftl b/app/bot/locales/ru.ftl similarity index 100% rename from bot/locales/ru.ftl rename to app/bot/locales/ru.ftl diff --git a/bot/locales/uk.ftl b/app/bot/locales/uk.ftl similarity index 100% rename from bot/locales/uk.ftl rename to app/bot/locales/uk.ftl diff --git a/app/container.js b/app/container.js index 5f3d039..89e2298 100644 --- a/app/container.js +++ b/app/container.js @@ -1,8 +1,11 @@ const { createContainer, asClass, asValue, asFunction } = require('awilix'); const config = require('./config'); +const logger = require('./logger'); const { connectToDatabase, mongoose } = require('./db/database'); const App = require('./app'); +const MarketsService = require('./market/MarketsService'); +const TelegramBot = require('./bot/TelegramBot'); const container = createContainer({ strict: true @@ -10,9 +13,12 @@ const container = createContainer({ container.register({ config: asValue(config), + logger: asValue(logger), mongoose: asValue(mongoose), db: asFunction(connectToDatabase).singleton(), - app: asClass(App) + app: asClass(App), + marketsService: asClass(MarketsService).singleton(), + bot: asClass(TelegramBot).singleton() }); module.exports = container; \ No newline at end of file diff --git a/app/db/database.js b/app/db/database.js index ee7c5c5..a180471 100644 --- a/app/db/database.js +++ b/app/db/database.js @@ -1,8 +1,10 @@ const mongoose = require('mongoose'); -const connectToDatabase = ({ config }) => async () => { - if (mongoose.connection.readyState !== 1) +const connectToDatabase = ({ config, logger }) => async () => { + if (mongoose.connection.readyState !== 1) { + logger.debug('DB')('Connecting...'); await mongoose.connect(config.dbConnectionString); + } }; module.exports = { diff --git a/db/schemas/Monitor.js b/app/db/schemas/Monitor.js similarity index 100% rename from db/schemas/Monitor.js rename to app/db/schemas/Monitor.js diff --git a/db/schemas/User.js b/app/db/schemas/User.js similarity index 100% rename from db/schemas/User.js rename to app/db/schemas/User.js diff --git a/app/logger/debug.js b/app/logger/debug.js new file mode 100644 index 0000000..08c0ae8 --- /dev/null +++ b/app/logger/debug.js @@ -0,0 +1,3 @@ +var debug = require('debug'); + +module.exports = source => debug(source); \ No newline at end of file diff --git a/app/logger/index.js b/app/logger/index.js new file mode 100644 index 0000000..c5a95a7 --- /dev/null +++ b/app/logger/index.js @@ -0,0 +1,6 @@ +const debug = require('./debug'); +const middleware = require('./middleware'); + +module.exports = { + debug, middleware +}; \ No newline at end of file diff --git a/app/logger/middleware.js b/app/logger/middleware.js new file mode 100644 index 0000000..7bc13da --- /dev/null +++ b/app/logger/middleware.js @@ -0,0 +1,10 @@ +var debug = require('debug'); + +const middleware = source => async (ctx, next) => { + debug(source)('Update Received:'); + debug(source)(ctx.message ?? ctx.update); + + await next(); +}; + +module.exports = middleware; \ No newline at end of file diff --git a/market/Market.js b/app/market/Market.js similarity index 94% rename from market/Market.js rename to app/market/Market.js index 457970e..dc9472b 100644 --- a/market/Market.js +++ b/app/market/Market.js @@ -1,6 +1,4 @@ -const round = num => { - return Math.ceil(num * 1000000) / 1000000; -} +const round = require("../utils/round"); module.exports = class Market { /** @type {string} */ diff --git a/market/MarketCollection.js b/app/market/MarketsService.js similarity index 50% rename from market/MarketCollection.js rename to app/market/MarketsService.js index 61cf31d..ed417de 100644 --- a/market/MarketCollection.js +++ b/app/market/MarketsService.js @@ -1,62 +1,38 @@ +const round = require("../utils/round"); const Market = require("./Market"); -const round = num => { - return Math.ceil(num * 1000000) / 1000000; -} +const apiUrl = 'https://www.worldcoinindex.com/apiservice/v2getmarkets'; +const fetchInterval = 60_000; -module.exports = class MarketCollection { - /** @type {Market[]} */ +class MarketsService { + #log; + #apiKey; + #onChange; #markets = []; - /** @type {MarketCollectionOptions['onMarketsChange']} */ - #onMarketsChange = () => {}; - - #interval = 60_000; - #apiUrl = 'https://www.worldcoinindex.com/apiservice/v2getmarkets'; - #apiKey = process.env.WCI_KEY; - #fiat = 'USD'; - - /** - * @param {MarketCollectionOptions} [options] - */ - constructor(options = {}) { - const { onMarketsChange } = options; + constructor({ logger, config }) { + this.#log = logger.debug(this.constructor.name); + this.#apiKey = config.wciApiKey; - if (onMarketsChange) - this.#onMarketsChange = onMarketsChange; + this.#log('Initializing...'); } - /** - * @returns {Promise} - */ - async #fetchMarkets() { + async watch(onChange) { try { - const response = await fetch(`${this.#apiUrl}?key=${this.#apiKey}&fiat=${this.#fiat}`); - const json = await response.json(); - - if (json.Markets && Array.isArray(json.Markets) && Array.isArray(json.Markets.at(0))) - return json.Markets.at(0); - } catch (err) { - console.error('ERROR', err); - return Promise.reject(err.message); - } - } + if (onChange && !this.#onChange) + this.#onChange = onChange; - async refresh() { - try { const changes = []; - const marketsData = await this.#fetchMarkets(); - - console.log('received markets data', marketsData.length); + const marketsData = await this.#fetch(); marketsData.sort((a, b) => a.Name.localeCompare(b.Name)).forEach(marketData => { const foundIndex = this.#markets.findIndex(m => m.getName() === marketData.Name); - if (foundIndex >= 0) { + if (foundIndex !== -1) { const oldPrice = round(this.#markets[foundIndex].getPrice()); const newPrice = round(marketData.Price); if (oldPrice !== newPrice) { - console.log('change found', oldPrice, newPrice); + this.#log('%s price changed: %s -> %s', this.#markets[foundIndex].getCoin(), oldPrice, newPrice); changes.push(new Market(marketData)); } @@ -67,21 +43,34 @@ module.exports = class MarketCollection { } }); - if (changes.length) - this.#onMarketsChange(changes); + if (changes.length && this.#onChange) + this.#onChange(changes); } catch (err) { - console.error('ERROR', err); + this.#log('ERROR:', err); } setTimeout(() => { - this.refresh(); - }, this.#interval); + this.watch(); + }, fetchInterval); + } + + async #fetch() { + try { + this.#log('Fetching markets...'); + const response = await fetch(`${apiUrl}?key=${this.#apiKey}&fiat=USD`); + const json = await response.json(); + + if (json.Markets && Array.isArray(json.Markets) && Array.isArray(json.Markets.at(0))) { + this.#log('Markets retrieved:', json.Markets.at(0).length); + return json.Markets.at(0); + } + + } catch (err) { + this.#log('ERROR:', err); + return Promise.reject(err.message); + } } - /** - * @param {string} phrase - * @returns {Market[]} - */ findMarkets(phrase) { const searchPhrase = phrase.toLowerCase(); @@ -90,11 +79,9 @@ module.exports = class MarketCollection { ); } - /** - * @param {string} coin - * @returns {Market} - */ getMarketByCoin(coin) { return this.#markets.find(m => m.getCoin() === coin); } -} \ No newline at end of file +} + +module.exports = MarketsService; \ No newline at end of file diff --git a/app/utils/round.js b/app/utils/round.js new file mode 100644 index 0000000..5b849b6 --- /dev/null +++ b/app/utils/round.js @@ -0,0 +1,5 @@ +const round = num => { + return Math.ceil(num * 1000000) / 1000000; +} + +module.exports = round; \ No newline at end of file diff --git a/db/connect.js b/db/connect.js deleted file mode 100644 index fdd4f5f..0000000 --- a/db/connect.js +++ /dev/null @@ -1,7 +0,0 @@ -const mongoose = require('mongoose'); - -mongoose.connect(process.env.DB_CONNECTION_STRING) - .catch(err => console.error(err)) - .then(() => console.log('Connected to DataBase.')); - -module.exports = mongoose; \ No newline at end of file diff --git a/logger/index.js b/logger/index.js deleted file mode 100644 index e70a8a9..0000000 --- a/logger/index.js +++ /dev/null @@ -1,6 +0,0 @@ -const logger = async (ctx, next) => { - console.log('LOGGER', ctx.message ?? ctx.update); - await next(); -}; - -module.exports = logger; \ No newline at end of file diff --git a/market/index.js b/market/index.js deleted file mode 100644 index 120b70e..0000000 --- a/market/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const MarketCollection = require("./MarketCollection"); - -module.exports = MarketCollection; \ No newline at end of file diff --git a/market/type.d.ts b/market/type.d.ts deleted file mode 100644 index 7c749b7..0000000 --- a/market/type.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare type MarketData = { - Label: string; - Name: string; - Price: number; - Volume_24h: number; - Timestamp: number; -}; - -declare type MarketCollectionOptions = { - onMarketsChange?: (markets: import('./Market')[]) => void -}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0eb4044..716c00d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@grammyjs/conversations": "^1.2.0", "@grammyjs/i18n": "^1.0.2", "awilix": "^10.0.2", + "debug": "^4.3.5", "dotenv": "^16.4.5", "grammy": "^1.24.1", "moment": "^2.30.1", diff --git a/package.json b/package.json index 61eb9fb..4e9b5d4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node ./index.js" + "start": "DEBUG=* node ./index.js" }, "author": "Anton Korotkov", "license": "ISC", @@ -13,6 +13,7 @@ "@grammyjs/conversations": "^1.2.0", "@grammyjs/i18n": "^1.0.2", "awilix": "^10.0.2", + "debug": "^4.3.5", "dotenv": "^16.4.5", "grammy": "^1.24.1", "moment": "^2.30.1",