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:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
build-and-push-docker-image:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run typecheck
|
||||
|
||||
auto-merge:
|
||||
if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
|
||||
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
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
- name: Auto-merge
|
||||
if: steps.metadata.outputs.update-type != 'version-update:semver-major'
|
||||
run: gh pr merge --auto --squash "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
- name: Build and Push Versioned Docker Image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
with:
|
||||
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"]
|
||||
25
README.md
25
README.md
@@ -34,10 +34,16 @@ Follow these steps to set up and run your bot using this template:
|
||||
2. **Environment Variables Setup**
|
||||
|
||||
Create an environment variables file by copying the provided example file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# development
|
||||
cp .env.example .env.bot.dev
|
||||
|
||||
# production
|
||||
cp .env.example .env.bot.prod
|
||||
```
|
||||
Open the newly created `.env` file and set the `BOT_TOKEN` environment variable.
|
||||
|
||||
Open the newly created `.env.bot.dev` and `.env.bot.prod` files and set the `BOT_TOKEN` environment variable.
|
||||
|
||||
3. **Launching the Bot**
|
||||
|
||||
@@ -46,28 +52,25 @@ Follow these steps to set up and run your bot using this template:
|
||||
**Development Mode:**
|
||||
|
||||
Install the required dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Start the bot in watch mode (auto-reload when code changes):
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
docker compose up
|
||||
```
|
||||
|
||||
**Production Mode:**
|
||||
|
||||
Install only production dependencies:
|
||||
```bash
|
||||
npm install --only=prod
|
||||
```
|
||||
|
||||
Set `DEBUG` environment variable to `false` in your `.env` file.
|
||||
|
||||
Start the bot in production mode:
|
||||
|
||||
```bash
|
||||
npm run start:force # skip type checking and start
|
||||
# or
|
||||
npm start # with type checking (requires development dependencies)
|
||||
docker compose -f compose.yml -f compose.prod.yml up
|
||||
```
|
||||
|
||||
### 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",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447",
|
||||
"description": "Telegram bot starter template",
|
||||
"imports": {
|
||||
"#root/*": "./build/src/*"
|
||||
@@ -21,7 +22,8 @@
|
||||
"dev": "tsc-watch --onSuccess \"tsx ./src/main.ts\"",
|
||||
"start": "tsc && tsx ./src/main.ts",
|
||||
"start:force": "tsx ./src/main.ts",
|
||||
"prepare": "husky || true"
|
||||
"prepare": "husky || true",
|
||||
"pretty": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@grammyjs/auto-chat-action": "0.1.1",
|
||||
@@ -43,12 +45,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "4.3.0",
|
||||
"@eslint/js": "^9.20.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"eslint": "^9.20.1",
|
||||
"globals": "^15.15.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.4.3",
|
||||
"prettier": "^3.5.1",
|
||||
"tsc-watch": "^6.2.1",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": "eslint"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createCallbackData } from 'callback-data'
|
||||
import { createCallbackData } from "callback-data";
|
||||
|
||||
export const changeLanguageData = createCallbackData('language', {
|
||||
code: String,
|
||||
})
|
||||
export const changeLanguageData = createCallbackData("language", {
|
||||
code: String
|
||||
});
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { Config } from '#root/config.js'
|
||||
import type { Logger } from '#root/logger.js'
|
||||
import type { AutoChatActionFlavor } from '@grammyjs/auto-chat-action'
|
||||
import type { HydrateFlavor } from '@grammyjs/hydrate'
|
||||
import type { I18nFlavor } from '@grammyjs/i18n'
|
||||
import type { ParseModeFlavor } from '@grammyjs/parse-mode'
|
||||
import type { Context as DefaultContext, SessionFlavor } from 'grammy'
|
||||
import type { Config } from "#root/config.js";
|
||||
import type { Logger } from "#root/logger.js";
|
||||
import type { AutoChatActionFlavor } from "@grammyjs/auto-chat-action";
|
||||
import type { HydrateFlavor } from "@grammyjs/hydrate";
|
||||
import type { I18nFlavor } from "@grammyjs/i18n";
|
||||
import type { ParseModeFlavor } from "@grammyjs/parse-mode";
|
||||
import type { Context as DefaultContext, SessionFlavor } from "grammy";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface SessionData {
|
||||
// field?: string;
|
||||
}
|
||||
|
||||
interface ExtendedContextFlavor {
|
||||
logger: Logger
|
||||
config: Config
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export type Context = ParseModeFlavor<
|
||||
@@ -23,4 +24,4 @@ export type Context = ParseModeFlavor<
|
||||
I18nFlavor &
|
||||
AutoChatActionFlavor
|
||||
>
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { Context } from '#root/bot/context.js'
|
||||
import { isAdmin } from '#root/bot/filters/is-admin.js'
|
||||
import { setCommandsHandler } from '#root/bot/handlers/commands/setcommands.js'
|
||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
||||
import { chatAction } from '@grammyjs/auto-chat-action'
|
||||
import { Composer } from 'grammy'
|
||||
import { chatAction } from "@grammyjs/auto-chat-action";
|
||||
import { Composer } from "grammy";
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import { isAdmin } from "#root/bot/filters/is-admin.js";
|
||||
import { setCommandsHandler } from "#root/bot/handlers/commands/setcommands.js";
|
||||
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||
|
||||
const composer = new Composer<Context>()
|
||||
const composer = new Composer<Context>();
|
||||
|
||||
const feature = composer
|
||||
.chatType('private')
|
||||
.filter(isAdmin)
|
||||
.chatType("private")
|
||||
.filter(ctx => isAdmin(ctx.config.botAdmins)(ctx));
|
||||
|
||||
feature.command(
|
||||
'setcommands',
|
||||
logHandle('command-setcommands'),
|
||||
chatAction('typing'),
|
||||
setCommandsHandler,
|
||||
)
|
||||
"setcommands",
|
||||
logHandle("command-setcommands"),
|
||||
chatAction("typing"),
|
||||
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 { changeLanguageData } from '#root/bot/callback-data/change-language.js'
|
||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
||||
import { i18n } from '#root/bot/i18n.js'
|
||||
import { createChangeLanguageKeyboard } from '#root/bot/keyboards/change-language.js'
|
||||
import { Composer } from 'grammy'
|
||||
import { Composer } from "grammy";
|
||||
import { changeLanguageData } from "#root/bot/callback-data/change-language.js";
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import { logHandle } from "#root/bot/helpers/logging.js";
|
||||
import { i18n } from "#root/bot/i18n.js";
|
||||
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) => {
|
||||
return ctx.reply(ctx.t('language-select'), {
|
||||
reply_markup: await createChangeLanguageKeyboard(ctx),
|
||||
})
|
||||
})
|
||||
feature.command("language", logHandle("command-language"), async ctx => {
|
||||
return ctx.reply(ctx.t("language-select"), {
|
||||
reply_markup: await createChangeLanguageKeyboard(ctx)
|
||||
});
|
||||
});
|
||||
|
||||
feature.callbackQuery(
|
||||
changeLanguageData.filter(),
|
||||
logHandle('keyboard-language-select'),
|
||||
async (ctx) => {
|
||||
logHandle("keyboard-language-select"),
|
||||
async ctx => {
|
||||
const { code: languageCode } = changeLanguageData.unpack(
|
||||
ctx.callbackQuery.data,
|
||||
)
|
||||
ctx.callbackQuery.data
|
||||
);
|
||||
|
||||
if (i18n.locales.includes(languageCode)) {
|
||||
await ctx.i18n.setLocale(languageCode)
|
||||
await ctx.i18n.setLocale(languageCode);
|
||||
|
||||
return ctx.editMessageText(ctx.t('language-changed'), {
|
||||
reply_markup: await createChangeLanguageKeyboard(ctx),
|
||||
})
|
||||
return ctx.editMessageText(ctx.t("language-changed"), {
|
||||
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 { logHandle } from '#root/bot/helpers/logging.js'
|
||||
import { Composer } from 'grammy'
|
||||
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 composer = new Composer<Context>();
|
||||
|
||||
const feature = composer.chatType('private')
|
||||
const feature = composer.chatType("private");
|
||||
|
||||
feature.on('message', logHandle('unhandled-message'), (ctx) => {
|
||||
return ctx.reply(ctx.t('unhandled'))
|
||||
})
|
||||
feature.on("message", logHandle("unhandled-message"), ctx => {
|
||||
return ctx.reply(ctx.t("unhandled"));
|
||||
});
|
||||
|
||||
feature.on('callback_query', logHandle('unhandled-callback-query'), (ctx) => {
|
||||
return ctx.answerCallbackQuery()
|
||||
})
|
||||
feature.on("callback_query", logHandle("unhandled-callback-query"), ctx => {
|
||||
return ctx.answerCallbackQuery();
|
||||
});
|
||||
|
||||
export { composer as unhandledFeature }
|
||||
export { composer as unhandledFeature };
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { Context } from '#root/bot/context.js'
|
||||
import { logHandle } from '#root/bot/helpers/logging.js'
|
||||
import { Composer } from 'grammy'
|
||||
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 composer = new Composer<Context>();
|
||||
|
||||
const feature = composer.chatType('private')
|
||||
const feature = composer.chatType("private");
|
||||
|
||||
feature.command('start', logHandle('command-start'), (ctx) => {
|
||||
return ctx.reply(ctx.t('welcome'))
|
||||
})
|
||||
feature.command("start", logHandle("command-start"), ctx => {
|
||||
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) {
|
||||
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 { LanguageCode } from '@grammyjs/types'
|
||||
import type { CommandContext } from 'grammy'
|
||||
import { i18n } from '#root/bot/i18n.js'
|
||||
import { Command, CommandGroup } from '@grammyjs/commands'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import type { LanguageCode } from "@grammyjs/types";
|
||||
import type { CommandContext } from "grammy";
|
||||
import { i18n } from "#root/bot/i18n.js";
|
||||
import { Command, CommandGroup } from "@grammyjs/commands";
|
||||
|
||||
function addCommandLocalizations(command: Command) {
|
||||
i18n.locales.forEach((locale) => {
|
||||
i18n.locales.forEach(locale => {
|
||||
command.localize(
|
||||
locale as LanguageCode,
|
||||
command.name,
|
||||
i18n.t(locale, `${command.name}.description`),
|
||||
)
|
||||
})
|
||||
return command
|
||||
i18n.t(locale, `${command.name}.description`)
|
||||
);
|
||||
});
|
||||
return command;
|
||||
}
|
||||
|
||||
function addCommandToChats(command: Command, chats: number[]) {
|
||||
for (const chatId of chats) {
|
||||
command.addToScope({
|
||||
type: 'chat',
|
||||
chat_id: chatId,
|
||||
})
|
||||
type: "chat",
|
||||
chat_id: chatId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function setCommandsHandler(ctx: CommandContext<Context>) {
|
||||
const start = new Command('start', i18n.t('en', 'start.description'))
|
||||
.addToScope({ type: 'all_private_chats' })
|
||||
addCommandLocalizations(start)
|
||||
addCommandToChats(start, ctx.config.botAdmins)
|
||||
const start = new Command(
|
||||
"start",
|
||||
i18n.t("en", "start.description")
|
||||
).addToScope({ type: "all_private_chats" });
|
||||
addCommandLocalizations(start);
|
||||
addCommandToChats(start, ctx.config.botAdmins);
|
||||
|
||||
const language = new Command('language', i18n.t('en', 'language.description'))
|
||||
.addToScope({ type: 'all_private_chats' })
|
||||
addCommandLocalizations(language)
|
||||
addCommandToChats(language, ctx.config.botAdmins)
|
||||
const language = new Command(
|
||||
"language",
|
||||
i18n.t("en", "language.description")
|
||||
).addToScope({ type: "all_private_chats" });
|
||||
addCommandLocalizations(language);
|
||||
addCommandToChats(language, ctx.config.botAdmins);
|
||||
|
||||
const setcommands = new Command('setcommands', i18n.t('en', 'setcommands.description'))
|
||||
addCommandToChats(setcommands, ctx.config.botAdmins)
|
||||
const setcommands = new Command(
|
||||
"setcommands",
|
||||
i18n.t("en", "setcommands.description")
|
||||
);
|
||||
addCommandToChats(setcommands, ctx.config.botAdmins);
|
||||
|
||||
const commands = new CommandGroup()
|
||||
.add(start)
|
||||
.add(language)
|
||||
.add(setcommands)
|
||||
const commands = new CommandGroup().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 { ErrorHandler } from 'grammy'
|
||||
import { getUpdateInfo } from '#root/bot/helpers/logging.js'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import type { ErrorHandler } from "grammy";
|
||||
import { getUpdateInfo } from "#root/bot/helpers/logging.js";
|
||||
|
||||
export const errorHandler: ErrorHandler<Context> = (error) => {
|
||||
const { ctx } = error
|
||||
export const errorHandler: ErrorHandler<Context> = error => {
|
||||
const { ctx } = error;
|
||||
|
||||
ctx.logger.error({
|
||||
err: error.error,
|
||||
update: getUpdateInfo(ctx),
|
||||
})
|
||||
}
|
||||
update: getUpdateInfo(ctx)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function chunk<T>(array: T[], size: number) {
|
||||
const result = []
|
||||
const result = [];
|
||||
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 { Update } from '@grammyjs/types'
|
||||
import type { Middleware } from 'grammy'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import type { Update } from "@grammyjs/types";
|
||||
import type { Middleware } from "grammy";
|
||||
|
||||
export function getUpdateInfo(ctx: Context): Omit<Update, 'update_id'> {
|
||||
const { update_id, ...update } = ctx.update
|
||||
export function getUpdateInfo(ctx: Context): Omit<Update, "update_id"> {
|
||||
const { /*update_id,*/ ...update } = ctx.update;
|
||||
|
||||
return update
|
||||
return update;
|
||||
}
|
||||
|
||||
export function logHandle(id: string): Middleware<Context> {
|
||||
return (ctx, next) => {
|
||||
ctx.logger.info({
|
||||
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 path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { I18n } from '@grammyjs/i18n'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { I18n } from "@grammyjs/i18n";
|
||||
|
||||
export const i18n = new I18n<Context>({
|
||||
defaultLocale: 'en',
|
||||
directory: path.resolve(process.cwd(), 'locales'),
|
||||
defaultLocale: "en",
|
||||
directory: path.resolve(process.cwd(), "locales"),
|
||||
useSession: true,
|
||||
fluentBundleOptions: {
|
||||
useIsolating: false,
|
||||
},
|
||||
})
|
||||
useIsolating: false
|
||||
}
|
||||
});
|
||||
|
||||
export const isMultipleLocales = i18n.locales.length > 1
|
||||
export const isMultipleLocales = i18n.locales.length > 1;
|
||||
|
||||
118
src/bot/index.ts
118
src/bot/index.ts
@@ -1,75 +1,89 @@
|
||||
import type { Context } from '#root/bot/context.js'
|
||||
import type { Config } from '#root/config.js'
|
||||
import type { Logger } from '#root/logger.js'
|
||||
import type { BotConfig } from 'grammy'
|
||||
import { adminFeature } from '#root/bot/features/admin.js'
|
||||
import { languageFeature } from '#root/bot/features/language.js'
|
||||
import { unhandledFeature } from '#root/bot/features/unhandled.js'
|
||||
import { welcomeFeature } from '#root/bot/features/welcome.js'
|
||||
import { errorHandler } from '#root/bot/handlers/error.js'
|
||||
import { i18n, isMultipleLocales } from '#root/bot/i18n.js'
|
||||
import { session } from '#root/bot/middlewares/session.js'
|
||||
import { updateLogger } from '#root/bot/middlewares/update-logger.js'
|
||||
import { autoChatAction } from '@grammyjs/auto-chat-action'
|
||||
import { hydrate } from '@grammyjs/hydrate'
|
||||
import { hydrateReply, parseMode } from '@grammyjs/parse-mode'
|
||||
import { sequentialize } from '@grammyjs/runner'
|
||||
import { MemorySessionStorage, Bot as TelegramBot } from 'grammy'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import type { Config } from "#root/config.js";
|
||||
import type { Logger } from "#root/logger.js";
|
||||
import type { BotConfig } from "grammy";
|
||||
import { adminFeature } from "#root/bot/features/admin.js";
|
||||
import { languageFeature } from "#root/bot/features/language.js";
|
||||
import { unhandledFeature } from "#root/bot/features/unhandled.js";
|
||||
import { welcomeFeature } from "#root/bot/features/welcome.js";
|
||||
import { errorHandler } from "#root/bot/handlers/error.js";
|
||||
import { i18n, isMultipleLocales } from "#root/bot/i18n.js";
|
||||
import { session } from "#root/bot/middlewares/session.js";
|
||||
import { updateLogger } from "#root/bot/middlewares/update-logger.js";
|
||||
import { autoChatAction } from "@grammyjs/auto-chat-action";
|
||||
import { hydrate } from "@grammyjs/hydrate";
|
||||
import { hydrateReply, parseMode } from "@grammyjs/parse-mode";
|
||||
import { sequentialize } from "@grammyjs/runner";
|
||||
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 {
|
||||
config: Config
|
||||
logger: Logger
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
function getSessionKey(ctx: Omit<Context, 'session'>) {
|
||||
return ctx.chat?.id.toString()
|
||||
function getSessionKey(ctx: Omit<Context, "session">) {
|
||||
return ctx.chat?.id.toString();
|
||||
}
|
||||
|
||||
export function createBot(token: string, dependencies: Dependencies, botConfig?: BotConfig<Context>) {
|
||||
const {
|
||||
config,
|
||||
logger,
|
||||
} = dependencies
|
||||
export function createBot(
|
||||
token: string,
|
||||
dependencies: Dependencies,
|
||||
botConfig?: BotConfig<Context>
|
||||
) {
|
||||
const { config, logger } = dependencies;
|
||||
|
||||
const bot = new TelegramBot<Context>(token, botConfig)
|
||||
const bot = new TelegramBot<Context>(token, botConfig);
|
||||
|
||||
bot.use(async (ctx, next) => {
|
||||
ctx.config = config
|
||||
ctx.config = config;
|
||||
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
|
||||
bot.api.config.use(parseMode('HTML'))
|
||||
bot.api.config.use(parseMode("HTML"));
|
||||
|
||||
if (config.isPollingMode)
|
||||
protectedBot.use(sequentialize(getSessionKey))
|
||||
if (config.isDebug)
|
||||
protectedBot.use(updateLogger())
|
||||
protectedBot.use(autoChatAction(bot.api))
|
||||
protectedBot.use(hydrateReply)
|
||||
protectedBot.use(hydrate())
|
||||
protectedBot.use(session({
|
||||
if (config.isPollingMode) protectedBot.use(sequentialize(getSessionKey));
|
||||
if (config.isDebug) protectedBot.use(updateLogger());
|
||||
protectedBot.use(autoChatAction(bot.api));
|
||||
protectedBot.use(hydrateReply);
|
||||
protectedBot.use(hydrate());
|
||||
protectedBot.use(
|
||||
session({
|
||||
getSessionKey,
|
||||
storage: new MemorySessionStorage(),
|
||||
}))
|
||||
protectedBot.use(i18n)
|
||||
storage: new MemorySessionStorage()
|
||||
})
|
||||
);
|
||||
protectedBot.use(i18n);
|
||||
|
||||
// Handlers
|
||||
protectedBot.use(welcomeFeature)
|
||||
protectedBot.use(adminFeature)
|
||||
if (isMultipleLocales)
|
||||
protectedBot.use(languageFeature)
|
||||
protectedBot.use(welcomeFeature);
|
||||
protectedBot.use(adminFeature);
|
||||
if (isMultipleLocales) 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
|
||||
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 { changeLanguageData } from '#root/bot/callback-data/change-language.js'
|
||||
import { chunk } from '#root/bot/helpers/keyboard.js'
|
||||
import { i18n } from '#root/bot/i18n.js'
|
||||
import { InlineKeyboard } from 'grammy'
|
||||
import ISO6391 from 'iso-639-1'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import { changeLanguageData } from "#root/bot/callback-data/change-language.js";
|
||||
import { chunk } from "#root/bot/helpers/keyboard.js";
|
||||
import { i18n } from "#root/bot/i18n.js";
|
||||
import { InlineKeyboard } from "grammy";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
||||
export async function createChangeLanguageKeyboard(ctx: Context) {
|
||||
const currentLocaleCode = await ctx.i18n.getLocale()
|
||||
const currentLocaleCode = await ctx.i18n.getLocale();
|
||||
|
||||
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(
|
||||
chunk(
|
||||
i18n.locales.map(localeCode => ({
|
||||
text: getLabel(localeCode),
|
||||
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 { Middleware, SessionOptions } from 'grammy'
|
||||
import { session as createSession } from 'grammy'
|
||||
import type { Context, SessionData } from "#root/bot/context.js";
|
||||
import type { Middleware, SessionOptions } 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> {
|
||||
return createSession({
|
||||
getSessionKey: options.getSessionKey,
|
||||
storage: options.storage,
|
||||
initial: () => ({}),
|
||||
})
|
||||
initial: () => ({})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import type { Context } from '#root/bot/context.js'
|
||||
import type { Middleware } from 'grammy'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { getUpdateInfo } from '#root/bot/helpers/logging.js'
|
||||
import type { Context } from "#root/bot/context.js";
|
||||
import type { Middleware } from "grammy";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { getUpdateInfo } from "#root/bot/helpers/logging.js";
|
||||
|
||||
export function updateLogger(): Middleware<Context> {
|
||||
return async (ctx, next) => {
|
||||
ctx.api.config.use((previous, method, payload, signal) => {
|
||||
ctx.logger.debug({
|
||||
msg: 'Bot API call',
|
||||
msg: "Bot API call",
|
||||
method,
|
||||
payload,
|
||||
})
|
||||
payload
|
||||
});
|
||||
|
||||
return previous(method, payload, signal)
|
||||
})
|
||||
return previous(method, payload, signal);
|
||||
});
|
||||
|
||||
ctx.logger.debug({
|
||||
msg: 'Update received',
|
||||
update: getUpdateInfo(ctx),
|
||||
})
|
||||
msg: "Update received",
|
||||
update: getUpdateInfo(ctx)
|
||||
});
|
||||
|
||||
const startTime = performance.now()
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
await next()
|
||||
}
|
||||
finally {
|
||||
const endTime = performance.now()
|
||||
await next();
|
||||
} finally {
|
||||
const endTime = performance.now();
|
||||
ctx.logger.debug({
|
||||
msg: 'Update processed',
|
||||
elapsed: endTime - startTime,
|
||||
})
|
||||
}
|
||||
msg: "Update processed",
|
||||
elapsed: endTime - startTime
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
116
src/config.ts
116
src/config.ts
@@ -1,98 +1,124 @@
|
||||
import process from 'node:process'
|
||||
import { API_CONSTANTS } from 'grammy'
|
||||
import * as v from 'valibot'
|
||||
import process from "node:process";
|
||||
import { API_CONSTANTS } from "grammy";
|
||||
import * as v from "valibot";
|
||||
|
||||
const baseConfigSchema = v.object({
|
||||
debug: v.optional(v.pipe(v.string(), v.transform(JSON.parse), v.boolean()), 'false'),
|
||||
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())), '[]'),
|
||||
})
|
||||
debug: v.optional(
|
||||
v.pipe(v.string(), v.transform(JSON.parse), v.boolean()),
|
||||
"false"
|
||||
),
|
||||
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
|
||||
v.pipe(
|
||||
v.object({
|
||||
botMode: v.literal('polling'),
|
||||
...baseConfigSchema.entries,
|
||||
botMode: v.literal("polling"),
|
||||
...baseConfigSchema.entries
|
||||
}),
|
||||
v.transform(input => ({
|
||||
...input,
|
||||
isDebug: input.debug,
|
||||
isWebhookMode: false as const,
|
||||
isPollingMode: true as const,
|
||||
})),
|
||||
isPollingMode: true as const
|
||||
}))
|
||||
),
|
||||
// webhook config
|
||||
v.pipe(
|
||||
v.object({
|
||||
botMode: v.literal('webhook'),
|
||||
botMode: v.literal("webhook"),
|
||||
...baseConfigSchema.entries,
|
||||
botWebhook: v.pipe(v.string(), v.url()),
|
||||
botWebhookSecret: v.pipe(v.string(), v.minLength(12)),
|
||||
serverHost: v.optional(v.string(), '0.0.0.0'),
|
||||
serverPort: v.optional(v.pipe(v.string(), v.transform(Number), v.number()), '80'),
|
||||
serverHost: v.optional(v.string(), "0.0.0.0"),
|
||||
serverPort: v.optional(
|
||||
v.pipe(v.string(), v.transform(Number), v.number()),
|
||||
"80"
|
||||
)
|
||||
}),
|
||||
v.transform(input => ({
|
||||
...input,
|
||||
isDebug: input.debug,
|
||||
isWebhookMode: true as const,
|
||||
isPollingMode: false as const,
|
||||
})),
|
||||
),
|
||||
])
|
||||
isPollingMode: false as const
|
||||
}))
|
||||
)
|
||||
]);
|
||||
|
||||
export type Config = v.InferOutput<typeof configSchema>
|
||||
export type PollingConfig = v.InferOutput<typeof configSchema['options'][0]>
|
||||
export type WebhookConfig = v.InferOutput<typeof configSchema['options'][1]>
|
||||
export type Config = v.InferOutput<typeof configSchema>;
|
||||
export type PollingConfig = v.InferOutput<(typeof configSchema)["options"][0]>;
|
||||
export type WebhookConfig = v.InferOutput<(typeof configSchema)["options"][1]>;
|
||||
|
||||
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() {
|
||||
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
|
||||
type CamelCase<S extends string> =
|
||||
S extends `${infer P1}_${infer P2}${infer P3}`
|
||||
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
|
||||
: Lowercase<S>
|
||||
: Lowercase<S>;
|
||||
|
||||
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 {
|
||||
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> {
|
||||
const result: any = {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const camelCaseKey = toCamelCase(key)
|
||||
result[camelCaseKey] = obj[key]
|
||||
const camelCaseKey = toCamelCase(key);
|
||||
result[camelCaseKey] = obj[key];
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
process.loadEnvFile()
|
||||
}
|
||||
catch {
|
||||
process.loadEnvFile();
|
||||
} catch {
|
||||
// No .env file found
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error create config from environment variables
|
||||
const config = createConfig(convertKeysToCamelCase(process.env))
|
||||
const config = createConfig(convertKeysToCamelCase(process.env));
|
||||
|
||||
return config
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error('Invalid config', {
|
||||
cause: error,
|
||||
})
|
||||
return config;
|
||||
} catch (error) {
|
||||
throw new Error("Invalid config", {
|
||||
cause: error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { config } from '#root/config.js'
|
||||
import { pino } from 'pino'
|
||||
import { config } from "#root/config.js";
|
||||
import { pino } from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
level: config.logLevel,
|
||||
@@ -8,24 +8,24 @@ export const logger = pino({
|
||||
...(config.isDebug
|
||||
? [
|
||||
{
|
||||
target: 'pino-pretty',
|
||||
target: "pino-pretty",
|
||||
level: config.logLevel,
|
||||
options: {
|
||||
ignore: 'pid,hostname',
|
||||
ignore: "pid,hostname",
|
||||
colorize: true,
|
||||
translateTime: true,
|
||||
},
|
||||
},
|
||||
translateTime: true
|
||||
}
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
target: 'pino/file',
|
||||
target: "pino/file",
|
||||
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
|
||||
/* eslint-disable antfu/no-top-level-await */
|
||||
|
||||
import type { PollingConfig, WebhookConfig } from '#root/config.js'
|
||||
import type { RunnerHandle } from '@grammyjs/runner'
|
||||
import process from 'node:process'
|
||||
import { createBot } from '#root/bot/index.js'
|
||||
import { config } from '#root/config.js'
|
||||
import { logger } from '#root/logger.js'
|
||||
import { createServer, createServerManager } from '#root/server/index.js'
|
||||
import { run } from '@grammyjs/runner'
|
||||
import type { PollingConfig, WebhookConfig } from "#root/config.js";
|
||||
import type { RunnerHandle } from "@grammyjs/runner";
|
||||
import process from "node:process";
|
||||
import { createBot } from "#root/bot/index.js";
|
||||
import { config } from "#root/config.js";
|
||||
import { logger } from "#root/logger.js";
|
||||
import { createServer, createServerManager } from "#root/server/index.js";
|
||||
import { run } from "@grammyjs/runner";
|
||||
|
||||
async function startPolling(config: PollingConfig) {
|
||||
const bot = createBot(config.botToken, {
|
||||
config,
|
||||
logger,
|
||||
})
|
||||
let runner: undefined | RunnerHandle
|
||||
logger
|
||||
});
|
||||
// eslint-disable-next-line prefer-const
|
||||
let runner: undefined | RunnerHandle;
|
||||
|
||||
// graceful shutdown
|
||||
onShutdown(async () => {
|
||||
logger.info('Shutdown')
|
||||
await runner?.stop()
|
||||
})
|
||||
logger.info("Shutdown");
|
||||
await runner?.stop();
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
bot.init(),
|
||||
bot.api.deleteWebhook(),
|
||||
])
|
||||
await Promise.all([bot.init(), bot.api.deleteWebhook()]);
|
||||
|
||||
// start bot
|
||||
runner = run(bot, {
|
||||
runner: {
|
||||
fetch: {
|
||||
allowed_updates: config.botAllowedUpdates,
|
||||
},
|
||||
},
|
||||
})
|
||||
allowed_updates: config.botAllowedUpdates
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info({
|
||||
msg: 'Bot running...',
|
||||
username: bot.botInfo.username,
|
||||
})
|
||||
msg: "Bot running...",
|
||||
username: bot.botInfo.username
|
||||
});
|
||||
}
|
||||
|
||||
async function startWebhook(config: WebhookConfig) {
|
||||
const bot = createBot(config.botToken, {
|
||||
config,
|
||||
logger,
|
||||
})
|
||||
logger
|
||||
});
|
||||
const server = createServer({
|
||||
bot,
|
||||
config,
|
||||
logger,
|
||||
})
|
||||
logger
|
||||
});
|
||||
const serverManager = createServerManager(server, {
|
||||
host: config.serverHost,
|
||||
port: config.serverPort,
|
||||
})
|
||||
port: config.serverPort
|
||||
});
|
||||
|
||||
// graceful shutdown
|
||||
onShutdown(async () => {
|
||||
logger.info('Shutdown')
|
||||
await serverManager.stop()
|
||||
})
|
||||
logger.info("Shutdown");
|
||||
await serverManager.stop();
|
||||
});
|
||||
|
||||
// to prevent receiving updates before the bot is ready
|
||||
await bot.init()
|
||||
await bot.init();
|
||||
|
||||
// start server
|
||||
const info = await serverManager.start()
|
||||
const info = await serverManager.start();
|
||||
logger.info({
|
||||
msg: 'Server started',
|
||||
url: info.url,
|
||||
})
|
||||
msg: "Server started",
|
||||
url: info.url
|
||||
});
|
||||
|
||||
// set webhook
|
||||
await bot.api.setWebhook(config.botWebhook, {
|
||||
allowed_updates: config.botAllowedUpdates,
|
||||
secret_token: config.botWebhookSecret,
|
||||
})
|
||||
secret_token: config.botWebhookSecret
|
||||
});
|
||||
logger.info({
|
||||
msg: 'Webhook was set',
|
||||
url: config.botWebhook,
|
||||
})
|
||||
msg: "Webhook was set",
|
||||
url: config.botWebhook
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (config.isWebhookMode)
|
||||
await startWebhook(config)
|
||||
else if (config.isPollingMode)
|
||||
await startPolling(config)
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(error)
|
||||
process.exit(1)
|
||||
if (config.isWebhookMode) await startWebhook(config);
|
||||
else if (config.isPollingMode) await startPolling(config);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Utils
|
||||
|
||||
function onShutdown(cleanUp: () => Promise<void>) {
|
||||
let isShuttingDown = false
|
||||
let isShuttingDown = false;
|
||||
const handleShutdown = async () => {
|
||||
if (isShuttingDown)
|
||||
return
|
||||
isShuttingDown = true
|
||||
await cleanUp()
|
||||
}
|
||||
process.on('SIGINT', handleShutdown)
|
||||
process.on('SIGTERM', handleShutdown)
|
||||
if (isShuttingDown) return;
|
||||
isShuttingDown = true;
|
||||
await cleanUp();
|
||||
};
|
||||
process.on("SIGINT", 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 {
|
||||
Variables: {
|
||||
requestId: string
|
||||
logger: Logger
|
||||
}
|
||||
requestId: string;
|
||||
logger: Logger;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,102 +1,98 @@
|
||||
import type { Bot } from '#root/bot/index.js'
|
||||
import type { Config } from '#root/config.js'
|
||||
import type { Logger } from '#root/logger.js'
|
||||
import type { Env } from '#root/server/environment.js'
|
||||
import { setLogger } from '#root/server/middlewares/logger.js'
|
||||
import { requestId } from '#root/server/middlewares/request-id.js'
|
||||
import { requestLogger } from '#root/server/middlewares/request-logger.js'
|
||||
import { serve } from '@hono/node-server'
|
||||
import { webhookCallback } from 'grammy'
|
||||
import { Hono } from 'hono'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import { getPath } from 'hono/utils/url'
|
||||
import type { Bot } from "#root/bot/index.js";
|
||||
import type { Config } from "#root/config.js";
|
||||
import type { Logger } from "#root/logger.js";
|
||||
import type { Env } from "#root/server/environment.js";
|
||||
import { setLogger } from "#root/server/middlewares/logger.js";
|
||||
import { requestId } from "#root/server/middlewares/request-id.js";
|
||||
import { requestLogger } from "#root/server/middlewares/request-logger.js";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { webhookCallback } from "grammy";
|
||||
import { Hono } from "hono";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { getPath } from "hono/utils/url";
|
||||
|
||||
interface Dependencies {
|
||||
bot: Bot
|
||||
config: Config
|
||||
logger: Logger
|
||||
bot: Bot;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export function createServer(dependencies: Dependencies) {
|
||||
const {
|
||||
bot,
|
||||
config,
|
||||
logger,
|
||||
} = dependencies
|
||||
const { bot, config, logger } = dependencies;
|
||||
|
||||
const server = new Hono<Env>()
|
||||
const server = new Hono<Env>();
|
||||
|
||||
server.use(requestId())
|
||||
server.use(setLogger(logger))
|
||||
if (config.isDebug)
|
||||
server.use(requestLogger())
|
||||
server.use(requestId());
|
||||
server.use(setLogger(logger));
|
||||
if (config.isDebug) server.use(requestLogger());
|
||||
|
||||
server.onError(async (error, c) => {
|
||||
if (error instanceof HTTPException) {
|
||||
if (error.status < 500)
|
||||
c.var.logger.info(error)
|
||||
else
|
||||
c.var.logger.error(error)
|
||||
if (error.status < 500) c.var.logger.info(error);
|
||||
else c.var.logger.error(error);
|
||||
|
||||
return error.getResponse()
|
||||
return error.getResponse();
|
||||
}
|
||||
|
||||
// unexpected error
|
||||
c.var.logger.error({
|
||||
err: error,
|
||||
method: c.req.raw.method,
|
||||
path: getPath(c.req.raw),
|
||||
})
|
||||
path: getPath(c.req.raw)
|
||||
});
|
||||
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) {
|
||||
server.post(
|
||||
'/webhook',
|
||||
webhookCallback(bot, 'hono', {
|
||||
secretToken: config.botWebhookSecret,
|
||||
}),
|
||||
)
|
||||
"/webhook",
|
||||
webhookCallback(bot, "hono", {
|
||||
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 }) {
|
||||
let handle: undefined | ReturnType<typeof serve>
|
||||
export function createServerManager(
|
||||
server: Server,
|
||||
options: { host: string; port: number }
|
||||
) {
|
||||
let handle: undefined | ReturnType<typeof serve>;
|
||||
return {
|
||||
start() {
|
||||
return new Promise<{ url: string }>((resolve) => {
|
||||
return new Promise<{ url: string }>(resolve => {
|
||||
handle = serve(
|
||||
{
|
||||
fetch: server.fetch,
|
||||
hostname: options.host,
|
||||
port: options.port,
|
||||
port: options.port
|
||||
},
|
||||
info => resolve({
|
||||
url: info.family === 'IPv6'
|
||||
info =>
|
||||
resolve({
|
||||
url:
|
||||
info.family === "IPv6"
|
||||
? `http://[${info.address}]:${info.port}`
|
||||
: `http://${info.address}:${info.port}`,
|
||||
}),
|
||||
)
|
||||
: `http://${info.address}:${info.port}`
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (handle)
|
||||
handle.close(() => resolve())
|
||||
else
|
||||
resolve()
|
||||
})
|
||||
},
|
||||
return new Promise<void>(resolve => {
|
||||
if (handle) handle.close(() => resolve());
|
||||
else resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Logger } from '#root/logger.js'
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import type { Logger } from "#root/logger.js";
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
|
||||
export function setLogger(logger: Logger): MiddlewareHandler {
|
||||
return async (c, next) => {
|
||||
c.set(
|
||||
'logger',
|
||||
"logger",
|
||||
logger.child({
|
||||
requestId: c.get('requestId'),
|
||||
}),
|
||||
)
|
||||
requestId: c.get("requestId")
|
||||
})
|
||||
);
|
||||
|
||||
await next()
|
||||
}
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export function requestId(): MiddlewareHandler {
|
||||
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 { getPath } from 'hono/utils/url'
|
||||
import type { MiddlewareHandler } from "hono";
|
||||
import { getPath } from "hono/utils/url";
|
||||
|
||||
export function requestLogger(): MiddlewareHandler {
|
||||
return async (c, next) => {
|
||||
const { method } = c.req
|
||||
const path = getPath(c.req.raw)
|
||||
const { method } = c.req;
|
||||
const path = getPath(c.req.raw);
|
||||
|
||||
c.var.logger.debug({
|
||||
msg: 'Incoming request',
|
||||
msg: "Incoming request",
|
||||
method,
|
||||
path,
|
||||
})
|
||||
const startTime = performance.now()
|
||||
path
|
||||
});
|
||||
const startTime = performance.now();
|
||||
|
||||
await next()
|
||||
await next();
|
||||
|
||||
const endTime = performance.now()
|
||||
const endTime = performance.now();
|
||||
c.var.logger.debug({
|
||||
msg: 'Request completed',
|
||||
msg: "Request completed",
|
||||
method,
|
||||
path,
|
||||
status: c.res.status,
|
||||
elapsed: endTime - startTime,
|
||||
})
|
||||
}
|
||||
elapsed: endTime - startTime
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"paths": {
|
||||
"#root/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"#root/*": ["./src/*"]
|
||||
},
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
@@ -17,7 +15,5 @@
|
||||
"skipLibCheck": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user