diff --git a/.gitignore b/.gitignore index 43728c7..fd9a36b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ node_modules/ +config/*.config.js config.js data/*.json \ No newline at end of file diff --git a/README.md b/README.md index adddb97..7df86ae 100644 --- a/README.md +++ b/README.md @@ -14,23 +14,35 @@ requirements: node.js >=16.14.2 ``` +### Install dependencies -Install dependencies - -`npm install` - -Before run, store a `config.js` file in the project folder. The config sample can be find in `config.sample.js` +``` +npm install +``` +### Set up storage with command +``` +npm run setup +``` +### Set up configs. -AND +Before run, store a `global.config.js` file in the project config folder. The config sample can be find in config folder `global.config.sample.js` +- SET `telegram` config. +If you want to run message via telegram transport, create `telegram.config.js` in `config/` folder. Sample is `telegram.config.sample.js` +- SET `discord` config. If you want to run message via discord transport, create `discord.config.js` in `config/` folder. Sample is `discord.config.sample.js` +- SET both config if you want to run `discord` and `telegram` -Add to data folder `telegram.json` file with default params `{ "last_notified_proposal_id": 0 }` -RUN with cron: +### RUN with cron: -example: +example with 2 transports: +``` + */30 * * * * cd /user/Heimdallbot && npm start -- --telegram --discord --silent >> /user/heimdallbot.log ``` - */30 * * * * cd /user/Heimdallbot && npm run run:tlg --silent >> /user/heimdallbot.log +example with 1 transports: ``` + */30 * * * * cd /user/Heimdallbot && npm start --silent -- --telegram >> /user/heimdallbot.log +``` + if use `nvm` do alias ``` diff --git a/config/discord.config.sample.js b/config/discord.config.sample.js new file mode 100644 index 0000000..7aadee1 --- /dev/null +++ b/config/discord.config.sample.js @@ -0,0 +1,4 @@ +module.exports = { + BOT_TOKEN: 'required', + CHAT_ID: 'required', +} diff --git a/config/global.config.sample.js b/config/global.config.sample.js new file mode 100644 index 0000000..96b821c --- /dev/null +++ b/config/global.config.sample.js @@ -0,0 +1,3 @@ +module.exports = { + PROPOSALS_API_REST_URL: 'required', +} diff --git a/config/telegram.config.sample.js b/config/telegram.config.sample.js new file mode 100644 index 0000000..7aadee1 --- /dev/null +++ b/config/telegram.config.sample.js @@ -0,0 +1,4 @@ +module.exports = { + BOT_TOKEN: 'required', + CHAT_ID: 'required', +} diff --git a/package-lock.json b/package-lock.json index e4d2e89..21334ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,11 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@discordjs/rest": "^0.3.0", "axios": "^0.26.1", + "commander": "^9.2.0", + "discord-api-types": "^0.31.1", + "discord.js": "^13.6.0", "grammy": "^1.7.3", "tslog": "^3.3.3" }, @@ -20,6 +24,64 @@ "prettier": "^2.6.2" } }, + "node_modules/@discordjs/builders": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.11.0.tgz", + "integrity": "sha512-ZTB8yJdJKrKlq44dpWkNUrAtEJEq0gqpb7ASdv4vmq6/mZal5kOv312hQ56I/vxwMre+VIkoHquNUAfnTbiYtg==", + "dependencies": { + "@sindresorhus/is": "^4.2.0", + "discord-api-types": "^0.26.0", + "ts-mixer": "^6.0.0", + "tslib": "^2.3.1", + "zod": "^3.11.6" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@discordjs/collection": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.4.0.tgz", + "integrity": "sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw==", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.3.0.tgz", + "integrity": "sha512-F9aeP3odlAlllM1ciBZLdd+adiAyBj4VaZBejj4UMj4afE2wfCkNTGvYYiRxrXUE9fN7e/BuDP2ePl0tVA2m7Q==", + "dependencies": { + "@discordjs/collection": "^0.4.0", + "@sapphire/async-queue": "^1.1.9", + "@sapphire/snowflake": "^3.0.1", + "discord-api-types": "^0.26.1", + "form-data": "^4.0.0", + "node-fetch": "^2.6.5", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", @@ -65,6 +127,70 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@sapphire/async-queue": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.1.tgz", + "integrity": "sha512-FFTlPOWZX1kDj9xCAsRzH5xEJfawg1lNoYAA+ecOWJMHOfiZYb1uXOI3ne9U4UILSEPwfE68p3T9wUHwIQfR0g==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.1.tgz", + "integrity": "sha512-vmZq1I6J6iNRQVXP+N9HzOMOY4ORB3MunoFeWCw/aBnZTf1cDgDvP0RZFQS53B1TN95AIgFY9T+ItQ/fWAUYWQ==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@types/node": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", + "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -143,6 +269,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "node_modules/axios": { "version": "0.26.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", @@ -215,6 +346,25 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -257,6 +407,47 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.31.1.tgz", + "integrity": "sha512-QBWl1/x20ZyKAiY750wuWAWokudhlOnQzXhCTEkQsCNOOM9tU/HIqkpLgZqJI1EFmZMeWHdaRKzpiD7n7iCS+w==" + }, + "node_modules/discord.js": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.6.0.tgz", + "integrity": "sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g==", + "dependencies": { + "@discordjs/builders": "^0.11.0", + "@discordjs/collection": "^0.4.0", + "@sapphire/async-queue": "^1.1.9", + "@types/node-fetch": "^2.5.12", + "@types/ws": "^8.2.2", + "discord-api-types": "^0.26.0", + "form-data": "^4.0.0", + "node-fetch": "^2.6.1", + "ws": "^8.4.0" + }, + "engines": { + "node": ">=16.6.0", + "npm": ">=7.0.0" + } + }, + "node_modules/discord.js/node_modules/discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -526,6 +717,19 @@ } } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -743,6 +947,25 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -995,6 +1218,16 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, + "node_modules/ts-mixer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.1.tgz", + "integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==" + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, "node_modules/tslog": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.3.3.tgz", @@ -1088,9 +1321,82 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.14.4.tgz", + "integrity": "sha512-U9BFLb2GO34Sfo9IUYp0w3wJLlmcyGoMd75qU9yf+DrdGA4kEx6e+l9KOkAlyUO0PSQzZCa3TR4qVlcmwqSDuw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { + "@discordjs/builders": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.11.0.tgz", + "integrity": "sha512-ZTB8yJdJKrKlq44dpWkNUrAtEJEq0gqpb7ASdv4vmq6/mZal5kOv312hQ56I/vxwMre+VIkoHquNUAfnTbiYtg==", + "requires": { + "@sindresorhus/is": "^4.2.0", + "discord-api-types": "^0.26.0", + "ts-mixer": "^6.0.0", + "tslib": "^2.3.1", + "zod": "^3.11.6" + }, + "dependencies": { + "discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==" + } + } + }, + "@discordjs/collection": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.4.0.tgz", + "integrity": "sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw==" + }, + "@discordjs/rest": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.3.0.tgz", + "integrity": "sha512-F9aeP3odlAlllM1ciBZLdd+adiAyBj4VaZBejj4UMj4afE2wfCkNTGvYYiRxrXUE9fN7e/BuDP2ePl0tVA2m7Q==", + "requires": { + "@discordjs/collection": "^0.4.0", + "@sapphire/async-queue": "^1.1.9", + "@sapphire/snowflake": "^3.0.1", + "discord-api-types": "^0.26.1", + "form-data": "^4.0.0", + "node-fetch": "^2.6.5", + "tslib": "^2.3.1" + }, + "dependencies": { + "discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==" + } + } + }, "@eslint/eslintrc": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", @@ -1130,6 +1436,55 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@sapphire/async-queue": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.1.tgz", + "integrity": "sha512-FFTlPOWZX1kDj9xCAsRzH5xEJfawg1lNoYAA+ecOWJMHOfiZYb1uXOI3ne9U4UILSEPwfE68p3T9wUHwIQfR0g==" + }, + "@sapphire/snowflake": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.1.tgz", + "integrity": "sha512-vmZq1I6J6iNRQVXP+N9HzOMOY4ORB3MunoFeWCw/aBnZTf1cDgDvP0RZFQS53B1TN95AIgFY9T+ItQ/fWAUYWQ==" + }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + }, + "@types/node": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", + "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" + }, + "@types/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "requires": { + "@types/node": "*" + } + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1184,6 +1539,11 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "axios": { "version": "0.26.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", @@ -1244,6 +1604,19 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1275,6 +1648,39 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "discord-api-types": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.31.1.tgz", + "integrity": "sha512-QBWl1/x20ZyKAiY750wuWAWokudhlOnQzXhCTEkQsCNOOM9tU/HIqkpLgZqJI1EFmZMeWHdaRKzpiD7n7iCS+w==" + }, + "discord.js": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.6.0.tgz", + "integrity": "sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g==", + "requires": { + "@discordjs/builders": "^0.11.0", + "@discordjs/collection": "^0.4.0", + "@sapphire/async-queue": "^1.1.9", + "@types/node-fetch": "^2.5.12", + "@types/ws": "^8.2.2", + "discord-api-types": "^0.26.0", + "form-data": "^4.0.0", + "node-fetch": "^2.6.1", + "ws": "^8.4.0" + }, + "dependencies": { + "discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==" + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1467,6 +1873,16 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1630,6 +2046,19 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1805,6 +2234,16 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, + "ts-mixer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.1.tgz", + "integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==" + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, "tslog": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.3.3.tgz", @@ -1877,6 +2316,17 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "requires": {} + }, + "zod": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.14.4.tgz", + "integrity": "sha512-U9BFLb2GO34Sfo9IUYp0w3wJLlmcyGoMd75qU9yf+DrdGA4kEx6e+l9KOkAlyUO0PSQzZCa3TR4qVlcmwqSDuw==" } } } diff --git a/package.json b/package.json index 9d1ccd1..e8c79b1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "./src/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "run:tlg": "NODE_ENV=production node ./src/index.js", + "start": "NODE_ENV=production node ./src/index.js run", + "setup": "NODE_ENV=prodction node ./src/set-up-data.js", "start:local": "DEBUG=\"grammy*\" nodemon index.js", "format:check": "prettier --check .", "format:write": "prettier --write .", @@ -22,7 +23,11 @@ "prettier": "^2.6.2" }, "dependencies": { + "@discordjs/rest": "^0.3.0", "axios": "^0.26.1", + "commander": "^9.2.0", + "discord-api-types": "^0.31.1", + "discord.js": "^13.6.0", "grammy": "^1.7.3", "tslog": "^3.3.3" } diff --git a/src/config-manager.js b/src/config-manager.js index d53fc80..602ed6f 100644 --- a/src/config-manager.js +++ b/src/config-manager.js @@ -1,6 +1,12 @@ const path = require('path') -const configSample = require('./../config.sample') -function checkConfigKeys(config) { + +function configSampleByConfigName(configName) { + const arr = configName.split('.') + arr.splice(2, 0, 'sample') + return arr.join('.') +} + +function checkConfigKeys(config, configSample) { const configKeys = Object.keys(config) Object.entries(configSample) .map(([key, value]) => (value ? key : null)) @@ -12,21 +18,30 @@ function checkConfigKeys(config) { }) } +// 'discord.config.sample.js'.split('.').filter(it => i!='sample') function Config() { let config = null return { init: (configPath) => { if (config) return - if (configPath) { - try { - config = require(path.resolve(process.cwd(), configPath)) - checkConfigKeys(config) - } catch (e) { - throw new Error('Error to load config') - } + if (!configPath) throw new Error('config path should be specified') + let configSample = {} + try { + config = require(path.resolve(process.cwd(), configPath)) + const configSampleName = configSampleByConfigName( + configPath.split('/').at(-1) + ) + configSample = require(path.resolve( + process.cwd(), + 'config', + configSampleName + )) + } catch (e) { + throw new Error('Error to load config') } - config = require(path.resolve(process.cwd(), 'config.js')) - checkConfigKeys(config) + checkConfigKeys(config, configSample) + // config = require(path.resolve(process.cwd(), 'config.js')) + // checkConfigKeys(config) }, get config() { if (!config) @@ -36,4 +51,7 @@ function Config() { } } -module.exports = Config() +module.exports = { + globalConfig: Config(), + Config, +} diff --git a/src/constant.js b/src/constant.js new file mode 100644 index 0000000..e2bfca5 --- /dev/null +++ b/src/constant.js @@ -0,0 +1,8 @@ +module.exports = { + DISCORD_DATA_PATH: './data/discord.json', + TELEGRAM_DATA_PATH: './data/telegram.json', + TELEGRAM_CONFIG_PATH: './config/telegram.config.js', + DISCORD_CONFIG_PATH: './config/discord.config.js', + GLOBAL_CONFIG_PATH: './config/global.config.js', + TO_VOTE_PROPOSAL_URL: 'https://mainnet.odinprotocol.io/proposal/', +} diff --git a/src/execute-message.js b/src/execute-message.js new file mode 100644 index 0000000..69a148b --- /dev/null +++ b/src/execute-message.js @@ -0,0 +1,47 @@ +const { getProposals } = require('./propsals-api') +const constant = require('./constant') +function createMessageFromProposal(proposal) { + return `New proposal #${proposal.proposal_id}\n\n***${ + proposal.content.title + }***\n\n${proposal.content.description}\n\nStart: ${ + proposal.voting_start_time.split('T')[0] + }\nEnd: ${proposal.voting_end_time.split('T')[0]} \n\nTo vote: ${ + constant.TO_VOTE_PROPOSAL_URL + }${proposal.proposal_id}` +} + +async function executeMessage(transport, log, store) { + log.info('Start') + const proposals = await getProposals() + const propData = store.read() + + const actualProposals = proposals + .filter( + (proposal) => proposal.status === 'PROPOSAL_STATUS_VOTING_PERIOD' + ) + .filter( + (prop) => + Number(prop.proposal_id) > propData['last_notified_proposal_id'] + ) + + if (actualProposals.length) { + for (const prop of actualProposals) { + // sender.send(content) + await transport.send(createMessageFromProposal(prop)) + log.info( + 'successfully notified! proposal number:', + prop.proposal_id + ) + } + const lastProp = actualProposals.sort( + (a, b) => b.proposal_id - a.proposal_id + )[0] + store.save({ last_notified_proposal_id: Number(lastProp.proposal_id) }) + log.info( + 'successfully state updated! proposal number changed:', + lastProp.proposal_id + ) + } + log.info('End') +} +module.exports = executeMessage diff --git a/src/index.js b/src/index.js index 5c115e8..8362e9c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,68 +1,56 @@ -const { Bot } = require('grammy') -const configManager = require('./config-manager') -configManager.init() -const { getProposals } = require('./propsals-api') -const fs = require('fs') -const log = require('./logger').Logger('telegram') -const bot = new Bot(configManager.config.BOT_TOKEN) +const commander = require('commander') +const { Config, globalConfig } = require('./config-manager') +const Logger = require('./logger') +const log = Logger.Logger('CLI') +const constant = require('./constant') +const telegramMessage = require('./message/telegram.message') +const discordMessage = require('./message/discord.message') +const executeMessage = require('./execute-message') +const Store = require('./store') +globalConfig.init('./config/global.config.js') -function createMessageFromProposal(proposal) { - return `New proposal #${proposal.proposal_id}\n\n***${ - proposal.content.title - }***\n\n${proposal.content.description}\n\nStart: ${ - proposal.voting_start_time.split('T')[0] - }\nEnd: ${ - proposal.voting_end_time.split('T')[0] - } \n\nTo vote: https://mainnet.odinprotocol.io/proposal/${ - proposal.proposal_id - }` -} - -async function main() { - log.info('Start') - const proposals = await getProposals() - const propData = JSON.parse( - fs.readFileSync('./data/telegram.json', 'utf-8') - ) - - const actualProposals = proposals - .filter( - (proposal) => proposal.status === 'PROPOSAL_STATUS_VOTING_PERIOD' - ) - .filter( - (prop) => - Number(prop.proposal_id) > propData['last_notified_proposal_id'] - ) - - if (actualProposals.length) { - for (const prop of actualProposals) { - await bot.api.sendMessage( - configManager.config.CHAT_ID, - createMessageFromProposal(prop), - { - parse_mode: 'Markdown', - } +const program = new commander.Command() +program + .command('run') + .description('Run a bot') + .option('--telegram', 'run with `telegram` transport') + .option('--discord', 'run with `discord` transport') + .action(async (option) => { + if (!option.telegram && !option.discord) { + log.error('specify transport') + } + const executions = [] + if (option.telegram) { + const telegramExecute = async () => { + const configInstance = Config() + configInstance.init(constant.TELEGRAM_CONFIG_PATH) + const logger = Logger.createLogger('telegram') + const store = new Store(constant.TELEGRAM_DATA_PATH) + const telegram = new telegramMessage(configInstance.config) + await executeMessage(telegram, logger, store) + } + executions.push( + telegramExecute().catch((e) => + log.fatal('telegram execution fail', e) + ) ) - log.info( - 'successfully notified! proposal number:', - prop.proposal_id + } + if (option.discord) { + const discordExecute = async () => { + const configInstance = Config() + configInstance.init(constant.DISCORD_CONFIG_PATH) + const logger = Logger.createLogger('discord') + const store = new Store(constant.DISCORD_DATA_PATH) + const discord = new discordMessage(configInstance.config) + await executeMessage(discord, logger, store) + } + executions.push( + discordExecute().catch((e) => + log.fatal('discord execution fail', e) + ) ) } - const lastProp = actualProposals.sort( - (a, b) => b.proposal_id - a.proposal_id - )[0] - fs.writeFileSync( - './data/telegram.json', - JSON.stringify({ - last_notified_proposal_id: Number(lastProp.proposal_id), - }) - ) - log.info( - 'successfully state updated! proposal number changed:', - lastProp.proposal_id - ) - } - log.info('End') -} + await Promise.allSettled(executions) + }) -main().catch(log.fatal) +program.parse() diff --git a/src/logger.js b/src/logger.js index 8761b08..68366fe 100644 --- a/src/logger.js +++ b/src/logger.js @@ -5,4 +5,7 @@ module.exports = { Logger: (name) => { return log.getChildLogger({ name }) }, + createLogger: (name) => { + return new Logger().getChildLogger({ name }) + }, } diff --git a/src/message/discord.message.js b/src/message/discord.message.js new file mode 100644 index 0000000..a1d34f5 --- /dev/null +++ b/src/message/discord.message.js @@ -0,0 +1,17 @@ +const MessageAbstract = require('./message.abstract') +const { REST } = require('@discordjs/rest') +const { Routes } = require('discord-api-types/v9') + +class DiscordMessage extends MessageAbstract { + constructor(config) { + super() + this.config = config + this.rest = new REST({ version: '9' }).setToken(config.BOT_TOKEN) + } + async send(content) { + await this.rest.post(Routes.channelMessages(this.config.CHAT_ID), { + body: { content: '> ' + content }, + }) + } +} +module.exports = DiscordMessage diff --git a/src/message/message.abstract.js b/src/message/message.abstract.js new file mode 100644 index 0000000..e80def2 --- /dev/null +++ b/src/message/message.abstract.js @@ -0,0 +1,5 @@ +module.exports = class MessageAbstract { + send() { + throw Error('required method') + } +} diff --git a/src/message/telegram.message.js b/src/message/telegram.message.js new file mode 100644 index 0000000..ddb92e7 --- /dev/null +++ b/src/message/telegram.message.js @@ -0,0 +1,18 @@ +const MessageAbstract = require('./message.abstract') +const { Bot } = require('grammy') + +class TelegramMessage extends MessageAbstract { + constructor(config) { + super() + this.bot = new Bot(config.BOT_TOKEN) + this.config = config + } + + async send(content) { + await this.bot.api.sendMessage(this.config.CHAT_ID, content, { + parse_mode: 'Markdown', + }) + } +} + +module.exports = TelegramMessage diff --git a/src/propsals-api.js b/src/propsals-api.js index 4afb8dd..874131e 100644 --- a/src/propsals-api.js +++ b/src/propsals-api.js @@ -1,8 +1,9 @@ -const { config } = require('./config-manager') +const { globalConfig } = require('./config-manager') const axios = require('axios') async function getProposals() { - const data = (await axios.get(config.PROPOSALS_API_REST_URL)).data + const data = (await axios.get(globalConfig.config.PROPOSALS_API_REST_URL)) + .data return data.proposals } diff --git a/src/set-up-data.js b/src/set-up-data.js new file mode 100644 index 0000000..56bede3 --- /dev/null +++ b/src/set-up-data.js @@ -0,0 +1,17 @@ +const fs = require('fs') +const constant = require('./constant') +const discordData = fs.existsSync(constant.DISCORD_DATA_PATH) +if (!discordData) { + fs.writeFileSync( + constant.DISCORD_DATA_PATH, + JSON.stringify({ last_notified_proposal_id: 0 }) + ) +} + +const telegramData = fs.existsSync(constant.TELEGRAM_DATA_PATH) +if (!telegramData) { + fs.writeFileSync( + constant.TELEGRAM_DATA_PATH, + JSON.stringify({ last_notified_proposal_id: 0 }) + ) +} diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..22eb41b --- /dev/null +++ b/src/store.js @@ -0,0 +1,14 @@ +const fs = require('fs') +class Store { + constructor(pathToData) { + this.path = pathToData + } + read() { + return JSON.parse(fs.readFileSync(this.path, 'utf-8')) + } + save(data) { + fs.writeFileSync(this.path, JSON.stringify(data)) + } +} + +module.exports = Store