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"
+ ]
+}