diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8555f4f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI workflow + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node-version: [10.x, 12.x, 14.x] + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install Dependencies + run: npm install --ignore-scripts + - name: Test + run: npm run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49a87e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,155 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files +*.swp + +# macOS files +.DS_Store + +# Clinic +.clinic + +# lock files +bun.lockb +package-lock.json +pnpm-lock.yaml +yarn.lock + +# editor files +.vscode +.idea + +#tap files +.tap/ + +# test tap report +out.tap diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.taprc b/.taprc new file mode 100644 index 0000000..68c6fab --- /dev/null +++ b/.taprc @@ -0,0 +1,5 @@ +ts: false +jsx: false +coverage: false +files: + - test/**/*.test.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d7c23e --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# fastify-grammy + +![CI](https://github.com/blasdfaa/fastify-grammy/workflows/CI/badge.svg) +[![NPM version](https://img.shields.io/npm/v/fastify-grammy.svg?style=flat)](https://www.npmjs.com/package/fastify-grammy) + +## 🚧 In development + +This library is a work in progress and in active development. + +Supports Fastify versions `5.x` + +## Install + +``` +npm i fastify-grammy +``` + +## Usage + +Require `fastify-grammy` and register. + +```js +const fastify = require('fastify')() + +fastify.register(require('fastify-grammy'), { + token: 'your-tg-bot-token' +}) + +fastify.listen({ port: 3000 }) +``` + +## Acknowledgements + +## License + +Licensed under [MIT](./LICENSE).
diff --git a/errors.js b/errors.js new file mode 100644 index 0000000..4a7283c --- /dev/null +++ b/errors.js @@ -0,0 +1,20 @@ +const { createError } = require('@fastify/error') + +const MissingBotTokenError = createError( + 'FST_ERR_GRAMMY_MISSING_BOT_TOKEN', + 'Missing bot token' +) +const NameAlreadyRegisterError = createError( + 'FST_ERR_GRAMMY_NAME_ALREADY_REGISTER', + '\'%s\' instance name has already been registered' +) +const AlreadyRegisterError = createError( + 'FST_ERR_GRAMMY_ALREADY_REGISTER', + 'bot has already been registered' +) + +module.exports = { + MissingBotTokenError, + NameAlreadyRegisterError, + AlreadyRegisterError +} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..3c8797d --- /dev/null +++ b/index.d.ts @@ -0,0 +1,23 @@ +import { FastifyPluginCallback } from 'fastify' +import { Bot } from 'grammy' + +declare module 'fastify' { + export interface FastifyInstance { + grammy: Bot + } +} + +type FastifyGrammy = FastifyPluginCallback + +declare namespace fastifyGrammy { + export interface FastifyGrammyOptions { + token: string + name?: string + } + + export const fastifyGrammy: FastifyGrammy + export { FastifyGrammy as default } +} + +declare function fastifyGrammy (...params: Parameters): ReturnType +export = fastifyGrammy diff --git a/index.js b/index.js new file mode 100644 index 0000000..4245212 --- /dev/null +++ b/index.js @@ -0,0 +1,44 @@ +'use strict' + +const fp = require('fastify-plugin') +const { Bot } = require('grammy') +const { NameAlreadyRegisterError, AlreadyRegisterError, MissingBotTokenError } = require('./errors') + +function fastifyGrammy (fastify, options, next) { + const { name, token } = options + + if (!token) { + return next(new MissingBotTokenError()) + } + + const bot = new Bot(token) + + if (name) { + if (!fastify.grammy) { + fastify.decorate('grammy', {}) + } + + if (fastify.grammy[name]) { + return next(new NameAlreadyRegisterError(name)) + } + + fastify.grammy[name] = bot + } else { + if (fastify.grammy) { + return next(new AlreadyRegisterError()) + } else { + fastify.decorate('grammy', bot) + } + } + + fastify.addHook('onClose', () => bot.stop()) + + next() +} + +module.exports = fp(fastifyGrammy, { + fastify: '5.x', + name: 'fastify-grammy' +}) +module.exports.default = fastifyGrammy +module.exports.fastifyGrammy = fastifyGrammy diff --git a/package.json b/package.json new file mode 100644 index 0000000..3586eb5 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "fastify-grammy", + "version": "0.0.1", + "description": "", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "npm run lint && npm run unit", + "lint": "standard && npm run lint:typescript", + "lint:typescript": "ts-standard", + "test:unit": "tap", + "unit": "node --test", + "release": "npm run test && changelogen --release && npm publish && git push --follow-tags" + }, + "keywords": [], + "author": "", + "license": "MIT", + "types": "index.d.ts", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastify-plugin": "^4.0.0", + "grammy": "^1.31.0" + }, + "devDependencies": { + "@types/node": "^20.4.4", + "changelogen": "^0.5.7", + "fastify": "^5.0.0", + "fastify-tsconfig": "^2.0.0", + "standard": "^17.0.0", + "tap": "^19.2.5", + "ts-standard": "^12.0.1", + "tsd": "^0.30.4", + "typescript": "5.2.2" + }, + "tsd": { + "directory": "test" + } +} diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 0000000..41162e1 --- /dev/null +++ b/test/helper.js @@ -0,0 +1,18 @@ +const fp = require('fastify-plugin') +const fastifyGrammy = require('..') +const Fastify = require('fastify') +const assert = require('node:assert') + +async function register (t, options = {}) { + const app = Fastify() + t.after(() => app.close()) + + await app.register(fp(fastifyGrammy), options) + + const ready = await app.ready() + assert.ok(ready) + + return app +} + +module.exports = { register } diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..87d9ac7 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,113 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { Bot } = require('grammy') +const { register } = require('./helper') +const Fastify = require('fastify') +const fastifyGrammy = require('..') + +const BOT_TOKEN = 'BOT_TOKEN' + +test('should register the correct decorator', async t => { + const fastify = await register(t, { token: 'aaa' }) + + assert.ok(fastify.grammy) + assert.ok(fastify.grammy instanceof Bot) +}) + +test('should not throw if registered within different scopes (with and without named instances)', async t => { + const fastify = Fastify() + t.after(() => fastify.close()) + + await fastify.register(function scopeOne (instance, _, next) { + instance.register(fastifyGrammy, { + token: BOT_TOKEN + }) + + next() + }) + + await fastify.register(function scopeTwo (instance, _, next) { + instance.register(fastifyGrammy, { + token: BOT_TOKEN, + name: 'one' + }) + + instance.register(fastifyGrammy, { + token: BOT_TOKEN, + name: 'two' + }) + + next() + }) + + const ready = await fastify.ready() + assert.ok(ready) +}) + +test('should throw when trying to register multiple instances without giving a name', async (t) => { + const fastify = Fastify() + t.after(() => fastify.close()) + + await fastify.register(fastifyGrammy, { + token: BOT_TOKEN + }) + + await assert.rejects( + async () => await fastify.register(fastifyGrammy, { + token: BOT_TOKEN + }), + (err) => { + assert.ok(err) + assert.strictEqual(err.message, 'bot has already been registered') + return true + } + ) +}) + +test('Should throw when trying to register duplicate connection names', async (t) => { + const fastify = Fastify() + t.after(() => fastify.close()) + + const name = 'test' + + await fastify.register(fastifyGrammy, { + token: BOT_TOKEN, + name + }) + + await assert.rejects( + async () => await fastify.register(fastifyGrammy, { + token: BOT_TOKEN, + name + }), + (err) => { + assert.ok(err) + assert.strictEqual(err.message, `'${name}' instance name has already been registered`) + return true + } + ) +}) + +test('const result = await fastify.grammy namespace should exist', async (t) => { + const fastify = await register(t, { token: BOT_TOKEN }) + + assert.ok(fastify.grammy) + assert.ok(fastify.grammy.start) + assert.ok(fastify.grammy.on) + assert.ok(fastify.grammy.use) +}) + +test('const result = await fastify.grammy custom namespace should exist if a name is set', async (t) => { + const fastify = await register(t, { + token: BOT_TOKEN, + name: 'test' + }) + + assert.ok(fastify.grammy) + assert.ok(fastify.grammy.test) + assert.ok(fastify.grammy.test.start) + assert.ok(fastify.grammy.test.on) + assert.ok(fastify.grammy.test.use) +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fb5c357 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "fastify-tsconfig", + "compilerOptions": { + "noEmit": true + }, + "include": [ + "**/*.ts" + ] +}