First build
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
61
.github/workflows/main.yml
vendored
61
.github/workflows/main.yml
vendored
@@ -6,39 +6,46 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-push-docker-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.x]
|
node-version: [20.x]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
uses: actions/setup-node@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
registry: ${{ env.REGISTRY }}
|
||||||
cache: npm
|
username: ${{ github.actor }}
|
||||||
- run: npm ci
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- run: npm run lint
|
- name: Setup Docker buildx
|
||||||
- run: npm run typecheck
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Extract Docker metadata
|
||||||
auto-merge:
|
id: meta
|
||||||
if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
|
uses: docker/metadata-action@v4
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-and-test
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Dependabot metadata
|
|
||||||
id: metadata
|
|
||||||
uses: dependabot/fetch-metadata@v2
|
|
||||||
with:
|
with:
|
||||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
- name: Auto-merge
|
- name: Build and Push Versioned Docker Image
|
||||||
if: steps.metadata.outputs.update-type != 'version-update:semver-major'
|
id: build-and-push
|
||||||
run: gh pr merge --auto --squash "$PR_URL"
|
uses: docker/build-push-action@v4
|
||||||
env:
|
if: ${{ github.ref != 'refs/heads/main' }}
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
with:
|
||||||
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
context: .
|
||||||
|
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
- name: Build and Push Latest Docker Image
|
||||||
|
id: build-and-push-latest
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
17
.prettierignore
Normal file
17
.prettierignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
build
|
||||||
|
coverage
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
.yarn
|
||||||
|
.github
|
||||||
|
.env*
|
||||||
|
.eslintrc.json
|
||||||
|
.gitignore
|
||||||
|
.yarnrc.yml
|
||||||
|
next-env.d.ts
|
||||||
|
next-env.d
|
||||||
|
package*
|
||||||
|
tsconfig.json
|
||||||
|
yarn.lock
|
||||||
|
next.config.js
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM node:lts-slim AS base
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
|
# Files required by npm install
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Bundle app source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Type check app
|
||||||
|
RUN npm run typecheck
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
# Files required by npm install
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install only production app dependencies
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
# Bundle app source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# Start the app
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["node", "--import", "tsx", "./src/main.ts"]
|
||||||
73
README.md
73
README.md
@@ -13,15 +13,15 @@ Bot starter template based on [grammY](https://grammy.dev/) bot framework.
|
|||||||
- Logger (powered by [pino](https://github.com/pinojs/pino))
|
- Logger (powered by [pino](https://github.com/pinojs/pino))
|
||||||
- Ultrafast and multi-runtime server (powered by [hono](https://github.com/honojs/hono))
|
- Ultrafast and multi-runtime server (powered by [hono](https://github.com/honojs/hono))
|
||||||
- Ready-to-use deployment setups:
|
- Ready-to-use deployment setups:
|
||||||
- [Docker](#docker-dockercom)
|
- [Docker](#docker-dockercom)
|
||||||
- [Vercel](#vercel-vercelcom)
|
- [Vercel](#vercel-vercelcom)
|
||||||
- Examples:
|
- Examples:
|
||||||
- grammY plugins:
|
- grammY plugins:
|
||||||
- [Conversations](#grammy-conversations-grammydevpluginsconversations)
|
- [Conversations](#grammy-conversations-grammydevpluginsconversations)
|
||||||
- Databases:
|
- Databases:
|
||||||
- [Prisma ORM](#prisma-orm-prismaio)
|
- [Prisma ORM](#prisma-orm-prismaio)
|
||||||
- Runtimes:
|
- Runtimes:
|
||||||
- [Bun](#bun-bunsh)
|
- [Bun](#bun-bunsh)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -29,46 +29,49 @@ Follow these steps to set up and run your bot using this template:
|
|||||||
|
|
||||||
1. **Create a New Repository**
|
1. **Create a New Repository**
|
||||||
|
|
||||||
Start by creating a new repository using this template. You can do this by clicking [here](https://github.com/bot-base/telegram-bot-template/generate).
|
Start by creating a new repository using this template. You can do this by clicking [here](https://github.com/bot-base/telegram-bot-template/generate).
|
||||||
|
|
||||||
2. **Environment Variables Setup**
|
2. **Environment Variables Setup**
|
||||||
|
|
||||||
Create an environment variables file by copying the provided example file:
|
Create an environment variables file by copying the provided example file:
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
```bash
|
||||||
```
|
# development
|
||||||
Open the newly created `.env` file and set the `BOT_TOKEN` environment variable.
|
cp .env.example .env.bot.dev
|
||||||
|
|
||||||
|
# production
|
||||||
|
cp .env.example .env.bot.prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the newly created `.env.bot.dev` and `.env.bot.prod` files and set the `BOT_TOKEN` environment variable.
|
||||||
|
|
||||||
3. **Launching the Bot**
|
3. **Launching the Bot**
|
||||||
|
|
||||||
You can run your bot in both development and production modes.
|
You can run your bot in both development and production modes.
|
||||||
|
|
||||||
**Development Mode:**
|
**Development Mode:**
|
||||||
|
|
||||||
Install the required dependencies:
|
Install the required dependencies:
|
||||||
```bash
|
|
||||||
npm install
|
```bash
|
||||||
```
|
npm install
|
||||||
Start the bot in watch mode (auto-reload when code changes):
|
```
|
||||||
```bash
|
|
||||||
npm run dev
|
Start the bot in watch mode (auto-reload when code changes):
|
||||||
```
|
|
||||||
|
```bash
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
**Production Mode:**
|
**Production Mode:**
|
||||||
|
|
||||||
Install only production dependencies:
|
Set `DEBUG` environment variable to `false` in your `.env` file.
|
||||||
```bash
|
|
||||||
npm install --only=prod
|
|
||||||
```
|
|
||||||
|
|
||||||
Set `DEBUG` environment variable to `false` in your `.env` file.
|
Start the bot in production mode:
|
||||||
|
|
||||||
Start the bot in production mode:
|
```bash
|
||||||
```bash
|
docker compose -f compose.yml -f compose.prod.yml up
|
||||||
npm run start:force # skip type checking and start
|
```
|
||||||
# or
|
|
||||||
npm start # with type checking (requires development dependencies)
|
|
||||||
```
|
|
||||||
|
|
||||||
### List of Available Commands
|
### List of Available Commands
|
||||||
|
|
||||||
|
|||||||
9
compose.override.yml
Normal file
9
compose.override.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
volumes:
|
||||||
|
- ".:/usr/src"
|
||||||
|
env_file:
|
||||||
|
- .env.bot.dev
|
||||||
|
command: npm run dev
|
||||||
4
compose.prod.yml
Normal file
4
compose.prod.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
env_file:
|
||||||
|
- .env.bot.prod
|
||||||
4
compose.yml
Normal file
4
compose.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
@@ -1,5 +1,25 @@
|
|||||||
import antfu from '@antfu/eslint-config'
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default antfu({
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
|
export default [
|
||||||
})
|
{ files: ["**/*.{js,mjs,cjs,ts}"] },
|
||||||
|
{ languageOptions: { globals: globals.browser } },
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
arrays: "never",
|
||||||
|
objects: "never",
|
||||||
|
imports: "never",
|
||||||
|
exports: "never",
|
||||||
|
functions: "never"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
6652
package-lock.json
generated
6652
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -3,6 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447",
|
||||||
"description": "Telegram bot starter template",
|
"description": "Telegram bot starter template",
|
||||||
"imports": {
|
"imports": {
|
||||||
"#root/*": "./build/src/*"
|
"#root/*": "./build/src/*"
|
||||||
@@ -21,7 +22,8 @@
|
|||||||
"dev": "tsc-watch --onSuccess \"tsx ./src/main.ts\"",
|
"dev": "tsc-watch --onSuccess \"tsx ./src/main.ts\"",
|
||||||
"start": "tsc && tsx ./src/main.ts",
|
"start": "tsc && tsx ./src/main.ts",
|
||||||
"start:force": "tsx ./src/main.ts",
|
"start:force": "tsx ./src/main.ts",
|
||||||
"prepare": "husky || true"
|
"prepare": "husky || true",
|
||||||
|
"pretty": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grammyjs/auto-chat-action": "0.1.1",
|
"@grammyjs/auto-chat-action": "0.1.1",
|
||||||
@@ -43,12 +45,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "4.3.0",
|
"@antfu/eslint-config": "4.3.0",
|
||||||
|
"@eslint/js": "^9.20.0",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.4",
|
||||||
"eslint": "^9.20.1",
|
"eslint": "^9.20.1",
|
||||||
|
"globals": "^15.15.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.4.3",
|
"lint-staged": "^15.4.3",
|
||||||
|
"prettier": "^3.5.1",
|
||||||
"tsc-watch": "^6.2.1",
|
"tsc-watch": "^6.2.1",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.24.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.ts": "eslint"
|
"*.ts": "eslint"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createCallbackData } from 'callback-data'
|
import { createCallbackData } from "callback-data";
|
||||||
|
|
||||||
export const changeLanguageData = createCallbackData('language', {
|
export const changeLanguageData = createCallbackData("language", {
|
||||||
code: String,
|
code: String
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import type { Config } from '#root/config.js'
|
import type { Config } from "#root/config.js";
|
||||||
import type { Logger } from '#root/logger.js'
|
import type { Logger } from "#root/logger.js";
|
||||||
import type { AutoChatActionFlavor } from '@grammyjs/auto-chat-action'
|
import type { AutoChatActionFlavor } from "@grammyjs/auto-chat-action";
|
||||||
import type { HydrateFlavor } from '@grammyjs/hydrate'
|
import type { HydrateFlavor } from "@grammyjs/hydrate";
|
||||||
import type { I18nFlavor } from '@grammyjs/i18n'
|
import type { I18nFlavor } from "@grammyjs/i18n";
|
||||||
import type { ParseModeFlavor } from '@grammyjs/parse-mode'
|
import type { ParseModeFlavor } from "@grammyjs/parse-mode";
|
||||||
import type { Context as DefaultContext, SessionFlavor } from 'grammy'
|
import type { Context as DefaultContext, SessionFlavor } from "grammy";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
export interface SessionData {
|
export interface SessionData {
|
||||||
// field?: string;
|
// field?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtendedContextFlavor {
|
interface ExtendedContextFlavor {
|
||||||
logger: Logger
|
logger: Logger;
|
||||||
config: Config
|
config: Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Context = ParseModeFlavor<
|
export type Context = ParseModeFlavor<
|
||||||
HydrateFlavor<
|
HydrateFlavor<
|
||||||
DefaultContext &
|
DefaultContext &
|
||||||
ExtendedContextFlavor &
|
ExtendedContextFlavor &
|
||||||
SessionFlavor<SessionData> &
|
SessionFlavor<SessionData> &
|
||||||
I18nFlavor &
|
I18nFlavor &
|
||||||
AutoChatActionFlavor
|
AutoChatActionFlavor
|
||||||
>
|
>
|
||||||
>
|
>;
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import { chatAction } from "@grammyjs/auto-chat-action";
|
||||||
import { isAdmin } from '#root/bot/filters/is-admin.js'
|
import { Composer } from "grammy";
|
||||||
import { setCommandsHandler } from '#root/bot/handlers/commands/setcommands.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
import { isAdmin } from "#root/bot/filters/is-admin.js";
|
||||||
import { chatAction } from '@grammyjs/auto-chat-action'
|
import { setCommandsHandler } from "#root/bot/handlers/commands/setcommands.js";
|
||||||
import { Composer } from 'grammy'
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
const composer = new Composer<Context>()
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
const feature = composer
|
const feature = composer
|
||||||
.chatType('private')
|
.chatType("private")
|
||||||
.filter(isAdmin)
|
.filter(ctx => isAdmin(ctx.config.botAdmins)(ctx));
|
||||||
|
|
||||||
feature.command(
|
feature.command(
|
||||||
'setcommands',
|
"setcommands",
|
||||||
logHandle('command-setcommands'),
|
logHandle("command-setcommands"),
|
||||||
chatAction('typing'),
|
chatAction("typing"),
|
||||||
setCommandsHandler,
|
setCommandsHandler
|
||||||
)
|
);
|
||||||
|
|
||||||
export { composer as adminFeature }
|
export { composer as adminFeature };
|
||||||
|
|||||||
49
src/bot/features/blacklistDelete.ts
Normal file
49
src/bot/features/blacklistDelete.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Composer } from "grammy";
|
||||||
|
import type { Context } from "#root/bot/context.js";
|
||||||
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
|
const feature = composer.chatType(["group", "supergroup"]);
|
||||||
|
|
||||||
|
const GROUP_IDS = process.env.GROUP_IDS
|
||||||
|
? process.env.GROUP_IDS.split(",")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
feature.hears(
|
||||||
|
/(x.com|twitter.com)/g,
|
||||||
|
logHandle("blacklist-detection"),
|
||||||
|
async (ctx: Context) => {
|
||||||
|
if (ctx.chat && ctx.msg) {
|
||||||
|
if (GROUP_IDS !== undefined) {
|
||||||
|
const groupID = ctx.chat.id;
|
||||||
|
const flag = GROUP_IDS.includes(`${groupID}`);
|
||||||
|
const username = ctx.msg.from?.username;
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
ctx.msg.delete();
|
||||||
|
await ctx.reply(
|
||||||
|
ctx.t(
|
||||||
|
`@${username} Twitter and X links are not allowed here\\. Please consider sharing the media directly or from other social media sources or websites\\. No administration action was taken against you other than the message being deleted\\.`
|
||||||
|
),
|
||||||
|
{ parse_mode: "MarkdownV2" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GROUP_IDS) {
|
||||||
|
await ctx.reply(
|
||||||
|
ctx.t(
|
||||||
|
`There was a problem retrieving the whitelist. Check the env variables and try again\\.`
|
||||||
|
),
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { composer as blacklistDetection };
|
||||||
32
src/bot/features/botInfoCommand.ts
Normal file
32
src/bot/features/botInfoCommand.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Composer } from "grammy";
|
||||||
|
import type { Context } from "#root/bot/context.js";
|
||||||
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
|
const feature = composer.chatType(["group", "supergroup", "private"]);
|
||||||
|
|
||||||
|
// const GROUP_IDS = process.env.GROUP_IDS
|
||||||
|
// ? process.env.GROUP_IDS.split(",")
|
||||||
|
// : undefined;
|
||||||
|
|
||||||
|
feature.hears(
|
||||||
|
"/botInfo",
|
||||||
|
logHandle("bot-info-command"),
|
||||||
|
async (ctx: Context) => {
|
||||||
|
console.info(
|
||||||
|
"BOT INFO! BOT INFO! BOT INFO! BOT INFO! BOT INFO! BOT INFO! BOT INFO! BOT INFO!"
|
||||||
|
);
|
||||||
|
if (ctx.chat && ctx.msg) {
|
||||||
|
await ctx.reply(
|
||||||
|
`I am a bot designed to delete any Twitter/X links and reformatting services within groups\\. By default I only work with whitelisted group IDs\\.\n\nYou can fork me from this link: https://github\\.com/LucidCreationsMedia/No\\-Twitter\\-Bot and deploy me for use in your own groups\\!`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { composer as botInfoCommand };
|
||||||
32
src/bot/features/getGroupIDCommand.ts
Normal file
32
src/bot/features/getGroupIDCommand.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Composer } from "grammy";
|
||||||
|
import type { Context } from "#root/bot/context.js";
|
||||||
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
|
const feature = composer.chatType(["group", "supergroup"]);
|
||||||
|
|
||||||
|
const GROUP_IDS = process.env.GROUP_IDS
|
||||||
|
? process.env.GROUP_IDS.split(",")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
feature.hears(
|
||||||
|
"/getGroupID",
|
||||||
|
logHandle("get-group-id"),
|
||||||
|
async (ctx: Context) => {
|
||||||
|
if (ctx.chat && ctx.msg) {
|
||||||
|
if (GROUP_IDS !== undefined) {
|
||||||
|
const groupID = ctx.chat.id;
|
||||||
|
const flag = GROUP_IDS.includes(`${groupID}`);
|
||||||
|
if (flag) {
|
||||||
|
await ctx.reply(`The group id is: \`${groupID}\``, {
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { composer as getGroupIDCommand };
|
||||||
64
src/bot/features/helpCommand.ts
Normal file
64
src/bot/features/helpCommand.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Composer } from "grammy";
|
||||||
|
import type { Context } from "#root/bot/context.js";
|
||||||
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
|
const feature = composer.chatType(["group", "supergroup"]);
|
||||||
|
|
||||||
|
const GROUP_IDS = process.env.GROUP_IDS
|
||||||
|
? process.env.GROUP_IDS.split(",")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
feature.hears(
|
||||||
|
"/help",
|
||||||
|
logHandle("blacklist-detection"),
|
||||||
|
async (ctx: Context) => {
|
||||||
|
if (ctx.chat && ctx.msg) {
|
||||||
|
if (GROUP_IDS !== undefined) {
|
||||||
|
const groupID = ctx.chat.id;
|
||||||
|
const flag = GROUP_IDS.includes(`${groupID}`);
|
||||||
|
// const username = ctx.msg.from?.username;
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
await ctx.reply(
|
||||||
|
`**Here are the availible commands you can use:**\n\n/getGroupID \\- replied with the ID of the group I am in\\.\n\n/isLCMGRoup \\- Checks if this group's ID is on the whitelist and responds accordingly\\.\n\n/botInfo \\- Info about me and how to fork me to deploy for your own use\\.\n\n/help \\- Displays this help message\\.`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flag) {
|
||||||
|
await ctx.reply(
|
||||||
|
`**Since this is not a whitelisted group the features are limited\\!\\!**\n\nHere are the availible commands you can use:\n\n/isLCMGRoup \\- Checks if this group's ID is on the whitelist and responds accordingly\\.\n\n/botInfo \\- Info about me and how to fork me to deploy for your own use\\.\n\n/help \\- Displays this help message\\.`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await ctx.reply(
|
||||||
|
`This group is NOT in the whitelisted and is NOT a part of the LCM Telegram groups/communities\\. I am a bot designed to delete any Twitter/X links and reformatting services within groups\\. You can fork me from this link: https://github\\.com/LucidCreationsMedia/No\\-Twitter\\-Bot and deploy me for use in your own groups\\!`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GROUP_IDS) {
|
||||||
|
await ctx.reply(
|
||||||
|
`There was a problem retrieving the whitelist. Check the env variables and try again\\.`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { composer as helpCommand };
|
||||||
57
src/bot/features/isLCMGroup.ts
Normal file
57
src/bot/features/isLCMGroup.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Composer } from "grammy";
|
||||||
|
import type { Context } from "#root/bot/context.js";
|
||||||
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
|
const feature = composer.chatType(["group", "supergroup"]);
|
||||||
|
|
||||||
|
const GROUP_IDS = process.env.GROUP_IDS
|
||||||
|
? process.env.GROUP_IDS.split(",")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
feature.hears(
|
||||||
|
"/isLCMGroup",
|
||||||
|
logHandle("is-LCM-group"),
|
||||||
|
async (ctx: Context) => {
|
||||||
|
if (ctx.chat && ctx.msg) {
|
||||||
|
const groupID = ctx.chat.id;
|
||||||
|
|
||||||
|
if (GROUP_IDS !== undefined) {
|
||||||
|
const flag = GROUP_IDS.includes(`${groupID}`);
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
await ctx.reply(
|
||||||
|
`This group is in the whitelisted and is a part of the LCM Telegram groups/communities\\. I should be deleting any Twitter/X links and reformatting services within this group\\.`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flag) {
|
||||||
|
await ctx.reply(
|
||||||
|
`This group is NOT in the whitelisted and is NOT a part of the LCM Telegram groups/communities\\. I am a bot designed to delete any Twitter/X links and reformatting services within groups\\. You can fork me from this link: https://github.com/LucidCreationsMedia/No-Twitter-Bot and deploy me for use in your own groups!`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GROUP_IDS) {
|
||||||
|
await ctx.reply(
|
||||||
|
`There was a problem retrieving the whitelist. Check the env variables and try again\\.`,
|
||||||
|
{
|
||||||
|
parse_mode: "MarkdownV2",
|
||||||
|
reply_parameters: { message_id: ctx.msg.message_id }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { composer as isLCMGroup };
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import { Composer } from "grammy";
|
||||||
import { changeLanguageData } from '#root/bot/callback-data/change-language.js'
|
import { changeLanguageData } from "#root/bot/callback-data/change-language.js";
|
||||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import { i18n } from '#root/bot/i18n.js'
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
import { createChangeLanguageKeyboard } from '#root/bot/keyboards/change-language.js'
|
import { i18n } from "#root/bot/i18n.js";
|
||||||
import { Composer } from 'grammy'
|
import { createChangeLanguageKeyboard } from "#root/bot/keyboards/change-language.js";
|
||||||
|
|
||||||
const composer = new Composer<Context>()
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
const feature = composer.chatType('private')
|
const feature = composer.chatType("private");
|
||||||
|
|
||||||
feature.command('language', logHandle('command-language'), async (ctx) => {
|
feature.command("language", logHandle("command-language"), async ctx => {
|
||||||
return ctx.reply(ctx.t('language-select'), {
|
return ctx.reply(ctx.t("language-select"), {
|
||||||
reply_markup: await createChangeLanguageKeyboard(ctx),
|
reply_markup: await createChangeLanguageKeyboard(ctx)
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
feature.callbackQuery(
|
feature.callbackQuery(
|
||||||
changeLanguageData.filter(),
|
changeLanguageData.filter(),
|
||||||
logHandle('keyboard-language-select'),
|
logHandle("keyboard-language-select"),
|
||||||
async (ctx) => {
|
async ctx => {
|
||||||
const { code: languageCode } = changeLanguageData.unpack(
|
const { code: languageCode } = changeLanguageData.unpack(
|
||||||
ctx.callbackQuery.data,
|
ctx.callbackQuery.data
|
||||||
)
|
);
|
||||||
|
|
||||||
if (i18n.locales.includes(languageCode)) {
|
if (i18n.locales.includes(languageCode)) {
|
||||||
await ctx.i18n.setLocale(languageCode)
|
await ctx.i18n.setLocale(languageCode);
|
||||||
|
|
||||||
return ctx.editMessageText(ctx.t('language-changed'), {
|
return ctx.editMessageText(ctx.t("language-changed"), {
|
||||||
reply_markup: await createChangeLanguageKeyboard(ctx),
|
reply_markup: await createChangeLanguageKeyboard(ctx)
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export { composer as languageFeature }
|
export { composer as languageFeature };
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import { Composer } from "grammy";
|
||||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import { Composer } from 'grammy'
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
const composer = new Composer<Context>()
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
const feature = composer.chatType('private')
|
const feature = composer.chatType("private");
|
||||||
|
|
||||||
feature.on('message', logHandle('unhandled-message'), (ctx) => {
|
feature.on("message", logHandle("unhandled-message"), ctx => {
|
||||||
return ctx.reply(ctx.t('unhandled'))
|
return ctx.reply(ctx.t("unhandled"));
|
||||||
})
|
});
|
||||||
|
|
||||||
feature.on('callback_query', logHandle('unhandled-callback-query'), (ctx) => {
|
feature.on("callback_query", logHandle("unhandled-callback-query"), ctx => {
|
||||||
return ctx.answerCallbackQuery()
|
return ctx.answerCallbackQuery();
|
||||||
})
|
});
|
||||||
|
|
||||||
export { composer as unhandledFeature }
|
export { composer as unhandledFeature };
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import { Composer } from "grammy";
|
||||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import { Composer } from 'grammy'
|
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
const composer = new Composer<Context>()
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
const feature = composer.chatType('private')
|
const feature = composer.chatType("private");
|
||||||
|
|
||||||
feature.command('start', logHandle('command-start'), (ctx) => {
|
feature.command("start", logHandle("command-start"), ctx => {
|
||||||
return ctx.reply(ctx.t('welcome'))
|
return ctx.reply(
|
||||||
})
|
ctx.t(
|
||||||
|
"Welcome! I am a bot created by Lucid for Lucid Creations media groups. I am designed to delete any Twitter/X links and reformatting services within groups. By default I only work with whitelisted group IDs. You can fork me from this link: https://github.com/LucidCreationsMedia/No-Twitter-Bot and deploy me for use in your own groups!"
|
||||||
|
),
|
||||||
|
{ parse_mode: "MarkdownV2" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export { composer as welcomeFeature }
|
export { composer as welcomeFeature };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
|
|
||||||
export function isAdmin(ctx: Context) {
|
export function isAdmin(ctx: Context) {
|
||||||
return !!ctx.from && ctx.config.botAdmins.includes(ctx.from.id)
|
return !!ctx.from && ctx.config.botAdmins.includes(ctx.from.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,53 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import type { LanguageCode } from '@grammyjs/types'
|
import type { LanguageCode } from "@grammyjs/types";
|
||||||
import type { CommandContext } from 'grammy'
|
import type { CommandContext } from "grammy";
|
||||||
import { i18n } from '#root/bot/i18n.js'
|
import { i18n } from "#root/bot/i18n.js";
|
||||||
import { Command, CommandGroup } from '@grammyjs/commands'
|
import { Command, CommandGroup } from "@grammyjs/commands";
|
||||||
|
|
||||||
function addCommandLocalizations(command: Command) {
|
function addCommandLocalizations(command: Command) {
|
||||||
i18n.locales.forEach((locale) => {
|
i18n.locales.forEach(locale => {
|
||||||
command.localize(
|
command.localize(
|
||||||
locale as LanguageCode,
|
locale as LanguageCode,
|
||||||
command.name,
|
command.name,
|
||||||
i18n.t(locale, `${command.name}.description`),
|
i18n.t(locale, `${command.name}.description`)
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
return command
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCommandToChats(command: Command, chats: number[]) {
|
function addCommandToChats(command: Command, chats: number[]) {
|
||||||
for (const chatId of chats) {
|
for (const chatId of chats) {
|
||||||
command.addToScope({
|
command.addToScope({
|
||||||
type: 'chat',
|
type: "chat",
|
||||||
chat_id: chatId,
|
chat_id: chatId
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setCommandsHandler(ctx: CommandContext<Context>) {
|
export async function setCommandsHandler(ctx: CommandContext<Context>) {
|
||||||
const start = new Command('start', i18n.t('en', 'start.description'))
|
const start = new Command(
|
||||||
.addToScope({ type: 'all_private_chats' })
|
"start",
|
||||||
addCommandLocalizations(start)
|
i18n.t("en", "start.description")
|
||||||
addCommandToChats(start, ctx.config.botAdmins)
|
).addToScope({ type: "all_private_chats" });
|
||||||
|
addCommandLocalizations(start);
|
||||||
|
addCommandToChats(start, ctx.config.botAdmins);
|
||||||
|
|
||||||
const language = new Command('language', i18n.t('en', 'language.description'))
|
const language = new Command(
|
||||||
.addToScope({ type: 'all_private_chats' })
|
"language",
|
||||||
addCommandLocalizations(language)
|
i18n.t("en", "language.description")
|
||||||
addCommandToChats(language, ctx.config.botAdmins)
|
).addToScope({ type: "all_private_chats" });
|
||||||
|
addCommandLocalizations(language);
|
||||||
|
addCommandToChats(language, ctx.config.botAdmins);
|
||||||
|
|
||||||
const setcommands = new Command('setcommands', i18n.t('en', 'setcommands.description'))
|
const setcommands = new Command(
|
||||||
addCommandToChats(setcommands, ctx.config.botAdmins)
|
"setcommands",
|
||||||
|
i18n.t("en", "setcommands.description")
|
||||||
|
);
|
||||||
|
addCommandToChats(setcommands, ctx.config.botAdmins);
|
||||||
|
|
||||||
const commands = new CommandGroup()
|
const commands = new CommandGroup().add(start).add(language).add(setcommands);
|
||||||
.add(start)
|
|
||||||
.add(language)
|
|
||||||
.add(setcommands)
|
|
||||||
|
|
||||||
await commands.setCommands(ctx)
|
await commands.setCommands(ctx);
|
||||||
|
|
||||||
return ctx.reply(ctx.t('admin-commands-updated'))
|
return ctx.reply(ctx.t("admin-commands-updated"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import type { ErrorHandler } from 'grammy'
|
import type { ErrorHandler } from "grammy";
|
||||||
import { getUpdateInfo } from '#root/bot/helpers/logging.js'
|
import { getUpdateInfo } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
export const errorHandler: ErrorHandler<Context> = (error) => {
|
export const errorHandler: ErrorHandler<Context> = error => {
|
||||||
const { ctx } = error
|
const { ctx } = error;
|
||||||
|
|
||||||
ctx.logger.error({
|
ctx.logger.error({
|
||||||
err: error.error,
|
err: error.error,
|
||||||
update: getUpdateInfo(ctx),
|
update: getUpdateInfo(ctx)
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export function chunk<T>(array: T[], size: number) {
|
export function chunk<T>(array: T[], size: number) {
|
||||||
const result = []
|
const result = [];
|
||||||
for (let index = 0; index < array.length; index += size)
|
for (let index = 0; index < array.length; index += size)
|
||||||
result.push(array.slice(index, index + size))
|
result.push(array.slice(index, index + size));
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import type { Update } from '@grammyjs/types'
|
import type { Update } from "@grammyjs/types";
|
||||||
import type { Middleware } from 'grammy'
|
import type { Middleware } from "grammy";
|
||||||
|
|
||||||
export function getUpdateInfo(ctx: Context): Omit<Update, 'update_id'> {
|
export function getUpdateInfo(ctx: Context): Omit<Update, "update_id"> {
|
||||||
const { update_id, ...update } = ctx.update
|
const { /*update_id,*/ ...update } = ctx.update;
|
||||||
|
|
||||||
return update
|
return update;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logHandle(id: string): Middleware<Context> {
|
export function logHandle(id: string): Middleware<Context> {
|
||||||
return (ctx, next) => {
|
return (ctx, next) => {
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
msg: `Handle "${id}"`,
|
msg: `Handle "${id}"`,
|
||||||
...(id.startsWith('unhandled') ? { update: getUpdateInfo(ctx) } : {}),
|
...(id.startsWith("unhandled") ? { update: getUpdateInfo(ctx) } : {})
|
||||||
})
|
});
|
||||||
|
|
||||||
return next()
|
return next();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import path from 'node:path'
|
import path from "node:path";
|
||||||
import process from 'node:process'
|
import process from "node:process";
|
||||||
import { I18n } from '@grammyjs/i18n'
|
import { I18n } from "@grammyjs/i18n";
|
||||||
|
|
||||||
export const i18n = new I18n<Context>({
|
export const i18n = new I18n<Context>({
|
||||||
defaultLocale: 'en',
|
defaultLocale: "en",
|
||||||
directory: path.resolve(process.cwd(), 'locales'),
|
directory: path.resolve(process.cwd(), "locales"),
|
||||||
useSession: true,
|
useSession: true,
|
||||||
fluentBundleOptions: {
|
fluentBundleOptions: {
|
||||||
useIsolating: false,
|
useIsolating: false
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
export const isMultipleLocales = i18n.locales.length > 1
|
export const isMultipleLocales = i18n.locales.length > 1;
|
||||||
|
|||||||
120
src/bot/index.ts
120
src/bot/index.ts
@@ -1,75 +1,89 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import type { Config } from '#root/config.js'
|
import type { Config } from "#root/config.js";
|
||||||
import type { Logger } from '#root/logger.js'
|
import type { Logger } from "#root/logger.js";
|
||||||
import type { BotConfig } from 'grammy'
|
import type { BotConfig } from "grammy";
|
||||||
import { adminFeature } from '#root/bot/features/admin.js'
|
import { adminFeature } from "#root/bot/features/admin.js";
|
||||||
import { languageFeature } from '#root/bot/features/language.js'
|
import { languageFeature } from "#root/bot/features/language.js";
|
||||||
import { unhandledFeature } from '#root/bot/features/unhandled.js'
|
import { unhandledFeature } from "#root/bot/features/unhandled.js";
|
||||||
import { welcomeFeature } from '#root/bot/features/welcome.js'
|
import { welcomeFeature } from "#root/bot/features/welcome.js";
|
||||||
import { errorHandler } from '#root/bot/handlers/error.js'
|
import { errorHandler } from "#root/bot/handlers/error.js";
|
||||||
import { i18n, isMultipleLocales } from '#root/bot/i18n.js'
|
import { i18n, isMultipleLocales } from "#root/bot/i18n.js";
|
||||||
import { session } from '#root/bot/middlewares/session.js'
|
import { session } from "#root/bot/middlewares/session.js";
|
||||||
import { updateLogger } from '#root/bot/middlewares/update-logger.js'
|
import { updateLogger } from "#root/bot/middlewares/update-logger.js";
|
||||||
import { autoChatAction } from '@grammyjs/auto-chat-action'
|
import { autoChatAction } from "@grammyjs/auto-chat-action";
|
||||||
import { hydrate } from '@grammyjs/hydrate'
|
import { hydrate } from "@grammyjs/hydrate";
|
||||||
import { hydrateReply, parseMode } from '@grammyjs/parse-mode'
|
import { hydrateReply, parseMode } from "@grammyjs/parse-mode";
|
||||||
import { sequentialize } from '@grammyjs/runner'
|
import { sequentialize } from "@grammyjs/runner";
|
||||||
import { MemorySessionStorage, Bot as TelegramBot } from 'grammy'
|
import { MemorySessionStorage, Bot as TelegramBot } from "grammy";
|
||||||
|
import { blacklistDetection } from "./features/blacklistDelete.js";
|
||||||
|
import { botInfoCommand } from "./features/botInfoCommand.js";
|
||||||
|
import { getGroupIDCommand } from "./features/getGroupIDCommand.js";
|
||||||
|
import { helpCommand } from "./features/helpCommand.js";
|
||||||
|
import { isLCMGroup } from "./features/isLCMGroup.js";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
config: Config
|
config: Config;
|
||||||
logger: Logger
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionKey(ctx: Omit<Context, 'session'>) {
|
function getSessionKey(ctx: Omit<Context, "session">) {
|
||||||
return ctx.chat?.id.toString()
|
return ctx.chat?.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBot(token: string, dependencies: Dependencies, botConfig?: BotConfig<Context>) {
|
export function createBot(
|
||||||
const {
|
token: string,
|
||||||
config,
|
dependencies: Dependencies,
|
||||||
logger,
|
botConfig?: BotConfig<Context>
|
||||||
} = dependencies
|
) {
|
||||||
|
const { config, logger } = dependencies;
|
||||||
|
|
||||||
const bot = new TelegramBot<Context>(token, botConfig)
|
const bot = new TelegramBot<Context>(token, botConfig);
|
||||||
|
|
||||||
bot.use(async (ctx, next) => {
|
bot.use(async (ctx, next) => {
|
||||||
ctx.config = config
|
ctx.config = config;
|
||||||
ctx.logger = logger.child({
|
ctx.logger = logger.child({
|
||||||
update_id: ctx.update.update_id,
|
update_id: ctx.update.update_id
|
||||||
})
|
});
|
||||||
|
|
||||||
await next()
|
await next();
|
||||||
})
|
});
|
||||||
|
|
||||||
const protectedBot = bot.errorBoundary(errorHandler)
|
const protectedBot = bot.errorBoundary(errorHandler);
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
bot.api.config.use(parseMode('HTML'))
|
bot.api.config.use(parseMode("HTML"));
|
||||||
|
|
||||||
if (config.isPollingMode)
|
if (config.isPollingMode) protectedBot.use(sequentialize(getSessionKey));
|
||||||
protectedBot.use(sequentialize(getSessionKey))
|
if (config.isDebug) protectedBot.use(updateLogger());
|
||||||
if (config.isDebug)
|
protectedBot.use(autoChatAction(bot.api));
|
||||||
protectedBot.use(updateLogger())
|
protectedBot.use(hydrateReply);
|
||||||
protectedBot.use(autoChatAction(bot.api))
|
protectedBot.use(hydrate());
|
||||||
protectedBot.use(hydrateReply)
|
protectedBot.use(
|
||||||
protectedBot.use(hydrate())
|
session({
|
||||||
protectedBot.use(session({
|
getSessionKey,
|
||||||
getSessionKey,
|
storage: new MemorySessionStorage()
|
||||||
storage: new MemorySessionStorage(),
|
})
|
||||||
}))
|
);
|
||||||
protectedBot.use(i18n)
|
protectedBot.use(i18n);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
protectedBot.use(welcomeFeature)
|
protectedBot.use(welcomeFeature);
|
||||||
protectedBot.use(adminFeature)
|
protectedBot.use(adminFeature);
|
||||||
if (isMultipleLocales)
|
if (isMultipleLocales) protectedBot.use(languageFeature);
|
||||||
protectedBot.use(languageFeature)
|
|
||||||
|
// Commands
|
||||||
|
protectedBot.use(botInfoCommand);
|
||||||
|
protectedBot.use(getGroupIDCommand);
|
||||||
|
protectedBot.use(isLCMGroup);
|
||||||
|
protectedBot.use(helpCommand);
|
||||||
|
|
||||||
|
// Blacklist Feature
|
||||||
|
protectedBot.use(blacklistDetection);
|
||||||
|
|
||||||
// must be the last handler
|
// must be the last handler
|
||||||
protectedBot.use(unhandledFeature)
|
protectedBot.use(unhandledFeature);
|
||||||
|
|
||||||
return bot
|
return bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Bot = ReturnType<typeof createBot>
|
export type Bot = ReturnType<typeof createBot>;
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import { changeLanguageData } from '#root/bot/callback-data/change-language.js'
|
import { changeLanguageData } from "#root/bot/callback-data/change-language.js";
|
||||||
import { chunk } from '#root/bot/helpers/keyboard.js'
|
import { chunk } from "#root/bot/helpers/keyboard.js";
|
||||||
import { i18n } from '#root/bot/i18n.js'
|
import { i18n } from "#root/bot/i18n.js";
|
||||||
import { InlineKeyboard } from 'grammy'
|
import { InlineKeyboard } from "grammy";
|
||||||
import ISO6391 from 'iso-639-1'
|
import ISO6391 from "iso-639-1";
|
||||||
|
|
||||||
export async function createChangeLanguageKeyboard(ctx: Context) {
|
export async function createChangeLanguageKeyboard(ctx: Context) {
|
||||||
const currentLocaleCode = await ctx.i18n.getLocale()
|
const currentLocaleCode = await ctx.i18n.getLocale();
|
||||||
|
|
||||||
const getLabel = (code: string) => {
|
const getLabel = (code: string) => {
|
||||||
const isActive = code === currentLocaleCode
|
const isActive = code === currentLocaleCode;
|
||||||
|
|
||||||
return `${isActive ? '✅ ' : ''}${ISO6391.getNativeName(code)}`
|
return `${isActive ? "✅ " : ""}${ISO6391.getNativeName(code)}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
return InlineKeyboard.from(
|
return InlineKeyboard.from(
|
||||||
chunk(
|
chunk(
|
||||||
i18n.locales.map(localeCode => ({
|
i18n.locales.map(localeCode => ({
|
||||||
text: getLabel(localeCode),
|
text: getLabel(localeCode),
|
||||||
callback_data: changeLanguageData.pack({
|
callback_data: changeLanguageData.pack({
|
||||||
code: localeCode,
|
code: localeCode
|
||||||
}),
|
})
|
||||||
})),
|
})),
|
||||||
2,
|
2
|
||||||
),
|
)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import type { Context, SessionData } from '#root/bot/context.js'
|
import type { Context, SessionData } from "#root/bot/context.js";
|
||||||
import type { Middleware, SessionOptions } from 'grammy'
|
import type { Middleware, SessionOptions } from "grammy";
|
||||||
import { session as createSession } from 'grammy'
|
import { session as createSession } from "grammy";
|
||||||
|
|
||||||
type Options = Pick<SessionOptions<SessionData, Context>, 'getSessionKey' | 'storage'>
|
type Options = Pick<
|
||||||
|
SessionOptions<SessionData, Context>,
|
||||||
|
"getSessionKey" | "storage"
|
||||||
|
>;
|
||||||
|
|
||||||
export function session(options: Options): Middleware<Context> {
|
export function session(options: Options): Middleware<Context> {
|
||||||
return createSession({
|
return createSession({
|
||||||
getSessionKey: options.getSessionKey,
|
getSessionKey: options.getSessionKey,
|
||||||
storage: options.storage,
|
storage: options.storage,
|
||||||
initial: () => ({}),
|
initial: () => ({})
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
import type { Context } from '#root/bot/context.js'
|
import type { Context } from "#root/bot/context.js";
|
||||||
import type { Middleware } from 'grammy'
|
import type { Middleware } from "grammy";
|
||||||
import { performance } from 'node:perf_hooks'
|
import { performance } from "node:perf_hooks";
|
||||||
import { getUpdateInfo } from '#root/bot/helpers/logging.js'
|
import { getUpdateInfo } from "#root/bot/helpers/logging.js";
|
||||||
|
|
||||||
export function updateLogger(): Middleware<Context> {
|
export function updateLogger(): Middleware<Context> {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
ctx.api.config.use((previous, method, payload, signal) => {
|
ctx.api.config.use((previous, method, payload, signal) => {
|
||||||
ctx.logger.debug({
|
ctx.logger.debug({
|
||||||
msg: 'Bot API call',
|
msg: "Bot API call",
|
||||||
method,
|
method,
|
||||||
payload,
|
payload
|
||||||
})
|
});
|
||||||
|
|
||||||
return previous(method, payload, signal)
|
return previous(method, payload, signal);
|
||||||
})
|
});
|
||||||
|
|
||||||
ctx.logger.debug({
|
ctx.logger.debug({
|
||||||
msg: 'Update received',
|
msg: "Update received",
|
||||||
update: getUpdateInfo(ctx),
|
update: getUpdateInfo(ctx)
|
||||||
})
|
});
|
||||||
|
|
||||||
const startTime = performance.now()
|
const startTime = performance.now();
|
||||||
try {
|
try {
|
||||||
await next()
|
await next();
|
||||||
}
|
} finally {
|
||||||
finally {
|
const endTime = performance.now();
|
||||||
const endTime = performance.now()
|
|
||||||
ctx.logger.debug({
|
ctx.logger.debug({
|
||||||
msg: 'Update processed',
|
msg: "Update processed",
|
||||||
elapsed: endTime - startTime,
|
elapsed: endTime - startTime
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/config.ts
118
src/config.ts
@@ -1,98 +1,124 @@
|
|||||||
import process from 'node:process'
|
import process from "node:process";
|
||||||
import { API_CONSTANTS } from 'grammy'
|
import { API_CONSTANTS } from "grammy";
|
||||||
import * as v from 'valibot'
|
import * as v from "valibot";
|
||||||
|
|
||||||
const baseConfigSchema = v.object({
|
const baseConfigSchema = v.object({
|
||||||
debug: v.optional(v.pipe(v.string(), v.transform(JSON.parse), v.boolean()), 'false'),
|
debug: v.optional(
|
||||||
logLevel: v.optional(v.pipe(v.string(), v.picklist(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent'])), 'info'),
|
v.pipe(v.string(), v.transform(JSON.parse), v.boolean()),
|
||||||
botToken: v.pipe(v.string(), v.regex(/^\d+:[\w-]+$/, 'Invalid token')),
|
"false"
|
||||||
botAllowedUpdates: v.optional(v.pipe(v.string(), v.transform(JSON.parse), v.array(v.picklist(API_CONSTANTS.ALL_UPDATE_TYPES))), '[]'),
|
),
|
||||||
botAdmins: v.optional(v.pipe(v.string(), v.transform(JSON.parse), v.array(v.number())), '[]'),
|
logLevel: v.optional(
|
||||||
})
|
v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.picklist(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
|
||||||
|
),
|
||||||
|
"info"
|
||||||
|
),
|
||||||
|
botToken: v.pipe(v.string(), v.regex(/^\d+:[\w-]+$/, "Invalid token")),
|
||||||
|
botAllowedUpdates: v.optional(
|
||||||
|
v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.transform(JSON.parse),
|
||||||
|
v.array(v.picklist(API_CONSTANTS.ALL_UPDATE_TYPES))
|
||||||
|
),
|
||||||
|
"[]"
|
||||||
|
),
|
||||||
|
botAdmins: v.optional(
|
||||||
|
v.pipe(v.string(), v.transform(JSON.parse), v.array(v.number())),
|
||||||
|
"[]"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
const configSchema = v.variant('botMode', [
|
const configSchema = v.variant("botMode", [
|
||||||
// polling config
|
// polling config
|
||||||
v.pipe(
|
v.pipe(
|
||||||
v.object({
|
v.object({
|
||||||
botMode: v.literal('polling'),
|
botMode: v.literal("polling"),
|
||||||
...baseConfigSchema.entries,
|
...baseConfigSchema.entries
|
||||||
}),
|
}),
|
||||||
v.transform(input => ({
|
v.transform(input => ({
|
||||||
...input,
|
...input,
|
||||||
isDebug: input.debug,
|
isDebug: input.debug,
|
||||||
isWebhookMode: false as const,
|
isWebhookMode: false as const,
|
||||||
isPollingMode: true as const,
|
isPollingMode: true as const
|
||||||
})),
|
}))
|
||||||
),
|
),
|
||||||
// webhook config
|
// webhook config
|
||||||
v.pipe(
|
v.pipe(
|
||||||
v.object({
|
v.object({
|
||||||
botMode: v.literal('webhook'),
|
botMode: v.literal("webhook"),
|
||||||
...baseConfigSchema.entries,
|
...baseConfigSchema.entries,
|
||||||
botWebhook: v.pipe(v.string(), v.url()),
|
botWebhook: v.pipe(v.string(), v.url()),
|
||||||
botWebhookSecret: v.pipe(v.string(), v.minLength(12)),
|
botWebhookSecret: v.pipe(v.string(), v.minLength(12)),
|
||||||
serverHost: v.optional(v.string(), '0.0.0.0'),
|
serverHost: v.optional(v.string(), "0.0.0.0"),
|
||||||
serverPort: v.optional(v.pipe(v.string(), v.transform(Number), v.number()), '80'),
|
serverPort: v.optional(
|
||||||
|
v.pipe(v.string(), v.transform(Number), v.number()),
|
||||||
|
"80"
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
v.transform(input => ({
|
v.transform(input => ({
|
||||||
...input,
|
...input,
|
||||||
isDebug: input.debug,
|
isDebug: input.debug,
|
||||||
isWebhookMode: true as const,
|
isWebhookMode: true as const,
|
||||||
isPollingMode: false as const,
|
isPollingMode: false as const
|
||||||
})),
|
}))
|
||||||
),
|
)
|
||||||
])
|
]);
|
||||||
|
|
||||||
export type Config = v.InferOutput<typeof configSchema>
|
export type Config = v.InferOutput<typeof configSchema>;
|
||||||
export type PollingConfig = v.InferOutput<typeof configSchema['options'][0]>
|
export type PollingConfig = v.InferOutput<(typeof configSchema)["options"][0]>;
|
||||||
export type WebhookConfig = v.InferOutput<typeof configSchema['options'][1]>
|
export type WebhookConfig = v.InferOutput<(typeof configSchema)["options"][1]>;
|
||||||
|
|
||||||
export function createConfig(input: v.InferInput<typeof configSchema>) {
|
export function createConfig(input: v.InferInput<typeof configSchema>) {
|
||||||
return v.parse(configSchema, input)
|
return v.parse(configSchema, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = createConfigFromEnvironment()
|
export const config = createConfigFromEnvironment();
|
||||||
|
|
||||||
function createConfigFromEnvironment() {
|
function createConfigFromEnvironment() {
|
||||||
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
|
type CamelCase<S extends string> =
|
||||||
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
|
S extends `${infer P1}_${infer P2}${infer P3}`
|
||||||
: Lowercase<S>
|
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
|
||||||
|
: Lowercase<S>;
|
||||||
|
|
||||||
type KeysToCamelCase<T> = {
|
type KeysToCamelCase<T> = {
|
||||||
[K in keyof T as CamelCase<string & K>]: T[K] extends object ? KeysToCamelCase<T[K]> : T[K]
|
[K in keyof T as CamelCase<string & K>]: T[K] extends object
|
||||||
}
|
? KeysToCamelCase<T[K]>
|
||||||
|
: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
function toCamelCase(str: string): string {
|
function toCamelCase(str: string): string {
|
||||||
return str.toLowerCase().replace(/_([a-z])/g, (_match, p1) => p1.toUpperCase())
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/_([a-z])/g, (_match, p1) => p1.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertKeysToCamelCase<T>(obj: T): KeysToCamelCase<T> {
|
function convertKeysToCamelCase<T>(obj: T): KeysToCamelCase<T> {
|
||||||
const result: any = {}
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const result: any = {};
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
const camelCaseKey = toCamelCase(key)
|
const camelCaseKey = toCamelCase(key);
|
||||||
result[camelCaseKey] = obj[key]
|
result[camelCaseKey] = obj[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.loadEnvFile()
|
process.loadEnvFile();
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
// No .env file found
|
// No .env file found
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error create config from environment variables
|
// @ts-expect-error create config from environment variables
|
||||||
const config = createConfig(convertKeysToCamelCase(process.env))
|
const config = createConfig(convertKeysToCamelCase(process.env));
|
||||||
|
|
||||||
return config
|
return config;
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
throw new Error("Invalid config", {
|
||||||
throw new Error('Invalid config', {
|
cause: error
|
||||||
cause: error,
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { config } from '#root/config.js'
|
import { config } from "#root/config.js";
|
||||||
import { pino } from 'pino'
|
import { pino } from "pino";
|
||||||
|
|
||||||
export const logger = pino({
|
export const logger = pino({
|
||||||
level: config.logLevel,
|
level: config.logLevel,
|
||||||
@@ -8,24 +8,24 @@ export const logger = pino({
|
|||||||
...(config.isDebug
|
...(config.isDebug
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
target: 'pino-pretty',
|
target: "pino-pretty",
|
||||||
level: config.logLevel,
|
level: config.logLevel,
|
||||||
options: {
|
options: {
|
||||||
ignore: 'pid,hostname',
|
ignore: "pid,hostname",
|
||||||
colorize: true,
|
colorize: true,
|
||||||
translateTime: true,
|
translateTime: true
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
target: 'pino/file',
|
target: "pino/file",
|
||||||
level: config.logLevel,
|
level: config.logLevel,
|
||||||
options: {},
|
options: {}
|
||||||
},
|
}
|
||||||
]),
|
])
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
export type Logger = typeof logger
|
export type Logger = typeof logger;
|
||||||
|
|||||||
115
src/main.ts
115
src/main.ts
@@ -1,111 +1,104 @@
|
|||||||
#!/usr/bin/env tsx
|
#!/usr/bin/env tsx
|
||||||
/* eslint-disable antfu/no-top-level-await */
|
|
||||||
|
|
||||||
import type { PollingConfig, WebhookConfig } from '#root/config.js'
|
import type { PollingConfig, WebhookConfig } from "#root/config.js";
|
||||||
import type { RunnerHandle } from '@grammyjs/runner'
|
import type { RunnerHandle } from "@grammyjs/runner";
|
||||||
import process from 'node:process'
|
import process from "node:process";
|
||||||
import { createBot } from '#root/bot/index.js'
|
import { createBot } from "#root/bot/index.js";
|
||||||
import { config } from '#root/config.js'
|
import { config } from "#root/config.js";
|
||||||
import { logger } from '#root/logger.js'
|
import { logger } from "#root/logger.js";
|
||||||
import { createServer, createServerManager } from '#root/server/index.js'
|
import { createServer, createServerManager } from "#root/server/index.js";
|
||||||
import { run } from '@grammyjs/runner'
|
import { run } from "@grammyjs/runner";
|
||||||
|
|
||||||
async function startPolling(config: PollingConfig) {
|
async function startPolling(config: PollingConfig) {
|
||||||
const bot = createBot(config.botToken, {
|
const bot = createBot(config.botToken, {
|
||||||
config,
|
config,
|
||||||
logger,
|
logger
|
||||||
})
|
});
|
||||||
let runner: undefined | RunnerHandle
|
// eslint-disable-next-line prefer-const
|
||||||
|
let runner: undefined | RunnerHandle;
|
||||||
|
|
||||||
// graceful shutdown
|
// graceful shutdown
|
||||||
onShutdown(async () => {
|
onShutdown(async () => {
|
||||||
logger.info('Shutdown')
|
logger.info("Shutdown");
|
||||||
await runner?.stop()
|
await runner?.stop();
|
||||||
})
|
});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([bot.init(), bot.api.deleteWebhook()]);
|
||||||
bot.init(),
|
|
||||||
bot.api.deleteWebhook(),
|
|
||||||
])
|
|
||||||
|
|
||||||
// start bot
|
// start bot
|
||||||
runner = run(bot, {
|
runner = run(bot, {
|
||||||
runner: {
|
runner: {
|
||||||
fetch: {
|
fetch: {
|
||||||
allowed_updates: config.botAllowedUpdates,
|
allowed_updates: config.botAllowedUpdates
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
logger.info({
|
logger.info({
|
||||||
msg: 'Bot running...',
|
msg: "Bot running...",
|
||||||
username: bot.botInfo.username,
|
username: bot.botInfo.username
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startWebhook(config: WebhookConfig) {
|
async function startWebhook(config: WebhookConfig) {
|
||||||
const bot = createBot(config.botToken, {
|
const bot = createBot(config.botToken, {
|
||||||
config,
|
config,
|
||||||
logger,
|
logger
|
||||||
})
|
});
|
||||||
const server = createServer({
|
const server = createServer({
|
||||||
bot,
|
bot,
|
||||||
config,
|
config,
|
||||||
logger,
|
logger
|
||||||
})
|
});
|
||||||
const serverManager = createServerManager(server, {
|
const serverManager = createServerManager(server, {
|
||||||
host: config.serverHost,
|
host: config.serverHost,
|
||||||
port: config.serverPort,
|
port: config.serverPort
|
||||||
})
|
});
|
||||||
|
|
||||||
// graceful shutdown
|
// graceful shutdown
|
||||||
onShutdown(async () => {
|
onShutdown(async () => {
|
||||||
logger.info('Shutdown')
|
logger.info("Shutdown");
|
||||||
await serverManager.stop()
|
await serverManager.stop();
|
||||||
})
|
});
|
||||||
|
|
||||||
// to prevent receiving updates before the bot is ready
|
// to prevent receiving updates before the bot is ready
|
||||||
await bot.init()
|
await bot.init();
|
||||||
|
|
||||||
// start server
|
// start server
|
||||||
const info = await serverManager.start()
|
const info = await serverManager.start();
|
||||||
logger.info({
|
logger.info({
|
||||||
msg: 'Server started',
|
msg: "Server started",
|
||||||
url: info.url,
|
url: info.url
|
||||||
})
|
});
|
||||||
|
|
||||||
// set webhook
|
// set webhook
|
||||||
await bot.api.setWebhook(config.botWebhook, {
|
await bot.api.setWebhook(config.botWebhook, {
|
||||||
allowed_updates: config.botAllowedUpdates,
|
allowed_updates: config.botAllowedUpdates,
|
||||||
secret_token: config.botWebhookSecret,
|
secret_token: config.botWebhookSecret
|
||||||
})
|
});
|
||||||
logger.info({
|
logger.info({
|
||||||
msg: 'Webhook was set',
|
msg: "Webhook was set",
|
||||||
url: config.botWebhook,
|
url: config.botWebhook
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (config.isWebhookMode)
|
if (config.isWebhookMode) await startWebhook(config);
|
||||||
await startWebhook(config)
|
else if (config.isPollingMode) await startPolling(config);
|
||||||
else if (config.isPollingMode)
|
} catch (error) {
|
||||||
await startPolling(config)
|
logger.error(error);
|
||||||
}
|
process.exit(1);
|
||||||
catch (error) {
|
|
||||||
logger.error(error)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
function onShutdown(cleanUp: () => Promise<void>) {
|
function onShutdown(cleanUp: () => Promise<void>) {
|
||||||
let isShuttingDown = false
|
let isShuttingDown = false;
|
||||||
const handleShutdown = async () => {
|
const handleShutdown = async () => {
|
||||||
if (isShuttingDown)
|
if (isShuttingDown) return;
|
||||||
return
|
isShuttingDown = true;
|
||||||
isShuttingDown = true
|
await cleanUp();
|
||||||
await cleanUp()
|
};
|
||||||
}
|
process.on("SIGINT", handleShutdown);
|
||||||
process.on('SIGINT', handleShutdown)
|
process.on("SIGTERM", handleShutdown);
|
||||||
process.on('SIGTERM', handleShutdown)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Logger } from '#root/logger.js'
|
import type { Logger } from "#root/logger.js";
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
Variables: {
|
Variables: {
|
||||||
requestId: string
|
requestId: string;
|
||||||
logger: Logger
|
logger: Logger;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,98 @@
|
|||||||
import type { Bot } from '#root/bot/index.js'
|
import type { Bot } from "#root/bot/index.js";
|
||||||
import type { Config } from '#root/config.js'
|
import type { Config } from "#root/config.js";
|
||||||
import type { Logger } from '#root/logger.js'
|
import type { Logger } from "#root/logger.js";
|
||||||
import type { Env } from '#root/server/environment.js'
|
import type { Env } from "#root/server/environment.js";
|
||||||
import { setLogger } from '#root/server/middlewares/logger.js'
|
import { setLogger } from "#root/server/middlewares/logger.js";
|
||||||
import { requestId } from '#root/server/middlewares/request-id.js'
|
import { requestId } from "#root/server/middlewares/request-id.js";
|
||||||
import { requestLogger } from '#root/server/middlewares/request-logger.js'
|
import { requestLogger } from "#root/server/middlewares/request-logger.js";
|
||||||
import { serve } from '@hono/node-server'
|
import { serve } from "@hono/node-server";
|
||||||
import { webhookCallback } from 'grammy'
|
import { webhookCallback } from "grammy";
|
||||||
import { Hono } from 'hono'
|
import { Hono } from "hono";
|
||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from "hono/http-exception";
|
||||||
import { getPath } from 'hono/utils/url'
|
import { getPath } from "hono/utils/url";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
bot: Bot
|
bot: Bot;
|
||||||
config: Config
|
config: Config;
|
||||||
logger: Logger
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createServer(dependencies: Dependencies) {
|
export function createServer(dependencies: Dependencies) {
|
||||||
const {
|
const { bot, config, logger } = dependencies;
|
||||||
bot,
|
|
||||||
config,
|
|
||||||
logger,
|
|
||||||
} = dependencies
|
|
||||||
|
|
||||||
const server = new Hono<Env>()
|
const server = new Hono<Env>();
|
||||||
|
|
||||||
server.use(requestId())
|
server.use(requestId());
|
||||||
server.use(setLogger(logger))
|
server.use(setLogger(logger));
|
||||||
if (config.isDebug)
|
if (config.isDebug) server.use(requestLogger());
|
||||||
server.use(requestLogger())
|
|
||||||
|
|
||||||
server.onError(async (error, c) => {
|
server.onError(async (error, c) => {
|
||||||
if (error instanceof HTTPException) {
|
if (error instanceof HTTPException) {
|
||||||
if (error.status < 500)
|
if (error.status < 500) c.var.logger.info(error);
|
||||||
c.var.logger.info(error)
|
else c.var.logger.error(error);
|
||||||
else
|
|
||||||
c.var.logger.error(error)
|
|
||||||
|
|
||||||
return error.getResponse()
|
return error.getResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// unexpected error
|
// unexpected error
|
||||||
c.var.logger.error({
|
c.var.logger.error({
|
||||||
err: error,
|
err: error,
|
||||||
method: c.req.raw.method,
|
method: c.req.raw.method,
|
||||||
path: getPath(c.req.raw),
|
path: getPath(c.req.raw)
|
||||||
})
|
});
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
error: 'Oops! Something went wrong.',
|
error: "Oops! Something went wrong."
|
||||||
},
|
},
|
||||||
500,
|
500
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
server.get('/', c => c.json({ status: true }))
|
server.get("/", c => c.json({ status: true }));
|
||||||
|
|
||||||
if (config.isWebhookMode) {
|
if (config.isWebhookMode) {
|
||||||
server.post(
|
server.post(
|
||||||
'/webhook',
|
"/webhook",
|
||||||
webhookCallback(bot, 'hono', {
|
webhookCallback(bot, "hono", {
|
||||||
secretToken: config.botWebhookSecret,
|
secretToken: config.botWebhookSecret
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return server
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Server = Awaited<ReturnType<typeof createServer>>
|
export type Server = Awaited<ReturnType<typeof createServer>>;
|
||||||
|
|
||||||
export function createServerManager(server: Server, options: { host: string, port: number }) {
|
export function createServerManager(
|
||||||
let handle: undefined | ReturnType<typeof serve>
|
server: Server,
|
||||||
|
options: { host: string; port: number }
|
||||||
|
) {
|
||||||
|
let handle: undefined | ReturnType<typeof serve>;
|
||||||
return {
|
return {
|
||||||
start() {
|
start() {
|
||||||
return new Promise<{ url: string }>((resolve) => {
|
return new Promise<{ url: string }>(resolve => {
|
||||||
handle = serve(
|
handle = serve(
|
||||||
{
|
{
|
||||||
fetch: server.fetch,
|
fetch: server.fetch,
|
||||||
hostname: options.host,
|
hostname: options.host,
|
||||||
port: options.port,
|
port: options.port
|
||||||
},
|
},
|
||||||
info => resolve({
|
info =>
|
||||||
url: info.family === 'IPv6'
|
resolve({
|
||||||
? `http://[${info.address}]:${info.port}`
|
url:
|
||||||
: `http://${info.address}:${info.port}`,
|
info.family === "IPv6"
|
||||||
}),
|
? `http://[${info.address}]:${info.port}`
|
||||||
)
|
: `http://${info.address}:${info.port}`
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>(resolve => {
|
||||||
if (handle)
|
if (handle) handle.close(() => resolve());
|
||||||
handle.close(() => resolve())
|
else resolve();
|
||||||
else
|
});
|
||||||
resolve()
|
}
|
||||||
})
|
};
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Logger } from '#root/logger.js'
|
import type { Logger } from "#root/logger.js";
|
||||||
import type { MiddlewareHandler } from 'hono'
|
import type { MiddlewareHandler } from "hono";
|
||||||
|
|
||||||
export function setLogger(logger: Logger): MiddlewareHandler {
|
export function setLogger(logger: Logger): MiddlewareHandler {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
c.set(
|
c.set(
|
||||||
'logger',
|
"logger",
|
||||||
logger.child({
|
logger.child({
|
||||||
requestId: c.get('requestId'),
|
requestId: c.get("requestId")
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
await next()
|
await next();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { MiddlewareHandler } from 'hono'
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
export function requestId(): MiddlewareHandler {
|
export function requestId(): MiddlewareHandler {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
c.set('requestId', randomUUID())
|
c.set("requestId", randomUUID());
|
||||||
|
|
||||||
await next()
|
await next();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import type { MiddlewareHandler } from 'hono'
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { getPath } from 'hono/utils/url'
|
import { getPath } from "hono/utils/url";
|
||||||
|
|
||||||
export function requestLogger(): MiddlewareHandler {
|
export function requestLogger(): MiddlewareHandler {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const { method } = c.req
|
const { method } = c.req;
|
||||||
const path = getPath(c.req.raw)
|
const path = getPath(c.req.raw);
|
||||||
|
|
||||||
c.var.logger.debug({
|
c.var.logger.debug({
|
||||||
msg: 'Incoming request',
|
msg: "Incoming request",
|
||||||
method,
|
method,
|
||||||
path,
|
path
|
||||||
})
|
});
|
||||||
const startTime = performance.now()
|
const startTime = performance.now();
|
||||||
|
|
||||||
await next()
|
await next();
|
||||||
|
|
||||||
const endTime = performance.now()
|
const endTime = performance.now();
|
||||||
c.var.logger.debug({
|
c.var.logger.debug({
|
||||||
msg: 'Request completed',
|
msg: "Request completed",
|
||||||
method,
|
method,
|
||||||
path,
|
path,
|
||||||
status: c.res.status,
|
status: c.res.status,
|
||||||
elapsed: endTime - startTime,
|
elapsed: endTime - startTime
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"paths": {
|
"paths": {
|
||||||
"#root/*": [
|
"#root/*": ["./src/*"]
|
||||||
"./src/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
@@ -17,7 +15,5 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"preserveWatchOutput": true
|
"preserveWatchOutput": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*"]
|
||||||
"src/**/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user