├── .dockerignore ├── .env.sample ├── .github └── FUNDING.yml ├── .gitignore ├── .secrets ├── mongo_admin_password.txt ├── mongo_shieldy_password.txt └── shieldy_token.txt ├── Dockerfile ├── LICENSE ├── README.md ├── design ├── banner.png ├── designs.sketch └── logo.png ├── docker-compose.yml ├── entrypoint.sh ├── init-mongo.sh ├── package.json ├── scripts ├── download.js └── upload.js ├── src ├── commands │ ├── 1inch.ts │ ├── admin.ts │ ├── allowInvitingBots.ts │ ├── ban.ts │ ├── banForFastRepliesToPosts.ts │ ├── banNewTelegramUsers.ts │ ├── banUsers.ts │ ├── buttonText.ts │ ├── captcha.ts │ ├── captchaMessage.ts │ ├── cas.ts │ ├── deleteEntryMessages.ts │ ├── deleteEntryOnKick.ts │ ├── deleteGreetingTime.ts │ ├── greeting.ts │ ├── greetingButtons.ts │ ├── help.ts │ ├── language.ts │ ├── lock.ts │ ├── noAttack.ts │ ├── noChannelLinks.ts │ ├── restrict.ts │ ├── restrictTime.ts │ ├── setConfig.ts │ ├── skipOldUsers.ts │ ├── skipVerifiedUsers.ts │ ├── strict.ts │ ├── subscription.ts │ ├── testLocales.ts │ ├── timeLimit.ts │ ├── trust.ts │ ├── underAttack.ts │ └── viewConfig.ts ├── controllers │ └── webhook.ts ├── helpers │ ├── bot.ts │ ├── candidates.ts │ ├── captcha.ts │ ├── cas.ts │ ├── clarifyIfPrivateMessages.ts │ ├── clarifyReply.ts │ ├── deleteMessageSafe.ts │ ├── equation.ts │ ├── error.ts │ ├── getUsername.ts │ ├── globallyRestricted.ts │ ├── isGroup.ts │ ├── localizations.ts │ ├── newcomers │ │ ├── addKickedUser.ts │ │ ├── checkButton.ts │ │ ├── checkPassingCaptchaWithText.ts │ │ ├── constructMessageWithEntities.ts │ │ ├── generateEquationOrImage.ts │ │ ├── getCandidate.ts │ │ ├── greetUser.ts │ │ ├── handleLeftChatMember.ts │ │ ├── handleNewChatMembers.ts │ │ ├── index.ts │ │ ├── kickCandidates.ts │ │ ├── kickChatMember.ts │ │ ├── notifyCandidate.ts │ │ └── restrictChatMember.ts │ ├── promo.ts │ ├── report.ts │ ├── restrictedUsers.ts │ ├── saveChatProperty.ts │ ├── strings.ts │ └── stripe.ts ├── index.ts ├── kickChecker.ts ├── messageDeleter.ts ├── middlewares │ ├── attachChatMember.ts │ ├── attachUser.ts │ ├── checkBlockList.ts │ ├── checkIfFromReplier.ts │ ├── checkIfGroup.ts │ ├── checkLock.ts │ ├── checkNoChannelLinks.ts │ ├── checkRestrict.ts │ ├── checkSubscription.ts │ ├── checkSuperAdmin.ts │ ├── checkTime.ts │ ├── logTimeReceived.ts │ ├── messageSaver.ts │ └── messageSaverWorker.ts ├── models │ ├── CappedKickedUser.ts │ ├── CappedMessage.ts │ ├── Chat.ts │ ├── EntryMessage.ts │ ├── MessageToDelete.ts │ ├── VerifiedUser.ts │ └── index.ts ├── server.ts ├── types │ └── telegraf.d.ts └── updateHandler.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | README.md 4 | LICENSE 5 | docker-compose.yml 6 | .env.sample 7 | node_modules 8 | dist 9 | design -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | TOKEN=123 2 | MONGO=mongodb://localhost:27017/shieldy 3 | ADMIN=123 4 | REPORT_CHAT_ID=123 5 | PREMIUM=false 6 | STRIPE_SECRET_KEY=123 7 | MONTHLY_PRICE=123 8 | YEARLY_PRICE=123 9 | LIFETIME_PRICE=123 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: backmeupplz 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | updates.log -------------------------------------------------------------------------------- /.secrets/mongo_admin_password.txt: -------------------------------------------------------------------------------- 1 | mongoadminpassword 2 | -------------------------------------------------------------------------------- /.secrets/mongo_shieldy_password.txt: -------------------------------------------------------------------------------- 1 | shieldyDBpass 2 | -------------------------------------------------------------------------------- /.secrets/shieldy_token.txt: -------------------------------------------------------------------------------- 1 | 1111111111:XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXX 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | RUN apk add --no-cache \ 7 | chromium \ 8 | git \ 9 | nss \ 10 | freetype \ 11 | freetype-dev \ 12 | harfbuzz \ 13 | ca-certificates \ 14 | ttf-freefont 15 | 16 | COPY ./package.json . 17 | COPY ./yarn.lock . 18 | 19 | # Install shieldy dependencies 20 | RUN yarn install \ 21 | && yarn cache clean 22 | 23 | COPY ./tsconfig.json . 24 | COPY ./scripts ./scripts 25 | COPY ./src ./src 26 | COPY ./entrypoint.sh . 27 | 28 | ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt 29 | 30 | CMD ["./entrypoint.sh"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nikita Kolmogorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![@shieldy_bot](/design/banner.png?raw=true)](https://t.me/shieldy_bot) 2 | 3 | # [@shieldy_bot](https://t.me/shieldy_bot) Telegram bot code 4 | 5 | This is the code for the anti-spam Telegram bot I've built. Enjoy and feel free to reuse! 6 | 7 | # Installation 8 | 9 | ## Local launch 10 | 11 | 1. Clone this repo: `git clone https://github.com/backmeupplz/shieldy` 12 | 2. Launch the [mongo database](https://www.mongodb.com/) locally 13 | 3. Create `.env` with the environment variables listed below 14 | 4. Run `yarn install` in the root folder 15 | 5. Run `yarn distribute` 16 | 17 | And you should be good to go! Feel free to fork and submit pull requests. Thanks! 18 | 19 | ## Docker 20 | 21 | 1. Clone this repo: `git clone https://github.com/backmeupplz/shieldy` 22 | 2. Replace the dummy environment variables in `docker-compose.yml` with the ones listed below 23 | 3. Run `docker-compose up -d` 24 | 25 | ## Environment variables 26 | 27 | - `TOKEN` — Telegram bot token 28 | - `MONGO`— URL of the mongo database 29 | - `ADMIN` — Telegram user ID of the bot administrator 30 | - `REPORT_CHAT_ID` — Telegram chat ID of the channel where the bot should report errors 31 | - `PREMIUM` — Whether the bot should be premium or not 32 | - `STRIPE_SECRET_KEY` — Stripe secret key 33 | - `STRIPE_SIGNING_SECRET` — Stripe signing secret 34 | - `MONTHLY_PRICE` — Monthly Stripe price id of the premium 35 | - `YEARLY_PRICE` — Yearly Stripe price id of the premium 36 | - `LIFETIME_PRICE` — Lifetime Stripe price id of the premium 37 | 38 | Also, please, consider looking at `.env.sample`. 39 | 40 | # Continuous integration 41 | 42 | Any commit pushed to master gets deployed to @shieldy_bot via [CI Ninja](https://github.com/backmeupplz/ci-ninja). 43 | 44 | # License 45 | 46 | MIT — use for any purpose. Would be great if you could leave a note about the original developers. Thanks! 47 | -------------------------------------------------------------------------------- /design/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1inch/shieldy/83feef662345e321cf064ebeb8cd3d76e839f390/design/banner.png -------------------------------------------------------------------------------- /design/designs.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1inch/shieldy/83feef662345e321cf064ebeb8cd3d76e839f390/design/designs.sketch -------------------------------------------------------------------------------- /design/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1inch/shieldy/83feef662345e321cf064ebeb8cd3d76e839f390/design/logo.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | shieldy: 5 | build: . 6 | restart: always 7 | environment: 8 | MONGO_SHIELDY_DB: shieldyDB 9 | MONGO_SHIELDY_USERNAME: shieldyDBuser 10 | MONGO_SHIELDY_PASSWORD_FILE: /run/secrets/mongo_shieldy_password 11 | MONGO_INITDB_DATABASE: shieldyDB 12 | TOKEN_FILE: /run/secrets/shieldy_token 13 | ADMIN: 658158536 14 | secrets: 15 | - shieldy_token 16 | - mongo_shieldy_password 17 | depends_on: 18 | - wait-dependencies 19 | mongo: 20 | image: mongo 21 | restart: always 22 | environment: 23 | MONGO_INITDB_ROOT_USERNAME: mongoadmin 24 | MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/mongo_admin_password 25 | MONGO_SHIELDY_USERNAME: shieldyDBuser 26 | MONGO_SHIELDY_PASSWORD_FILE: /run/secrets/mongo_shieldy_password 27 | MONGO_INITDB_DATABASE: shieldyDB 28 | secrets: 29 | - mongo_admin_password 30 | - mongo_shieldy_password 31 | volumes: 32 | - dbdata:/data/db 33 | - ./init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro 34 | wait-dependencies: 35 | image: willwill/wait-for-it:latest 36 | depends_on: 37 | - mongo 38 | entrypoint: /bin/sh 39 | command: > 40 | -c "./wait-for-it.sh mongo:27017 --strict --timeout=1" 41 | # If database debugging is needed 42 | # mongo-express: 43 | # image: mongo-express 44 | # restart: always 45 | # ports: 46 | # - 8081:8081 47 | # environment: 48 | # ME_CONFIG_MONGODB_ADMINUSERNAME: mongoadmin 49 | # ME_CONFIG_MONGODB_ADMINPASSWORD: example 50 | # depends_on: 51 | # - mongo 52 | secrets: 53 | mongo_admin_password: 54 | file: .secrets/mongo_admin_password.txt 55 | mongo_shieldy_password: 56 | file: .secrets/mongo_shieldy_password.txt 57 | shieldy_token: 58 | file: .secrets/shieldy_token.txt 59 | volumes: 60 | dbdata: 61 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -z ${MONGO_SHIELDY_PASSWORD_FILE+x} ]; then 4 | MONGO_SHIELDY_PASSWORD=$(cat ${MONGO_SHIELDY_PASSWORD_FILE}) 5 | fi 6 | 7 | if [ ! -z ${TOKEN_FILE+x} ]; then 8 | TOKEN=$(cat ${TOKEN_FILE}) 9 | export TOKEN 10 | fi 11 | 12 | export MONGO=mongodb://${MONGO_SHIELDY_USERNAME}:${MONGO_SHIELDY_PASSWORD}@mongo:27017/${MONGO_INITDB_DATABASE} 13 | 14 | yarn distribute 15 | 16 | -------------------------------------------------------------------------------- /init-mongo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -Eeuo pipefail 3 | 4 | if [ ! -z ${MONGO_SHIELDY_PASSWORD_FILE+x} ]; then 5 | MONGO_SHIELDY_PASSWORD=$(cat ${MONGO_SHIELDY_PASSWORD_FILE}) 6 | fi 7 | 8 | if [ "$MONGO_SHIELDY_USERNAME" ] && [ "$MONGO_SHIELDY_PASSWORD" ]; then 9 | "${mongo[@]}" -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase "$rootAuthDatabase" "$MONGO_INITDB_DATABASE" <<-EOJS 10 | db.createUser({ 11 | user: $(_js_escape "$MONGO_SHIELDY_USERNAME"), 12 | pwd: $(_js_escape "$MONGO_SHIELDY_PASSWORD"), 13 | roles: [{ role: "readWrite", db: $(_js_escape "$MONGO_INITDB_DATABASE")}] 14 | }); 15 | EOJS 16 | fi 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captcha_bot", 3 | "version": "1.0.0", 4 | "description": "Telegram anti-spam bot", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/backmeupplz/shieldy", 7 | "author": "backmeupplz", 8 | "license": "MIT", 9 | "private": false, 10 | "scripts": { 11 | "distribute": "(yarn build-ts || true) && concurrently -k -p \"[{name}]\" -n \"Bot,Kicker,Deleter\" -c \"yellow.bold,cyan.bold,blue.bold\" \"yarn start-bot\" \"yarn start-kicker\" \"yarn start-deleter\"", 12 | "develop": "concurrently -i -k -p \"[{name}]\" -n \"Bot,Kicker,Deleter,TypeScript\" -c \"yellow.bold,blue.bold,green.bold,cyan.bold\" \"yarn watch-js-bot\" \"yarn watch-js-kicker\" \"yarn watch-js-deleter\" \"yarn watch-ts\"", 13 | "build-ts": "tsc --skipLibCheck", 14 | "watch-ts": "tsc -w --skipLibCheck", 15 | "watch-js-bot": "nodemon --inspect dist/index.js", 16 | "watch-js-kicker": "nodemon dist/kickChecker.js", 17 | "watch-js-deleter": "nodemon dist/messageDeleter.js", 18 | "start-bot": "node --max-old-space-size=12000 dist/index.js", 19 | "start-kicker": "node --max-old-space-size=12000 dist/kickChecker.js", 20 | "start-deleter": "node dist/messageDeleter.js", 21 | "upload-translations": "node scripts/upload.js", 22 | "download-translations": "node scripts/download.js && yarn prettier --single-quote --no-semi --write ./src/helpers/localizations.ts" 23 | }, 24 | "dependencies": { 25 | "@koa/cors": "^3.1.0", 26 | "@typegoose/typegoose": "^7.4.7", 27 | "@types/axios": "^0.14.0", 28 | "@types/dotenv": "^8.2.0", 29 | "@types/lodash": "^4.14.165", 30 | "@types/mongoose": "^5.7.36", 31 | "@types/node": "^14.11.10", 32 | "amala": "^7.0.0", 33 | "axios": "^0.21.1", 34 | "concurrently": "^5.3.0", 35 | "dotenv": "^8.2.0", 36 | "glob": "^7.1.7", 37 | "koa": "^2.13.1", 38 | "koa-bodyparser": "^4.3.0", 39 | "koa-router": "^10.1.1", 40 | "lodash": "^4.17.20", 41 | "module-alias": "^2.2.2", 42 | "mongoose": "^5.10.9", 43 | "sharp": "^0.28.3", 44 | "stripe": "^8.175.0", 45 | "svg-captcha": "^1.4.0", 46 | "tall": "^3.1.0", 47 | "telegraf": "git+https://github.com/backmeupplz/telegraf.git#ccef1dc6c811359d4d36667b57237bfba74841b1", 48 | "telegram-typings": "^5.0.0", 49 | "typescript": "^4.0.3" 50 | }, 51 | "devDependencies": { 52 | "flat": "^5.0.2", 53 | "nodemon": "^2.0.5", 54 | "prettier": "^2.1.2" 55 | }, 56 | "_moduleAliases": { 57 | "@commands": "dist/commands", 58 | "@helpers": "dist/helpers", 59 | "@middlewares": "dist/middlewares", 60 | "@models": "dist/models" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripts/download.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | dotenv.config({ path: `${__dirname}/../.env` }) 3 | const axios = require('axios') 4 | const unflatten = require('flat').unflatten 5 | const fs = require('fs') 6 | 7 | ;(async function getTranslations() { 8 | console.log('==== Getting localizations') 9 | const translations = ( 10 | await axios.get('https://localizer.borodutch.com/localizations') 11 | ).data.filter((l) => { 12 | return l.tags.indexOf('shieldy_bot') > -1 13 | }) 14 | console.log('==== Got localizations:') 15 | console.log(JSON.stringify(translations, undefined, 2)) 16 | // Get flattened map 17 | const flattenedMap = {} // { key: {en: '', ru: ''}} 18 | translations.forEach((t) => { 19 | const key = t.key 20 | const variants = t.variants.filter((v) => !!v.selected) 21 | flattenedMap[key] = variants.reduce((p, c) => { 22 | p[c.language] = c.text 23 | return p 24 | }, {}) 25 | }) 26 | console.log('==== Decoded response:') 27 | console.log(flattenedMap) 28 | const unflattened = unflatten(flattenedMap) 29 | console.log('==== Reversed and unflattened map') 30 | console.log(unflattened) 31 | fs.writeFileSync( 32 | `${__dirname}/../src/helpers/localizations.ts`, 33 | `export const localizations = ${JSON.stringify(unflattened, undefined, 2)}` 34 | ) 35 | console.log('==== Saved object to the file') 36 | })() 37 | -------------------------------------------------------------------------------- /scripts/upload.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | dotenv.config({ path: `${__dirname}/../.env` }) 3 | const axios = require('axios') 4 | const flatten = require('flat') 5 | const fs = require('fs') 6 | fs.copyFileSync( 7 | `${__dirname}/../src/helpers/localizations.ts`, 8 | `${__dirname}/localization.js` 9 | ) 10 | const localizationsFileContent = fs 11 | .readFileSync(`${__dirname}/localization.js`, 'utf8') 12 | .replace('export const localizations', 'module.exports') 13 | fs.writeFileSync(`${__dirname}/localization.js`, localizationsFileContent) 14 | 15 | const localizations = require(`${__dirname}/localization.js`) 16 | 17 | fs.unlinkSync(`${__dirname}/localization.js`) 18 | 19 | const flattenedLocalizations = {} 20 | Object.keys(localizations).forEach((language) => { 21 | flattenedLocalizations[language] = flatten(localizations[language]) 22 | }) 23 | ;(async function postLocalizations() { 24 | console.log('==== Posting body:') 25 | console.log(JSON.stringify(flattenedLocalizations, undefined, 2)) 26 | try { 27 | await axios.post(`https://localizer.borodutch.com/localizations`, { 28 | // await axios.post(`http://localhost:1337/localizations`, { 29 | localizations: flattenedLocalizations, 30 | password: process.env.PASSWORD, 31 | username: 'borodutch', 32 | tags: ['shieldy_bot'], 33 | }) 34 | console.error(`==== Body posted!`) 35 | } catch (err) { 36 | console.error(`==== Error posting: ${err.message}`) 37 | } 38 | })() 39 | -------------------------------------------------------------------------------- /src/commands/1inch.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf, Context } from 'telegraf' 2 | import { strings } from '@helpers/strings' 3 | import { checkLock } from '@middlewares/checkLock' 4 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 5 | 6 | export function setup1inchInfo(bot: Telegraf) { 7 | bot.command(['1inch'], sendInfo) 8 | } 9 | 10 | export function sendInfo(ctx: Context) { 11 | if (ctx.update.message?.date) { 12 | console.log( 13 | 'Replying to 1inch', 14 | Date.now() / 1000 - ctx.update.message?.date 15 | ) 16 | } 17 | 18 | const aboutOneInch = strings(ctx.dbchat, 'oneInchInfo'); 19 | const link = 20 | '[1inch Network](http://1inch.io/?utm_source=shieldy_en&utm_medium=cpc&utm_campaign=powered) ([iOS](https://apps.apple.com/app/apple-store/id1546049391?pt=122481420&ct=shieldy_ru&mt=8))'; 21 | 22 | return ctx.replyWithMarkdown(`${aboutOneInch}\n\n${link}`, { 23 | disable_web_page_preview: false, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/admin.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf, Context } from 'telegraf' 2 | import { checkSuperAdmin } from '@middlewares/checkSuperAdmin' 3 | 4 | export function setupAdmin(bot: Telegraf) { 5 | bot.command('delete', checkSuperAdmin, async (ctx) => { 6 | if ( 7 | !ctx || 8 | !ctx.message || 9 | !ctx.message.reply_to_message || 10 | !ctx.message.reply_to_message.message_id 11 | ) { 12 | return 13 | } 14 | await ctx.telegram.deleteMessage( 15 | ctx.chat.id, 16 | ctx.message.reply_to_message.message_id 17 | ) 18 | await ctx.deleteMessage() 19 | }) 20 | 21 | bot.command('admin', checkSuperAdmin, async (ctx) => { 22 | if ( 23 | !ctx || 24 | !ctx.message || 25 | !ctx.message.reply_to_message || 26 | !ctx.message.reply_to_message.from 27 | ) { 28 | return 29 | } 30 | await ctx.promoteChatMember(ctx.message.reply_to_message.from.id, { 31 | can_change_info: true, 32 | can_post_messages: true, 33 | can_edit_messages: true, 34 | can_delete_messages: true, 35 | can_invite_users: true, 36 | can_restrict_members: true, 37 | can_pin_messages: true, 38 | can_promote_members: true, 39 | }) 40 | await ctx.deleteMessage() 41 | }) 42 | 43 | bot.command('source', checkSuperAdmin, async (ctx) => { 44 | if (!ctx || !ctx.message || !ctx.message.reply_to_message) { 45 | return 46 | } 47 | await ctx.replyWithHTML( 48 | `${JSON.stringify( 49 | ctx.message.reply_to_message, 50 | undefined, 51 | 2 52 | )}` 53 | ) 54 | await ctx.deleteMessage() 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/allowInvitingBots.ts: -------------------------------------------------------------------------------- 1 | import { bot } from '@helpers/bot' 2 | import { kickChatMember } from '@helpers/newcomers/kickChatMember' 3 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 4 | import { Telegraf, Context, Extra } from 'telegraf' 5 | import { strings } from '@helpers/strings' 6 | import { checkLock } from '@middlewares/checkLock' 7 | import { saveChatProperty } from '@helpers/saveChatProperty' 8 | 9 | export function setupAllowInvitingBots(bot: Telegraf) { 10 | bot.command( 11 | 'allowInvitingBots', 12 | checkLock, 13 | clarifyIfPrivateMessages, 14 | async (ctx) => { 15 | let chat = ctx.dbchat 16 | chat.allowInvitingBots = !chat.allowInvitingBots 17 | await saveChatProperty(chat, 'allowInvitingBots') 18 | ctx.replyWithMarkdown( 19 | strings( 20 | ctx.dbchat, 21 | chat.allowInvitingBots 22 | ? 'allowInvitingBots_true' 23 | : 'allowInvitingBots_false' 24 | ), 25 | Extra.inReplyTo(ctx.message.message_id) 26 | ) 27 | } 28 | ) 29 | } 30 | 31 | export function checkAllowInvitingBots(ctx: Context, next: Function) { 32 | // Kick bots if required 33 | if ( 34 | !!ctx.message?.new_chat_members?.length && 35 | !ctx.dbchat.allowInvitingBots 36 | ) { 37 | ctx.message.new_chat_members 38 | .filter((m) => m.is_bot && m.username !== (bot as any).botInfo.username) 39 | .forEach((m) => { 40 | kickChatMember(ctx.dbchat, m) 41 | }) 42 | } 43 | return next() 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/ban.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { modifyCandidates } from '@helpers/candidates' 3 | import { Candidate } from '@models/Chat' 4 | import { modifyRestrictedUsers } from '@helpers/restrictedUsers' 5 | import { isGroup } from '@helpers/isGroup' 6 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 7 | import { Telegraf, Context } from 'telegraf' 8 | import { strings } from '@helpers/strings' 9 | import { checkLock } from '@middlewares/checkLock' 10 | 11 | export function setupBan(bot: Telegraf) { 12 | bot.command('ban', checkLock, clarifyIfPrivateMessages, async (ctx) => { 13 | // Check if reply 14 | if (!ctx.message || !ctx.message.reply_to_message) { 15 | return 16 | } 17 | // Check if not a group 18 | if (!isGroup(ctx)) { 19 | return 20 | } 21 | // Get replied 22 | const repliedId = ctx.message.reply_to_message.from.id 23 | // Check if sent by admin 24 | const admins = await ctx.getChatAdministrators() 25 | if (!admins.map((a) => a.user.id).includes(ctx.from.id)) { 26 | return 27 | } 28 | // Check permissions 29 | const admin = admins.find((v) => v.user.id === ctx.from.id) 30 | if (admin.status !== 'creator' && !admin.can_restrict_members) { 31 | return 32 | } 33 | // Ban in Telegram 34 | await ctx.telegram.kickChatMember(ctx.dbchat.id, repliedId) 35 | // Unrestrict in shieldy 36 | modifyRestrictedUsers(ctx.dbchat, false, [{ id: repliedId } as Candidate]) 37 | // Remove from candidates 38 | const candidate = ctx.dbchat.candidates 39 | .filter((c) => c.id === repliedId) 40 | .pop() 41 | if (candidate) { 42 | // Delete message 43 | await deleteMessageSafeWithBot(ctx.dbchat.id, candidate.messageId) 44 | // Remove from candidates 45 | modifyCandidates(ctx.dbchat, false, [{ id: repliedId } as Candidate]) 46 | } 47 | // Reply with success 48 | await ctx.replyWithMarkdown(strings(ctx.dbchat, 'trust_success')) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/banForFastRepliesToPosts.ts: -------------------------------------------------------------------------------- 1 | import { kickChatMember } from '@helpers/newcomers/kickChatMember' 2 | import { deleteMessageSafe } from '@helpers/deleteMessageSafe' 3 | import { CappedMessageModel } from '@models/CappedMessage' 4 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 5 | import { saveChatProperty } from '@helpers/saveChatProperty' 6 | import { Telegraf, Context, Extra } from 'telegraf' 7 | import { strings } from '@helpers/strings' 8 | import { checkLock } from '@middlewares/checkLock' 9 | 10 | export function setupBanForFastRepliesToPosts(bot: Telegraf) { 11 | // Reply to command 12 | bot.command( 13 | 'banForFastRepliesToPosts', 14 | checkLock, 15 | clarifyIfPrivateMessages, 16 | async (ctx) => { 17 | let chat = ctx.dbchat 18 | chat.banForFastRepliesToPosts = !chat.banForFastRepliesToPosts 19 | await saveChatProperty(chat, 'banForFastRepliesToPosts') 20 | ctx.replyWithMarkdown( 21 | strings( 22 | ctx.dbchat, 23 | chat.banForFastRepliesToPosts 24 | ? 'banForFastRepliesToPosts_true' 25 | : 'banForFastRepliesToPosts_false' 26 | ), 27 | Extra.inReplyTo(ctx.message.message_id) 28 | ) 29 | } 30 | ) 31 | // Save channel messages 32 | bot.use(async (ctx, next) => { 33 | // Check if a channel post 34 | if (ctx.message?.from?.id !== 777000) { 35 | return next() 36 | } 37 | // Check if needs saving 38 | if (!ctx.dbchat.banForFastRepliesToPosts) { 39 | return next() 40 | } 41 | // Save 42 | const message = ctx.message 43 | try { 44 | await new CappedMessageModel({ 45 | message_id: message.message_id, 46 | from_id: message.from.id, 47 | chat_id: message.chat.id, 48 | }).save() 49 | } catch { 50 | // Do nothing 51 | } finally { 52 | return next() 53 | } 54 | }) 55 | // 56 | bot.use(async (ctx, next) => { 57 | // Check if a reply to a channel post 58 | if (!ctx.message || ctx.message?.reply_to_message?.from?.id !== 777000) { 59 | return next() 60 | } 61 | // Check if an admin 62 | if (ctx.isAdministrator) { 63 | return next() 64 | } 65 | // Check if needs checking 66 | if (!ctx.dbchat.banForFastRepliesToPosts) { 67 | return next() 68 | } 69 | // Check the message 70 | const now = Date.now() 71 | try { 72 | // Try to find this channel post 73 | const post = await CappedMessageModel.findOne({ 74 | message_id: ctx.message.reply_to_message.message_id, 75 | from_id: ctx.message.reply_to_message.from.id, 76 | chat_id: ctx.message.reply_to_message.chat.id, 77 | }) 78 | if (!post) { 79 | return next() 80 | } 81 | if (now - post.createdAt.getTime() < 5 * 1000) { 82 | await kickChatMember(ctx.dbchat, ctx.from) 83 | if (ctx.dbchat.deleteEntryOnKick) { 84 | await deleteMessageSafe(ctx) 85 | } 86 | } 87 | } catch { 88 | // Do nothing 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/commands/banNewTelegramUsers.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupBanNewTelegramUsers(bot: Telegraf) { 8 | bot.command( 9 | 'banNewTelegramUsers', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.banNewTelegramUsers = !chat.banNewTelegramUsers 15 | await saveChatProperty(chat, 'banNewTelegramUsers') 16 | ctx.replyWithMarkdown( 17 | strings( 18 | ctx.dbchat, 19 | chat.banNewTelegramUsers 20 | ? 'banNewTelegramUsers_true' 21 | : 'banNewTelegramUsers_false' 22 | ), 23 | Extra.inReplyTo(ctx.message.message_id) 24 | ) 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/banUsers.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupBanUsers(bot: Telegraf) { 8 | bot.command('banUsers', checkLock, clarifyIfPrivateMessages, async (ctx) => { 9 | let chat = ctx.dbchat 10 | chat.banUsers = !chat.banUsers 11 | await saveChatProperty(chat, 'banUsers') 12 | ctx.replyWithMarkdown( 13 | strings(ctx.dbchat, chat.banUsers ? 'banUsers_true' : 'banUsers_false'), 14 | Extra.inReplyTo(ctx.message.message_id) 15 | ) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/buttonText.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupButtonText(bot: Telegraf) { 8 | bot.command( 9 | 'buttonText', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | const text = ctx.message.text.substr(12) 14 | if (!text) { 15 | ctx.dbchat.buttonText = undefined 16 | } else { 17 | ctx.dbchat.buttonText = text 18 | } 19 | await saveChatProperty(ctx.dbchat, 'buttonText') 20 | await ctx.replyWithMarkdown( 21 | strings(ctx.dbchat, 'trust_success'), 22 | Extra.inReplyTo(ctx.message.message_id) 23 | ) 24 | } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/captcha.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkIfFromReplier } from '@middlewares/checkIfFromReplier' 6 | import { CaptchaType } from '@models/Chat' 7 | import { checkLock } from '@middlewares/checkLock' 8 | 9 | export function setupCaptcha(bot: Telegraf) { 10 | bot.command('captcha', checkLock, clarifyIfPrivateMessages, (ctx) => { 11 | ctx.replyWithMarkdown( 12 | strings(ctx.dbchat, 'captcha'), 13 | Extra.inReplyTo(ctx.message.message_id).markup((m) => 14 | m.inlineKeyboard([ 15 | m.callbackButton(strings(ctx.dbchat, 'simple'), 'simple'), 16 | m.callbackButton(strings(ctx.dbchat, 'digits'), 'digits'), 17 | m.callbackButton(strings(ctx.dbchat, 'button'), 'button'), 18 | m.callbackButton(strings(ctx.dbchat, 'image'), 'image'), 19 | ]) 20 | ) 21 | ) 22 | }) 23 | 24 | bot.action( 25 | ['simple', 'digits', 'button', 'image'], 26 | checkIfFromReplier, 27 | async (ctx) => { 28 | let chat = ctx.dbchat 29 | chat.captchaType = ctx.callbackQuery.data as CaptchaType 30 | await saveChatProperty(chat, 'captchaType') 31 | const message = ctx.callbackQuery.message 32 | 33 | ctx.telegram.editMessageText( 34 | message.chat.id, 35 | message.message_id, 36 | undefined, 37 | `${strings(chat, 'captcha_selected')} (${strings( 38 | chat, 39 | chat.captchaType 40 | )})` 41 | ) 42 | } 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/captchaMessage.ts: -------------------------------------------------------------------------------- 1 | import { clarifyReply } from '@helpers/clarifyReply' 2 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 3 | import { saveChatProperty } from '@helpers/saveChatProperty' 4 | import { Telegraf, Context, Extra } from 'telegraf' 5 | import { strings, localizations } from '@helpers/strings' 6 | import { checkLock } from '@middlewares/checkLock' 7 | import { report } from '@helpers/report' 8 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 9 | 10 | export function setupCaptchaMessage(bot: Telegraf) { 11 | // Setup command 12 | bot.command( 13 | 'customCaptchaMessage', 14 | checkLock, 15 | clarifyIfPrivateMessages, 16 | async (ctx) => { 17 | let chat = ctx.dbchat 18 | chat.customCaptchaMessage = !chat.customCaptchaMessage 19 | await saveChatProperty(chat, 'customCaptchaMessage') 20 | await ctx.replyWithMarkdown( 21 | strings( 22 | ctx.dbchat, 23 | chat.customCaptchaMessage 24 | ? chat.captchaMessage 25 | ? 'captchaMessage_true_message' 26 | : 'captchaMessage_true' 27 | : 'captchaMessage_false' 28 | ), 29 | Extra.inReplyTo(ctx.message.message_id) 30 | ) 31 | if (chat.customCaptchaMessage && chat.captchaMessage) { 32 | chat.captchaMessage.message.chat = undefined 33 | await ctx.telegram.sendCopy(chat.id, chat.captchaMessage.message, { 34 | entities: chat.captchaMessage.message.entities, 35 | }) 36 | } 37 | await clarifyReply(ctx) 38 | } 39 | ) 40 | // Setup checker 41 | bot.use(async (ctx, next) => { 42 | try { 43 | // Check if needs to check 44 | if (!ctx.dbchat.customCaptchaMessage) { 45 | return 46 | } 47 | // Check if reply 48 | if (!ctx.message || !ctx.message.reply_to_message) { 49 | return 50 | } 51 | // Check if text 52 | if (!ctx.message.text) { 53 | return 54 | } 55 | // Check if reply to shieldy 56 | if ( 57 | !ctx.message.reply_to_message.from || 58 | !ctx.message.reply_to_message.from.username || 59 | ctx.message.reply_to_message.from.username !== 60 | (bot as any).botInfo.username 61 | ) { 62 | return 63 | } 64 | // Check if reply to the correct message 65 | const captchaMessages = Object.keys(localizations.captchaMessage_true) 66 | .map((k) => localizations.captchaMessage_true[k]) 67 | .concat( 68 | Object.keys(localizations.captchaMessage_true_message).map( 69 | (k) => localizations.captchaMessage_true_message[k] 70 | ) 71 | ) 72 | if ( 73 | !ctx.message.reply_to_message.text || 74 | captchaMessages.indexOf(ctx.message.reply_to_message.text) < 0 75 | ) { 76 | return 77 | } 78 | // Save text 79 | ctx.dbchat.captchaMessage = { 80 | message: ctx.message, 81 | } 82 | await saveChatProperty(ctx.dbchat, 'captchaMessage') 83 | ctx.reply( 84 | strings(ctx.dbchat, 'greetsUsers_message_accepted'), 85 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 86 | ) 87 | } catch (err) { 88 | report(err, setupCaptchaMessage.name) 89 | } finally { 90 | next() 91 | } 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /src/commands/cas.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupCAS(bot: Telegraf) { 8 | bot.command('cas', checkLock, clarifyIfPrivateMessages, async (ctx) => { 9 | let chat = ctx.dbchat 10 | chat.cas = !chat.cas 11 | await saveChatProperty(chat, 'cas') 12 | ctx.replyWithMarkdown( 13 | strings(ctx.dbchat, chat.cas ? 'cas_true' : 'cas_false'), 14 | Extra.inReplyTo(ctx.message.message_id) 15 | ) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/deleteEntryMessages.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupDeleteEntryMessages(bot: Telegraf) { 8 | bot.command( 9 | 'deleteEntryMessages', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.deleteEntryMessages = !chat.deleteEntryMessages 15 | await saveChatProperty(chat, 'deleteEntryMessages') 16 | ctx.replyWithMarkdown( 17 | strings( 18 | ctx.dbchat, 19 | chat.deleteEntryMessages 20 | ? 'deleteEntryMessages_true' 21 | : 'deleteEntryMessages_false' 22 | ), 23 | Extra.inReplyTo(ctx.message.message_id) 24 | ) 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/deleteEntryOnKick.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupDeleteEntryOnKick(bot: Telegraf) { 8 | bot.command( 9 | 'deleteEntryOnKick', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.deleteEntryOnKick = !chat.deleteEntryOnKick 15 | await saveChatProperty(chat, 'deleteEntryOnKick') 16 | ctx.replyWithMarkdown( 17 | strings( 18 | ctx.dbchat, 19 | chat.deleteEntryOnKick 20 | ? 'deleteEntryOnKick_true' 21 | : 'deleteEntryOnKick_false' 22 | ), 23 | Extra.inReplyTo(ctx.message.message_id) 24 | ) 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/deleteGreetingTime.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { checkLock } from '@middlewares/checkLock' 5 | import { strings } from '@helpers/strings' 6 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 7 | 8 | export function setupDeleteGreetingTime(bot: Telegraf) { 9 | bot.command( 10 | 'deleteGreetingTime', 11 | checkLock, 12 | clarifyIfPrivateMessages, 13 | async (ctx) => { 14 | // Check if limit is set 15 | const limitNumber = 16 | +ctx.message.text.substr(19).trim() || 17 | +ctx.message.text 18 | .substr(20 + (bot as any).botInfo.username.length) 19 | .trim() 20 | if (!isNaN(limitNumber) && limitNumber > 0 && limitNumber < 100000) { 21 | ctx.dbchat.deleteGreetingTime = limitNumber 22 | await saveChatProperty(ctx.dbchat, 'deleteGreetingTime') 23 | ctx.reply( 24 | strings(ctx.dbchat, 'greetsUsers_message_accepted'), 25 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 26 | ) 27 | } else { 28 | ctx.dbchat.deleteGreetingTime = undefined 29 | await saveChatProperty(ctx.dbchat, 'deleteGreetingTime') 30 | ctx.reply( 31 | `${strings(ctx.dbchat, 'greetsUsers_message_accepted')} 0`, 32 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 33 | ) 34 | } 35 | } 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/greeting.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings, localizations } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | import { report } from '@helpers/report' 7 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 8 | import { clarifyReply } from '@helpers/clarifyReply' 9 | 10 | export function setupGreeting(bot: Telegraf) { 11 | // Setup command 12 | bot.command('greeting', checkLock, clarifyIfPrivateMessages, async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.greetsUsers = !chat.greetsUsers 15 | await saveChatProperty(chat, 'greetsUsers') 16 | await ctx.replyWithMarkdown( 17 | strings( 18 | ctx.dbchat, 19 | chat.greetsUsers 20 | ? chat.greetingMessage 21 | ? 'greetsUsers_true_message' 22 | : 'greetsUsers_true' 23 | : 'greetsUsers_false' 24 | ), 25 | Extra.inReplyTo(ctx.message.message_id) 26 | ) 27 | if (chat.greetingMessage && chat.greetsUsers) { 28 | chat.greetingMessage.message.chat = undefined 29 | await ctx.telegram.sendCopy(chat.id, chat.greetingMessage.message, { 30 | entities: chat.greetingMessage.message.entities, 31 | }) 32 | } 33 | await clarifyReply(ctx) 34 | }) 35 | // Setup checker 36 | bot.use(async (ctx, next) => { 37 | try { 38 | // Check if needs to check 39 | if (!ctx.dbchat.greetsUsers) { 40 | return 41 | } 42 | // Check if reply 43 | if (!ctx.message || !ctx.message.reply_to_message) { 44 | return 45 | } 46 | // Check if text 47 | if (!ctx.message.text) { 48 | return 49 | } 50 | // Check if reply to shieldy 51 | if ( 52 | !ctx.message.reply_to_message.from || 53 | !ctx.message.reply_to_message.from.username || 54 | ctx.message.reply_to_message.from.username !== 55 | (bot as any).botInfo.username 56 | ) { 57 | return 58 | } 59 | // Check if reply to the correct message 60 | const greetingMessages = Object.keys(localizations.greetsUsers_true) 61 | .map((k) => localizations.greetsUsers_true[k]) 62 | .concat( 63 | Object.keys(localizations.greetsUsers_true_message).map( 64 | (k) => localizations.greetsUsers_true_message[k] 65 | ) 66 | ) 67 | if ( 68 | !ctx.message.reply_to_message.text || 69 | greetingMessages.indexOf(ctx.message.reply_to_message.text) < 0 70 | ) { 71 | return 72 | } 73 | // Save text 74 | ctx.dbchat.greetingMessage = { 75 | message: ctx.message, 76 | } 77 | await saveChatProperty(ctx.dbchat, 'greetingMessage') 78 | ctx.reply( 79 | strings(ctx.dbchat, 'greetsUsers_message_accepted'), 80 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 81 | ) 82 | } catch (err) { 83 | report(err, setupGreeting.name) 84 | } finally { 85 | next() 86 | } 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /src/commands/greetingButtons.ts: -------------------------------------------------------------------------------- 1 | import { clarifyReply } from '@helpers/clarifyReply' 2 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 3 | import { saveChatProperty } from '@helpers/saveChatProperty' 4 | import { Telegraf, Context, Extra } from 'telegraf' 5 | import { strings, localizations } from '@helpers/strings' 6 | import { checkLock } from '@middlewares/checkLock' 7 | import { report } from '@helpers/report' 8 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 9 | 10 | export function setupGreetingButtons(bot: Telegraf) { 11 | // Setup command 12 | bot.command( 13 | 'greetingButtons', 14 | checkLock, 15 | clarifyIfPrivateMessages, 16 | async (ctx) => { 17 | await ctx.replyWithMarkdown( 18 | `${strings(ctx.dbchat, 'greetingButtons')}`, 19 | Extra.inReplyTo(ctx.message.message_id).webPreview(false) 20 | ) 21 | await ctx.replyWithMarkdown( 22 | `${ 23 | ctx.dbchat.greetingButtons || 24 | strings(ctx.dbchat, 'greetingButtonsEmpty') 25 | }`, 26 | Extra.webPreview(false).HTML(true) 27 | ) 28 | await clarifyReply(ctx) 29 | } 30 | ) 31 | // Setup checker 32 | bot.use(async (ctx, next) => { 33 | try { 34 | // Check if reply 35 | if (!ctx.message || !ctx.message.reply_to_message) { 36 | return 37 | } 38 | // Check if text 39 | if (!ctx.message.text) { 40 | return 41 | } 42 | // Check if reply to shieldy 43 | if ( 44 | !ctx.message.reply_to_message.from || 45 | !ctx.message.reply_to_message.from.username || 46 | ctx.message.reply_to_message.from.username !== 47 | (bot as any).botInfo.username 48 | ) { 49 | return 50 | } 51 | // Check if reply to the correct message 52 | const greetingButtonsMessages = Object.keys( 53 | localizations.greetingButtons 54 | ).map((k) => localizations.greetingButtons[k]) 55 | if ( 56 | !ctx.message.reply_to_message.text || 57 | greetingButtonsMessages.indexOf(ctx.message.reply_to_message.text) < 0 58 | ) { 59 | return 60 | } 61 | // Check format 62 | const components = ctx.message.text.split('\n') 63 | let result = [] 64 | for (const component of components) { 65 | const parts = component.split(' - ') 66 | if (parts.length !== 2) { 67 | // Default 68 | ctx.dbchat.greetingButtons = undefined 69 | await saveChatProperty(ctx.dbchat, 'greetingButtons') 70 | return 71 | } else { 72 | result.push(component) 73 | } 74 | } 75 | // Save text 76 | ctx.dbchat.greetingButtons = result.join('\n') 77 | await saveChatProperty(ctx.dbchat, 'greetingButtons') 78 | ctx.reply( 79 | strings(ctx.dbchat, 'greetsUsers_message_accepted'), 80 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 81 | ) 82 | } catch (err) { 83 | report(err, setupGreetingButtons.name) 84 | } finally { 85 | next() 86 | } 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf, Context } from 'telegraf' 2 | import { strings } from '@helpers/strings' 3 | import { checkLock } from '@middlewares/checkLock' 4 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 5 | 6 | export function setupHelp(bot: Telegraf) { 7 | bot.command(['help', 'start'], checkLock, clarifyIfPrivateMessages, sendHelp) 8 | } 9 | 10 | export function sendHelp(ctx: Context) { 11 | if (ctx.update.message?.date) { 12 | console.log( 13 | 'Replying to help', 14 | Date.now() / 1000 - ctx.update.message?.date 15 | ) 16 | } 17 | return ctx.replyWithMarkdown(getHelpText(ctx), { 18 | disable_web_page_preview: true, 19 | }) 20 | } 21 | 22 | export function sendHelpSafe(ctx: Context) { 23 | try { 24 | return ctx.replyWithMarkdown(getHelpText(ctx), { 25 | disable_web_page_preview: true, 26 | }) 27 | } catch { 28 | // Do nothing 29 | } 30 | } 31 | 32 | function getHelpText(ctx: Context) { 33 | let text = strings(ctx.dbchat, 'helpShieldy') 34 | if (process.env.PREMIUM === 'true') { 35 | text += '\n\n' + strings(ctx.dbchat, 'subscription') 36 | } 37 | return text 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/language.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { Language } from '@models/Chat' 6 | import { checkIfFromReplier } from '@middlewares/checkIfFromReplier' 7 | import { checkLock } from '@middlewares/checkLock' 8 | 9 | export function setupLanguage(bot: Telegraf) { 10 | bot.command('language', checkLock, clarifyIfPrivateMessages, (ctx) => { 11 | ctx.replyWithMarkdown( 12 | strings(ctx.dbchat, 'language_shieldy'), 13 | Extra.webPreview(false) 14 | .inReplyTo(ctx.message.message_id) 15 | .markup((m) => 16 | m.inlineKeyboard([ 17 | [ 18 | m.callbackButton('English', 'en'), 19 | m.callbackButton('Русский', 'ru'), 20 | ], 21 | [ 22 | m.callbackButton('Italiano', 'it'), 23 | m.callbackButton('Eesti', 'et'), 24 | ], 25 | [ 26 | m.callbackButton('Українська', 'uk'), 27 | m.callbackButton('Português Brasil', 'br'), 28 | ], 29 | [ 30 | m.callbackButton('Español', 'es'), 31 | m.callbackButton('Chinese', 'zh'), 32 | ], 33 | [ 34 | m.callbackButton('Norwegian', 'no'), 35 | m.callbackButton('Deutsch', 'de'), 36 | ], 37 | [ 38 | m.callbackButton('Taiwan', 'tw'), 39 | m.callbackButton('French', 'fr'), 40 | ], 41 | [ 42 | m.callbackButton('Indonesian', 'id'), 43 | m.callbackButton('Korean', 'ko'), 44 | ], 45 | [ 46 | m.callbackButton('Amharic', 'am'), 47 | m.callbackButton('Czech', 'cz'), 48 | ], 49 | [ 50 | m.callbackButton('Arabic', 'ar'), 51 | m.callbackButton('Türkçe', 'tr'), 52 | ], 53 | [ 54 | m.callbackButton('Romanian', 'ro'), 55 | m.callbackButton('Japanese', 'ja'), 56 | ], 57 | [ 58 | m.callbackButton('Slovak', 'sk'), 59 | m.callbackButton('Catalan', 'ca'), 60 | ], 61 | [ 62 | m.callbackButton('Cantonese', 'yue'), 63 | m.callbackButton('Hungarian', 'hu'), 64 | ], 65 | [ 66 | m.callbackButton('Finnish', 'fi'), 67 | m.callbackButton('Bulgarian', 'bg'), 68 | ], 69 | ]) 70 | ) 71 | ) 72 | }) 73 | 74 | bot.action( 75 | [ 76 | 'en', 77 | 'ru', 78 | 'it', 79 | 'et', 80 | 'uk', 81 | 'br', 82 | 'tr', 83 | 'es', 84 | 'zh', 85 | 'no', 86 | 'de', 87 | 'tw', 88 | 'fr', 89 | 'id', 90 | 'ko', 91 | 'am', 92 | 'cz', 93 | 'ar', 94 | 'ja', 95 | 'ro', 96 | 'sk', 97 | 'ca', 98 | 'yue', 99 | 'hu', 100 | 'fi', 101 | 'bg', 102 | ], 103 | checkIfFromReplier, 104 | async (ctx) => { 105 | let chat = ctx.dbchat 106 | chat.language = ctx.callbackQuery.data as Language 107 | await saveChatProperty(chat, 'language') 108 | const message = ctx.callbackQuery.message 109 | 110 | ctx.telegram.editMessageText( 111 | message.chat.id, 112 | message.message_id, 113 | undefined, 114 | strings(chat, 'language_selected') 115 | ) 116 | } 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /src/commands/lock.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupLock(bot: Telegraf) { 8 | bot.command('lock', checkLock, clarifyIfPrivateMessages, async (ctx) => { 9 | let chat = ctx.dbchat 10 | chat.adminLocked = !chat.adminLocked 11 | await saveChatProperty(chat, 'adminLocked') 12 | ctx.replyWithMarkdown( 13 | strings( 14 | ctx.dbchat, 15 | chat.adminLocked ? 'lock_true_shieldy' : 'lock_false_shieldy' 16 | ), 17 | Extra.inReplyTo(ctx.message.message_id) 18 | ) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/noAttack.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupNoAttack(bot: Telegraf) { 8 | bot.command('noAttack', checkLock, clarifyIfPrivateMessages, async (ctx) => { 9 | ctx.dbchat.noAttack = !ctx.dbchat.noAttack 10 | await saveChatProperty(ctx.dbchat, 'noAttack') 11 | ctx.replyWithMarkdown( 12 | strings( 13 | ctx.dbchat, 14 | ctx.dbchat.noAttack ? 'noAttack_true' : 'noAttack_false' 15 | ), 16 | Extra.inReplyTo(ctx.message.message_id) 17 | ) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/noChannelLinks.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupNoChannelLinks(bot: Telegraf) { 8 | bot.command( 9 | 'noChannelLinks', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.noChannelLinks = !chat.noChannelLinks 15 | await saveChatProperty(chat, 'noChannelLinks') 16 | await saveChatProperty(chat, '') 17 | ctx.replyWithMarkdown( 18 | strings( 19 | ctx.dbchat, 20 | chat.noChannelLinks ? 'noChannelLinks_true' : 'noChannelLinks_false' 21 | ), 22 | Extra.inReplyTo(ctx.message.message_id) 23 | ) 24 | } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/restrict.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupRestrict(bot: Telegraf) { 8 | bot.command('restrict', checkLock, clarifyIfPrivateMessages, async (ctx) => { 9 | let chat = ctx.dbchat 10 | chat.restrict = !chat.restrict 11 | await saveChatProperty(chat, 'restrict') 12 | ctx.replyWithMarkdown( 13 | strings(ctx.dbchat, chat.restrict ? 'restrict_true' : 'restrict_false'), 14 | Extra.inReplyTo(ctx.message.message_id) 15 | ) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/restrictTime.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { checkLock } from '@middlewares/checkLock' 5 | import { strings } from '@helpers/strings' 6 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 7 | 8 | export function setupRestrictTime(bot: Telegraf) { 9 | bot.command( 10 | 'restrictTime', 11 | checkLock, 12 | clarifyIfPrivateMessages, 13 | async (ctx) => { 14 | // Check if limit is set 15 | const limitNumber = 16 | +ctx.message.text.substr('/restrictTime'.length).trim() || 17 | +ctx.message.text 18 | .substr( 19 | '/restrictTime@'.length + (bot as any).botInfo.username.length 20 | ) 21 | .trim() 22 | if (!isNaN(limitNumber) && limitNumber > 0 && limitNumber < 745) { 23 | // roughly 31 days 24 | ctx.dbchat.restrictTime = limitNumber 25 | await saveChatProperty(ctx.dbchat, 'restrictTime') 26 | ctx.reply( 27 | strings(ctx.dbchat, 'greetsUsers_message_accepted'), 28 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 29 | ) 30 | } else { 31 | ctx.dbchat.restrictTime = 24 32 | await saveChatProperty(ctx.dbchat, 'restrictTime') 33 | ctx.reply( 34 | `${strings(ctx.dbchat, 'greetsUsers_message_accepted')} 0`, 35 | Extra.inReplyTo(ctx.message.message_id) as ExtraReplyMessage 36 | ) 37 | } 38 | } 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/setConfig.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 3 | import { Language, CaptchaType } from '@models/Chat' 4 | import { Telegraf, Context, Extra } from 'telegraf' 5 | import { checkLock } from '@middlewares/checkLock' 6 | import { sendCurrentConfig } from '@commands/viewConfig' 7 | import { strings } from '@helpers/strings' 8 | 9 | export function setupSetConfig(bot: Telegraf) { 10 | bot.command('setConfig', checkLock, clarifyIfPrivateMessages, async (ctx) => { 11 | try { 12 | const configText = ctx.message.text 13 | .replace(`/setConfig@${(bot as any).botInfo.username}`, '') 14 | .replace('/setConfig', '') 15 | .trim() 16 | console.log(configText) 17 | if (!configText) { 18 | ctx.reply( 19 | strings(ctx.dbchat, 'setConfigHelp'), 20 | Extra.inReplyTo(ctx.message.message_id).HTML( 21 | true 22 | ) as ExtraReplyMessage 23 | ) 24 | return 25 | } 26 | const configMap = configText 27 | .split('\n') 28 | .map((c) => { 29 | const configComponents = c.split(': ') 30 | return configComponents.length < 2 31 | ? undefined 32 | : { key: configComponents[0], value: configComponents[1] } 33 | }) 34 | .filter((v) => !!v) 35 | .reduce((p, c) => { 36 | const result = p 37 | result[c.key] = c.value 38 | return result 39 | }, {}) as { [index: string]: string } 40 | 41 | for (const key in configMap) { 42 | const value = configMap[key] 43 | switch (key) { 44 | case 'language': { 45 | // Validated by db 46 | ctx.dbchat.language = value as Language 47 | break 48 | } 49 | case 'captchaType': { 50 | // Validated by db 51 | ctx.dbchat.captchaType = value as CaptchaType 52 | break 53 | } 54 | case 'timeGiven': { 55 | const numValue = +value 56 | if (!isNaN(numValue) && numValue > 0 && numValue < 100000) { 57 | ctx.dbchat.timeGiven = numValue 58 | } 59 | break 60 | } 61 | case 'adminLocked': { 62 | const boolValue = value === 'true' 63 | ctx.dbchat.adminLocked = boolValue 64 | break 65 | } 66 | case 'restrict': { 67 | const boolValue = value === 'true' 68 | ctx.dbchat.restrict = boolValue 69 | break 70 | } 71 | case 'noChannelLinks': { 72 | const boolValue = value === 'true' 73 | ctx.dbchat.noChannelLinks = boolValue 74 | break 75 | } 76 | case 'deleteEntryMessages': { 77 | const boolValue = value === 'true' 78 | ctx.dbchat.deleteEntryMessages = boolValue 79 | break 80 | } 81 | case 'greetsUsers': { 82 | const boolValue = value === 'true' 83 | ctx.dbchat.greetsUsers = boolValue 84 | break 85 | } 86 | case 'customCaptchaMessage': { 87 | const boolValue = value === 'true' 88 | ctx.dbchat.customCaptchaMessage = boolValue 89 | break 90 | } 91 | case 'strict': { 92 | const boolValue = value === 'true' 93 | ctx.dbchat.strict = boolValue 94 | break 95 | } 96 | case 'deleteGreetingTime': { 97 | const numValue = +value 98 | if (!isNaN(numValue) && numValue > 0 && numValue < 100000) { 99 | ctx.dbchat.deleteGreetingTime = numValue 100 | } 101 | break 102 | } 103 | case 'banUsers': { 104 | const boolValue = value === 'true' 105 | ctx.dbchat.banUsers = boolValue 106 | break 107 | } 108 | case 'deleteEntryOnKick': { 109 | const boolValue = value === 'true' 110 | ctx.dbchat.deleteEntryOnKick = boolValue 111 | break 112 | } 113 | case 'cas': { 114 | const boolValue = value === 'true' 115 | ctx.dbchat.cas = boolValue 116 | break 117 | } 118 | case 'underAttack': { 119 | const boolValue = value === 'true' 120 | ctx.dbchat.underAttack = boolValue 121 | break 122 | } 123 | case 'noAttack': { 124 | const boolValue = value === 'true' 125 | ctx.dbchat.noAttack = boolValue 126 | break 127 | } 128 | case 'buttonText': { 129 | ctx.dbchat.buttonText = value 130 | break 131 | } 132 | case 'allowInvitingBots': { 133 | const boolValue = value === 'true' 134 | ctx.dbchat.allowInvitingBots = boolValue 135 | break 136 | } 137 | case 'skipOldUsers': { 138 | const boolValue = value === 'true' 139 | ctx.dbchat.skipOldUsers = boolValue 140 | break 141 | } 142 | case 'skipVerifiedUsers': { 143 | const boolValue = value === 'true' 144 | ctx.dbchat.skipVerifiedUsers = boolValue 145 | break 146 | } 147 | default: 148 | break 149 | } 150 | } 151 | await ctx.dbchat.save() 152 | await ctx.reply( 153 | '👍', 154 | Extra.inReplyTo(ctx.message.message_id).HTML(true) as ExtraReplyMessage 155 | ) 156 | await sendCurrentConfig(ctx, ctx.dbchat) 157 | } catch (err) { 158 | await ctx.reply( 159 | '👎', 160 | Extra.inReplyTo(ctx.message.message_id).HTML(true) as ExtraReplyMessage 161 | ) 162 | await ctx.reply( 163 | err.message, 164 | Extra.inReplyTo(ctx.message.message_id).HTML(true) as ExtraReplyMessage 165 | ) 166 | } 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /src/commands/skipOldUsers.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupSkipOldUsers(bot: Telegraf) { 8 | bot.command( 9 | 'skipOldUsers', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.skipOldUsers = !chat.skipOldUsers 15 | await saveChatProperty(chat, 'skipOldUsers') 16 | ctx.replyWithMarkdown( 17 | strings( 18 | ctx.dbchat, 19 | chat.skipOldUsers ? 'skipOldUsers_true' : 'skipOldUsers_false' 20 | ), 21 | Extra.inReplyTo(ctx.message.message_id) 22 | ) 23 | } 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/skipVerifiedUsers.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupSkipVerifiedUsers(bot: Telegraf) { 8 | bot.command( 9 | 'skipVerifiedUsers', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | let chat = ctx.dbchat 14 | chat.skipVerifiedUsers = !chat.skipVerifiedUsers 15 | await saveChatProperty(chat, 'skipVerifiedUsers') 16 | ctx.replyWithMarkdown( 17 | strings( 18 | ctx.dbchat, 19 | chat.skipVerifiedUsers 20 | ? 'skipVerifiedUsers_true' 21 | : 'skipVerifiedUsers_false' 22 | ), 23 | Extra.inReplyTo(ctx.message.message_id) 24 | ) 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/strict.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupStrict(bot: Telegraf) { 8 | bot.command('strict', checkLock, clarifyIfPrivateMessages, async (ctx) => { 9 | let chat = ctx.dbchat 10 | chat.strict = !chat.strict 11 | await saveChatProperty(chat, 'strict') 12 | ctx.replyWithMarkdown( 13 | strings(ctx.dbchat, chat.strict ? 'strict_true' : 'strict_false'), 14 | Extra.inReplyTo(ctx.message.message_id) 15 | ) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/subscription.ts: -------------------------------------------------------------------------------- 1 | import { strings } from '@helpers/strings' 2 | import { checkIfGroup } from '@middlewares/checkIfGroup' 3 | import { Telegraf, Context } from 'telegraf' 4 | import { checkLock } from '@middlewares/checkLock' 5 | import { stripe } from '@helpers/stripe' 6 | import { Language } from '@models/Chat' 7 | import Stripe from 'stripe' 8 | 9 | const prices = { 10 | monthly: process.env.MONTHLY_PRICE, 11 | yearly: process.env.YEARLY_PRICE, 12 | lifetime: process.env.LIFETIME_PRICE, 13 | } 14 | 15 | export function setupSubscription(bot: Telegraf) { 16 | bot.command('subscription', checkIfGroup, checkLock, async (ctx) => { 17 | if (!ctx.dbchat.subscriptionId) { 18 | return sendSubscriptionButtons(ctx) 19 | } 20 | if ( 21 | ctx.from.id !== parseInt(process.env.ADMIN) && 22 | ctx.from?.username !== 'GroupAnonymousBot' && 23 | !ctx.isAdministrator 24 | ) { 25 | try { 26 | await ctx.deleteMessage() 27 | } catch { 28 | // do nothing 29 | } 30 | return 31 | } 32 | return sendManageSubscription(ctx) 33 | }) 34 | } 35 | 36 | export async function sendSubscriptionButtons(ctx: Context) { 37 | await ctx.telegram.sendChatAction(ctx.chat.id, 'typing') 38 | return ctx.reply(strings(ctx.dbchat, 'noSubscription'), { 39 | reply_markup: { 40 | inline_keyboard: await subscriptionsKeyboard(ctx), 41 | }, 42 | }) 43 | } 44 | 45 | export async function sendManageSubscription(ctx: Context) { 46 | const keyboard = [] 47 | const subscription = await stripe.subscriptions.retrieve( 48 | ctx.dbchat.subscriptionId 49 | ) 50 | const customerId = subscription.customer as string 51 | const url = ( 52 | await stripe.billingPortal.sessions.create({ 53 | customer: customerId, 54 | }) 55 | ).url 56 | keyboard.push([ 57 | { 58 | text: strings(ctx.dbchat, 'manageSubscription'), 59 | url, 60 | }, 61 | ]) 62 | return ctx.reply(strings(ctx.dbchat, 'manageSubscription'), { 63 | reply_markup: { 64 | inline_keyboard: keyboard, 65 | }, 66 | }) 67 | } 68 | 69 | async function subscriptionsKeyboard(ctx: Context) { 70 | let unsafeLocale = ctx.dbchat.language || 'en' 71 | if (unsafeLocale === Language.UKRAINIAN) { 72 | unsafeLocale = 'en' 73 | } 74 | const locale = unsafeLocale as Stripe.Checkout.Session.Locale 75 | const getBackUrl = 'https://t.me/shieldy_premium_bot' 76 | const monthlySession = await stripe.checkout.sessions.create({ 77 | payment_method_types: ['card'], 78 | line_items: [ 79 | { 80 | price: prices.monthly, 81 | quantity: 1, 82 | }, 83 | ], 84 | success_url: getBackUrl, 85 | cancel_url: getBackUrl, 86 | client_reference_id: `${ctx.chat.id}`, 87 | locale, 88 | mode: 'subscription', 89 | allow_promotion_codes: true, 90 | }) 91 | const yearlySession = await stripe.checkout.sessions.create({ 92 | payment_method_types: ['card'], 93 | line_items: [ 94 | { 95 | price: prices.yearly, 96 | quantity: 1, 97 | }, 98 | ], 99 | success_url: getBackUrl, 100 | cancel_url: getBackUrl, 101 | client_reference_id: `${ctx.chat.id}`, 102 | locale, 103 | mode: 'subscription', 104 | allow_promotion_codes: true, 105 | }) 106 | const lifetimeSession = await stripe.checkout.sessions.create({ 107 | payment_method_types: ['card'], 108 | line_items: [ 109 | { 110 | price: prices.lifetime, 111 | quantity: 1, 112 | }, 113 | ], 114 | success_url: getBackUrl, 115 | cancel_url: getBackUrl, 116 | client_reference_id: `${ctx.chat.id}`, 117 | locale, 118 | mode: 'payment', 119 | allow_promotion_codes: true, 120 | }) 121 | return [ 122 | [ 123 | { 124 | text: `$15/${strings(ctx.dbchat, 'monthly')}`, 125 | url: monthlySession.url, 126 | }, 127 | ], 128 | [ 129 | { 130 | text: `$108/${strings(ctx.dbchat, 'yearly')}`, 131 | url: yearlySession.url, 132 | }, 133 | ], 134 | [ 135 | { 136 | text: `$450 ${strings(ctx.dbchat, 'lifetime')}`, 137 | url: lifetimeSession.url, 138 | }, 139 | ], 140 | ] 141 | } 142 | -------------------------------------------------------------------------------- /src/commands/testLocales.ts: -------------------------------------------------------------------------------- 1 | import { Telegraf, Context, Extra } from 'telegraf' 2 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 3 | import { Chat } from '@models/Chat' 4 | 5 | export function setupTestLocales(bot: Telegraf) { 6 | bot.command('testLocales', async (ctx) => { 7 | if (ctx.from.id !== 76104711) { 8 | return 9 | } 10 | const { strings, localizations } = require('../helpers/strings') 11 | 12 | for (const key of Object.keys(localizations)) { 13 | const value = localizations[key] 14 | 15 | for (const locale of Object.keys(value)) { 16 | try { 17 | await ctx.reply( 18 | strings({ language: locale } as Chat, key), 19 | Extra.markdown(true) as ExtraReplyMessage 20 | ) 21 | } catch (err) { 22 | console.error(key, locale, err.message) 23 | } 24 | } 25 | } 26 | console.log('Done') 27 | return ctx.reply('Done') 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/timeLimit.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkIfFromReplier } from '@middlewares/checkIfFromReplier' 6 | import { checkLock } from '@middlewares/checkLock' 7 | 8 | const options = [ 9 | ['10', '20', '30'], 10 | ['60', '120', '240'], 11 | ] 12 | 13 | export function setupTimeLimit(bot: Telegraf) { 14 | bot.command('timeLimit', checkLock, clarifyIfPrivateMessages, async (ctx) => { 15 | // Check if limit is set 16 | const limitNumber = 17 | +ctx.message.text.substr(11).trim() || 18 | +ctx.message.text.substr(12 + (bot as any).botInfo.username.length).trim() 19 | if (!isNaN(limitNumber) && limitNumber > 0 && limitNumber < 100000) { 20 | let chat = ctx.dbchat 21 | chat.timeGiven = limitNumber 22 | await saveChatProperty(chat, 'timeGiven') 23 | return ctx.replyWithMarkdown( 24 | `${strings(chat, 'time_limit_selected')} (${chat.timeGiven} ${strings( 25 | chat, 26 | 'seconds' 27 | )})` 28 | ) 29 | } 30 | 31 | return ctx.replyWithMarkdown( 32 | strings(ctx.dbchat, 'time_limit'), 33 | Extra.inReplyTo(ctx.message.message_id).markup((m) => 34 | m.inlineKeyboard( 35 | options.map((a) => 36 | a.map((o) => 37 | m.callbackButton(`${o} ${strings(ctx.dbchat, 'seconds')}`, o) 38 | ) 39 | ) 40 | ) 41 | ) 42 | ) 43 | }) 44 | 45 | bot.action( 46 | options.reduce((p, c) => p.concat(c), []), 47 | checkIfFromReplier, 48 | async (ctx) => { 49 | let chat = ctx.dbchat 50 | chat.timeGiven = Number(ctx.callbackQuery.data) 51 | await saveChatProperty(chat, 'timeGiven') 52 | const message = ctx.callbackQuery.message 53 | 54 | ctx.telegram.editMessageText( 55 | message.chat.id, 56 | message.message_id, 57 | undefined, 58 | `${strings(chat, 'time_limit_selected')} (${chat.timeGiven} ${strings( 59 | chat, 60 | 'seconds' 61 | )})` 62 | ) 63 | } 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/trust.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { modifyCandidates } from '@helpers/candidates' 3 | import { modifyRestrictedUsers } from '@helpers/restrictedUsers' 4 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 5 | import { Telegraf, Context, Extra } from 'telegraf' 6 | import { strings } from '@helpers/strings' 7 | import { checkLock } from '@middlewares/checkLock' 8 | import { report } from '@helpers/report' 9 | import { Candidate } from '@models/Chat' 10 | 11 | export function setupTrust(bot: Telegraf) { 12 | bot.command('trust', checkLock, clarifyIfPrivateMessages, async (ctx) => { 13 | // Check if it is a handle message 14 | const handle = ctx.message.text.substr(7).replace('@', '') 15 | let handleId: number | undefined 16 | if (handle) { 17 | for (const c of ctx.dbchat.candidates) { 18 | if (c.username === handle) { 19 | handleId = c.id 20 | break 21 | } 22 | } 23 | } 24 | // Check if reply 25 | if (!ctx.message || (!ctx.message.reply_to_message && !handleId)) { 26 | return 27 | } 28 | // Get replied 29 | const repliedId = handleId || ctx.message.reply_to_message.from.id 30 | // Unrestrict in Telegram 31 | try { 32 | await (ctx.telegram as any).restrictChatMember(ctx.dbchat.id, repliedId, { 33 | can_send_messages: true, 34 | can_send_media_messages: true, 35 | can_send_other_messages: true, 36 | can_add_web_page_previews: true, 37 | }) 38 | } catch (err) { 39 | report(err, setupTrust.name) 40 | } 41 | // Unrestrict in shieldy 42 | modifyRestrictedUsers(ctx.dbchat, false, [{ id: repliedId } as Candidate]) 43 | // Remove from candidates 44 | const candidate = ctx.dbchat.candidates 45 | .filter((c) => c.id === repliedId) 46 | .pop() 47 | if (candidate) { 48 | // Delete message 49 | await deleteMessageSafeWithBot(ctx.dbchat.id, candidate.messageId) 50 | // Remove from candidates 51 | modifyCandidates(ctx.dbchat, false, [{ id: repliedId } as Candidate]) 52 | } 53 | // Reply with success 54 | await ctx.replyWithMarkdown( 55 | strings(ctx.dbchat, 'trust_success'), 56 | Extra.inReplyTo(ctx.message.message_id) 57 | ) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/underAttack.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { saveChatProperty } from '@helpers/saveChatProperty' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | 7 | export function setupUnderAttack(bot: Telegraf) { 8 | bot.command( 9 | 'underAttack', 10 | checkLock, 11 | clarifyIfPrivateMessages, 12 | async (ctx) => { 13 | ctx.dbchat.underAttack = !ctx.dbchat.underAttack 14 | await saveChatProperty(ctx.dbchat, 'underAttack') 15 | ctx.replyWithMarkdown( 16 | strings( 17 | ctx.dbchat, 18 | ctx.dbchat.underAttack ? 'underAttack_true' : 'underAttack_false' 19 | ), 20 | Extra.inReplyTo(ctx.message.message_id) 21 | ) 22 | } 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/viewConfig.ts: -------------------------------------------------------------------------------- 1 | import { clarifyIfPrivateMessages } from '@helpers/clarifyIfPrivateMessages' 2 | import { findChat, Chat } from '@models/Chat' 3 | import { Telegraf, Context, Extra } from 'telegraf' 4 | import { strings } from '@helpers/strings' 5 | import { checkLock } from '@middlewares/checkLock' 6 | import { bot } from '@helpers/bot' 7 | 8 | export function setupViewConfig(bot: Telegraf) { 9 | bot.command( 10 | 'viewConfig', 11 | checkLock, 12 | clarifyIfPrivateMessages, 13 | async (ctx) => { 14 | const secondPart = ctx.message.text.split(' ')[1] 15 | if (secondPart) { 16 | try { 17 | let chatId: number | undefined 18 | if (!isNaN(+secondPart)) { 19 | chatId = +secondPart 20 | } else if (secondPart.startsWith('@')) { 21 | const telegramChat = await ctx.telegram.getChat(secondPart) 22 | chatId = telegramChat.id 23 | } 24 | if (chatId) { 25 | const chat = await findChat(chatId) 26 | return sendCurrentConfig(ctx, chat) 27 | } 28 | } catch (err) { 29 | return ctx.reply(strings(ctx.dbchat, 'noChatFound')) 30 | } 31 | } 32 | await sendCurrentConfig(ctx, ctx.dbchat) 33 | } 34 | ) 35 | } 36 | 37 | export async function sendCurrentConfig(ctx: Context, chat: Chat) { 38 | await ctx.replyWithMarkdown( 39 | `${strings(ctx.dbchat, 'viewConfig')} 40 | 41 | id: ${chat.id} 42 | type: ${ctx.chat.type} 43 | shieldyRole: ${ 44 | ctx.chat.type === 'private' 45 | ? 'N/A' 46 | : (await ctx.getChatMember((await ctx.telegram.getMe()).id)).status 47 | } 48 | language: ${chat.language} 49 | captchaType: ${chat.captchaType} 50 | timeGiven: ${chat.timeGiven} 51 | adminLocked: ${chat.adminLocked} 52 | restrict: ${chat.restrict} 53 | noChannelLinks: ${chat.noChannelLinks} 54 | deleteEntryMessages: ${chat.deleteEntryMessages} 55 | greetsUsers: ${chat.greetsUsers} 56 | customCaptchaMessage: ${chat.customCaptchaMessage} 57 | strict: ${chat.strict} 58 | deleteGreetingTime: ${chat.deleteGreetingTime || 0} 59 | banUsers: ${chat.banUsers} 60 | deleteEntryOnKick: ${chat.deleteEntryOnKick} 61 | cas: ${chat.cas} 62 | underAttack: ${chat.underAttack} 63 | noAttack: ${chat.noAttack} 64 | buttonText: ${chat.buttonText || 'Not set'} 65 | allowInvitingBots: ${chat.allowInvitingBots} 66 | skipOldUsers: ${chat.skipOldUsers} 67 | skipVerifiedUsers: ${chat.skipVerifiedUsers} 68 | restrictTime: ${chat.restrictTime || 24} 69 | banNewTelegramUsers: ${chat.banNewTelegramUsers} 70 | greetingButtons: 71 | ${chat.greetingButtons || 'Not set'}`, 72 | Extra.inReplyTo(ctx.message.message_id).HTML(true) 73 | ) 74 | if (chat.greetingMessage) { 75 | chat.greetingMessage.message.chat = undefined 76 | await ctx.telegram.sendCopy(ctx.dbchat.id, chat.greetingMessage.message, { 77 | ...Extra.webPreview(false).inReplyTo(ctx.message.message_id), 78 | entities: chat.greetingMessage.message.entities, 79 | }) 80 | } 81 | if (chat.captchaMessage) { 82 | chat.captchaMessage.message.chat = undefined 83 | await ctx.telegram.sendCopy(ctx.dbchat.id, chat.captchaMessage.message, { 84 | ...Extra.webPreview(false).inReplyTo(ctx.message.message_id), 85 | entities: chat.captchaMessage.message.entities, 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/controllers/webhook.ts: -------------------------------------------------------------------------------- 1 | import { ChatModel, SubscriptionStatus } from '@models/Chat' 2 | import { Context } from 'koa' 3 | import { Controller, Ctx, Post } from 'amala' 4 | import { stripe } from '@helpers/stripe' 5 | 6 | @Controller('/webhook') 7 | export default class WebhookController { 8 | @Post('/') 9 | async webhook(@Ctx() ctx: Context) { 10 | try { 11 | // Construct event 12 | const event = stripe.webhooks.constructEvent( 13 | String(ctx.request.rawBody), 14 | ctx.headers['stripe-signature'], 15 | process.env.STRIPE_SIGNING_SECRET 16 | ) 17 | // Handle event 18 | if (event.type === 'customer.subscription.deleted') { 19 | const anyData = event.data.object as any 20 | const subscriptionId = anyData.id 21 | const chat = await ChatModel.findOne({ subscriptionId }) 22 | if (!chat) { 23 | return ctx.throw( 24 | 400, 25 | `Webhook Error: No chat found for subscription id ${subscriptionId}` 26 | ) 27 | } 28 | if (chat.subscriptionStatus !== SubscriptionStatus.lifetime) { 29 | chat.subscriptionStatus = SubscriptionStatus.inactive 30 | } 31 | await chat.save() 32 | } else if (event.type === 'checkout.session.completed') { 33 | const anyData = event.data.object as any 34 | const chatId = +anyData.client_reference_id 35 | const chat = await ChatModel.findOne({ id: chatId }) 36 | if (!chat) { 37 | return ctx.throw( 38 | 400, 39 | `Webhook Error: No chat found with id ${chatId}` 40 | ) 41 | } 42 | if (anyData.mode === 'subscription') { 43 | chat.subscriptionId = anyData.subscription 44 | chat.subscriptionStatus = SubscriptionStatus.active 45 | } else { 46 | chat.subscriptionStatus = SubscriptionStatus.lifetime 47 | } 48 | await chat.save() 49 | } 50 | // Respond 51 | return { received: true } 52 | } catch (err) { 53 | return ctx.throw(400, `Webhook Error: ${err.message}`) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/helpers/bot.ts: -------------------------------------------------------------------------------- 1 | import { Context, Telegraf } from 'telegraf' 2 | const TelegrafBot = require('telegraf') 3 | 4 | export const bot = new TelegrafBot(process.env.TOKEN, { 5 | handlerTimeout: 1, 6 | }) as Telegraf 7 | -------------------------------------------------------------------------------- /src/helpers/candidates.ts: -------------------------------------------------------------------------------- 1 | import { report } from '@helpers/report' 2 | import { Chat, Candidate, ChatModel } from '@models/Chat' 3 | import { User } from 'telegraf/typings/telegram-types' 4 | 5 | export async function modifyCandidates( 6 | chat: Chat, 7 | add: boolean, 8 | candidatesAndUsers: Array 9 | ) { 10 | if (!candidatesAndUsers.length) { 11 | return 12 | } 13 | try { 14 | if (add) { 15 | await ChatModel.updateOne( 16 | { _id: chat._id }, 17 | { $push: { candidates: candidatesAndUsers } } 18 | ) 19 | } else { 20 | const candidatesIds = candidatesAndUsers.map((c) => c.id) 21 | await ChatModel.updateOne( 22 | { _id: chat._id }, 23 | { $pull: { candidates: { id: { $in: candidatesIds } } } }, 24 | { multi: true } 25 | ) 26 | } 27 | } catch (err) { 28 | report(err, modifyCandidates.name) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers/captcha.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'svg-captcha' 2 | import * as sharp from 'sharp' 3 | 4 | export async function getImageCaptcha() { 5 | const letters = 'abcdefghijklmnopqrstuvwxyz' 6 | const catpcha = create({ 7 | size: 6, 8 | ignoreChars: letters + letters.toUpperCase(), 9 | noise: 2, 10 | width: 150, 11 | height: 100, 12 | }) 13 | return { 14 | png: await sharp(Buffer.from(catpcha.data)).png().toBuffer(), 15 | text: catpcha.text, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/helpers/cas.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export async function checkCAS(userId: number) { 4 | try { 5 | const result = await axios(`https://api.cas.chat/check?user_id=${userId}`) 6 | return !result.data.ok 7 | } catch (err) { 8 | return true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers/clarifyIfPrivateMessages.ts: -------------------------------------------------------------------------------- 1 | import { strings } from '@helpers/strings' 2 | import { Context } from 'telegraf' 3 | 4 | export async function clarifyIfPrivateMessages(ctx: Context, next: Function) { 5 | if (ctx.chat?.type !== 'private') { 6 | return next() 7 | } 8 | await ctx.reply(strings(ctx.dbchat, 'commandsInPrivateWarning')) 9 | return next() 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers/clarifyReply.ts: -------------------------------------------------------------------------------- 1 | import { addMessageToDelete } from '@models/MessageToDelete' 2 | import { strings } from '@helpers/strings' 3 | import { Context } from 'telegraf' 4 | 5 | export async function clarifyReply(ctx: Context) { 6 | const sent = await ctx.reply(strings(ctx.dbchat, 'thisIsNotAReply')) 7 | const sent2 = await ctx.reply(strings(ctx.dbchat, 'thisIsAReply'), { 8 | reply_to_message_id: sent.message_id, 9 | }) 10 | const deleteTime = new Date() 11 | deleteTime.setSeconds(deleteTime.getSeconds() + 30) 12 | addMessageToDelete(sent.chat.id, sent.message_id, deleteTime) 13 | addMessageToDelete(sent2.chat.id, sent2.message_id, deleteTime) 14 | } 15 | -------------------------------------------------------------------------------- /src/helpers/deleteMessageSafe.ts: -------------------------------------------------------------------------------- 1 | import { report } from '@helpers/report' 2 | import { bot } from '@helpers/bot' 3 | import { Context } from 'telegraf' 4 | 5 | export async function deleteMessageSafe(ctx: Context) { 6 | try { 7 | await ctx.deleteMessage() 8 | } catch (err) { 9 | report(err, deleteMessageSafe.name) 10 | } 11 | } 12 | 13 | export async function deleteMessageSafeWithBot( 14 | chatId: number, 15 | messageId: number 16 | ) { 17 | try { 18 | await bot.telegram.deleteMessage(chatId, messageId) 19 | } catch (err) { 20 | report(err, deleteMessageSafeWithBot.name) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/helpers/equation.ts: -------------------------------------------------------------------------------- 1 | import { Equation } from '@models/Chat' 2 | 3 | export function generateEquation() { 4 | const a = getRandomInt(10) 5 | const b = getRandomInt(10) 6 | return { 7 | question: `${a} + ${b}`, 8 | answer: `${a + b}`, 9 | } as Equation 10 | } 11 | 12 | function getRandomInt(max) { 13 | return Math.floor(Math.random() * Math.floor(max)) + 1 14 | } 15 | -------------------------------------------------------------------------------- /src/helpers/error.ts: -------------------------------------------------------------------------------- 1 | export function checkIfErrorDismissable(error: Error) { 2 | const dismissableMessages = [ 3 | 'not enough rights', 4 | 'message to delete not found', 5 | 'bot was kicked', 6 | 'not in the chat', 7 | 'need to be inviter of a user', 8 | 'matching document found for id', 9 | 'bot is not a member', 10 | 'user is an administrator of the chat', 11 | 'USER_NOT_PARTICIPANT', 12 | 'CHAT_ADMIN_REQUIRED', 13 | "message can't be deleted", 14 | 'group chat was upgraded to a supergroup', 15 | 'CHANNEL_PRIVATE', 16 | 'method is available only for supergroups', 17 | 'have no rights to send a message', 18 | 'CHAT_WRITE_FORBIDDEN', 19 | 'message identifier is not specified', 20 | 'demote chat creator', 21 | 'USER_BANNED_IN_CHANNEL', 22 | 'Too Many Requests', 23 | ] 24 | for (const message of dismissableMessages) { 25 | if (error.message.indexOf(message) > -1) { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers/getUsername.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'telegraf/typings/telegram-types' 2 | 3 | export function getUsername(user: User, link = false) { 4 | return `${user.username ? `@${user.username}` : getName(user, link)}` 5 | } 6 | 7 | export function getName(user: User, link = false) { 8 | const linkStart = link ? `` : '' 9 | const linkEnd = link ? '' : '' 10 | return `${linkStart}${user.first_name}${ 11 | user.last_name ? ` ${user.last_name}` : '' 12 | }${linkEnd}` 13 | } 14 | 15 | export function getLink(user: User) { 16 | return `tg://user?id=${user.id}` 17 | } 18 | -------------------------------------------------------------------------------- /src/helpers/globallyRestricted.ts: -------------------------------------------------------------------------------- 1 | const globalyRestrictedMap = {} as { [index: number]: boolean } 2 | 3 | export function modifyGloballyRestricted(ids: number[], restrict: boolean) { 4 | for (const id of ids) { 5 | globalyRestrictedMap[id] = restrict 6 | } 7 | } 8 | 9 | export function isGloballyRestricted(id: number) { 10 | return !!globalyRestrictedMap[id] 11 | } 12 | -------------------------------------------------------------------------------- /src/helpers/isGroup.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | export function isGroup(ctx: Context) { 3 | return ['group', 'supergroup'].includes(ctx.chat?.type) 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/newcomers/addKickedUser.ts: -------------------------------------------------------------------------------- 1 | import { Chat } from '@models/Chat' 2 | import { CappedKickedUserModel } from '@models/CappedKickedUser' 3 | 4 | export function addKickedUser(chat: Chat, userId: number) { 5 | if (!chat.deleteEntryOnKick) { 6 | return 7 | } 8 | return new CappedKickedUserModel({ chatId: chat.id, userId }).save() 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/newcomers/checkButton.ts: -------------------------------------------------------------------------------- 1 | import { report } from '@helpers/report' 2 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 3 | import { greetUser } from '@helpers/newcomers/greetUser' 4 | import { modifyCandidates } from '@helpers/candidates' 5 | import { strings } from '@helpers/strings' 6 | import { Context } from 'telegraf' 7 | 8 | const buttonPresses = {} as { [index: string]: boolean } 9 | 10 | export async function handleButtonPress(ctx: Context) { 11 | // Ignore muptiple taps 12 | if (buttonPresses[ctx.callbackQuery.data]) { 13 | return 14 | } 15 | buttonPresses[ctx.callbackQuery.data] = true 16 | // Handle the button tap 17 | try { 18 | // Get user id and chat id 19 | const params = ctx.callbackQuery.data.split('~') 20 | const userId = parseInt(params[1]) 21 | // Check if button is pressed by the candidate 22 | if (userId !== ctx.from.id) { 23 | try { 24 | await ctx.answerCbQuery(strings(ctx.dbchat, 'only_candidate_can_reply')) 25 | } catch { 26 | // Do nothing 27 | } 28 | return 29 | } 30 | // Check if this user is within candidates 31 | if (!ctx.dbchat.candidates.map((c) => c.id).includes(userId)) { 32 | return 33 | } 34 | // Get the candidate 35 | const candidate = ctx.dbchat.candidates.filter((c) => c.id === userId).pop() 36 | // Remove candidate from the chat 37 | await modifyCandidates(ctx.dbchat, false, [candidate]) 38 | // Delete the captcha message 39 | deleteMessageSafeWithBot(ctx.chat!.id, candidate.messageId) 40 | // Greet the user 41 | greetUser(ctx) 42 | console.log( 43 | 'greeted a user', 44 | ctx.dbchat.captchaType, 45 | ctx.dbchat.customCaptchaMessage, 46 | ctx.dbchat.greetsUsers 47 | ) 48 | } catch (err) { 49 | report(err, handleButtonPress.name) 50 | } finally { 51 | buttonPresses[ctx.callbackQuery.data] = undefined 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/newcomers/checkPassingCaptchaWithText.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deleteMessageSafe, 3 | deleteMessageSafeWithBot, 4 | } from '@helpers/deleteMessageSafe' 5 | import { addVerifiedUser } from '@models/VerifiedUser' 6 | import { greetUser } from '@helpers/newcomers/greetUser' 7 | import { modifyCandidates } from '@helpers/candidates' 8 | import { CaptchaType } from '@models/Chat' 9 | 10 | export async function checkPassingCaptchaWithText(ctx, next) { 11 | // Check if it is a message is from a candidates 12 | if ( 13 | !ctx.dbchat.candidates.length || 14 | !ctx.dbchat.candidates.map((c) => c.id).includes(ctx.from.id) 15 | ) { 16 | return next() 17 | } 18 | // Check if it is not a text message in a strict mode 19 | if (!ctx.message?.text) { 20 | if (ctx.dbchat.strict) { 21 | deleteMessageSafe(ctx) 22 | } 23 | return next() 24 | } 25 | // Check if it is a button captcha (shouldn't get to this function then) 26 | if (ctx.dbchat.captchaType === CaptchaType.BUTTON) { 27 | // Delete message of restricted 28 | if (ctx.dbchat.strict) { 29 | deleteMessageSafe(ctx) 30 | } 31 | // Exit the function 32 | return next() 33 | } 34 | // Get candidate 35 | const candidate = ctx.dbchat.candidates 36 | .filter((c) => c.id === ctx.from.id) 37 | .pop() 38 | // Check if it is digits captcha 39 | if (candidate.captchaType === CaptchaType.DIGITS) { 40 | // Check the format 41 | const hasCorrectAnswer = ctx.message.text.includes( 42 | candidate.equationAnswer as string 43 | ) 44 | const hasNoMoreThanTwoDigits = 45 | (ctx.message.text.match(/\d/g) || []).length <= 2 46 | if (!hasCorrectAnswer || !hasNoMoreThanTwoDigits) { 47 | if (ctx.dbchat.strict) { 48 | deleteMessageSafe(ctx) 49 | } 50 | return next() 51 | } 52 | // Delete message to decrease the amount of messages left 53 | deleteMessageSafe(ctx) 54 | } 55 | // Check if it is image captcha 56 | if (candidate.captchaType === CaptchaType.IMAGE) { 57 | const hasCorrectAnswer = ctx.message.text.includes(candidate.imageText) 58 | if (!hasCorrectAnswer) { 59 | if (ctx.dbchat.strict) { 60 | deleteMessageSafe(ctx) 61 | } 62 | return next() 63 | } 64 | // Delete message to decrease the amount of messages left 65 | deleteMessageSafe(ctx) 66 | } 67 | // Passed the captcha, delete from candidates 68 | await modifyCandidates(ctx.dbchat, false, [candidate]) 69 | // Delete the captcha message 70 | deleteMessageSafeWithBot(ctx.chat!.id, candidate.messageId) 71 | // Greet user 72 | greetUser(ctx) 73 | console.log( 74 | 'greeted a user', 75 | ctx.dbchat.captchaType, 76 | ctx.dbchat.customCaptchaMessage, 77 | ctx.dbchat.greetsUsers 78 | ) 79 | if ( 80 | candidate.captchaType === CaptchaType.DIGITS || 81 | candidate.captchaType === CaptchaType.IMAGE 82 | ) { 83 | addVerifiedUser(ctx.from.id) 84 | } 85 | return next() 86 | } 87 | -------------------------------------------------------------------------------- /src/helpers/newcomers/constructMessageWithEntities.ts: -------------------------------------------------------------------------------- 1 | import { 2 | promoAdditionsWithoutHtml, 3 | } from '@helpers/promo' 4 | import { cloneDeep } from 'lodash' 5 | import { Message, User } from 'telegram-typings' 6 | 7 | export function constructMessageWithEntities( 8 | originalMessage: Message, 9 | user: User, 10 | tags: { [index: string]: string }, 11 | addPromoText = false, 12 | language = 'en' 13 | ) { 14 | const message = cloneDeep(originalMessage) 15 | let originalText = message.text 16 | for (const tag in tags) { 17 | const tag_value = tags[tag] 18 | if (!tag_value) { 19 | continue 20 | } 21 | while (originalText.includes(tag)) { 22 | const tag_offset = originalText.indexOf(tag) 23 | 24 | const tag_length = tag.length 25 | const tag_value_length = tag_value.length 26 | 27 | // Replace the tag with the value in the message 28 | originalText = originalText.replace(tag, tag_value) 29 | // Update the offset of links if it is after the replaced tag 30 | if (message.entities && message.entities.length) { 31 | message.entities.forEach((msgEntity) => { 32 | // Entities after 33 | if (msgEntity.offset > tag_offset + tag_length) { 34 | msgEntity.offset = msgEntity.offset - tag_length + tag_value_length 35 | } else if ( 36 | msgEntity.offset <= tag_offset && 37 | msgEntity.offset + msgEntity.length >= tag_offset + tag_length 38 | ) { 39 | msgEntity.length += tag_value_length - tag_length 40 | } 41 | }) 42 | } 43 | if (tag === '$username' || tag === '$fullname') { 44 | if (!message.entities) { 45 | message.entities = [] 46 | } 47 | message.entities.push({ 48 | type: 'text_mention', 49 | offset: tag_offset, 50 | length: tag_value_length, 51 | user, 52 | }) 53 | } 54 | } 55 | } 56 | if (addPromoText) { 57 | if (!message.entities) { 58 | message.entities = [] 59 | } 60 | const promoAddition = promoAdditionsWithoutHtml[language](Math.random()) 61 | message.entities.push(...promoAddition.links.map(item => ({ 62 | type: 'text_link', 63 | offset: `${originalText}\n`.length + item.offset, 64 | length: item.length, 65 | url: item.link, 66 | }))) 67 | originalText = `${originalText}\n${promoAddition.text}` 68 | } 69 | message.text = originalText 70 | return message 71 | } 72 | -------------------------------------------------------------------------------- /src/helpers/newcomers/generateEquationOrImage.ts: -------------------------------------------------------------------------------- 1 | import { DocumentType } from '@typegoose/typegoose' 2 | import { CaptchaType, Chat, Equation } from '@models/Chat' 3 | import { getImageCaptcha } from '@helpers/captcha' 4 | import { generateEquation } from '@helpers/equation' 5 | 6 | export async function generateEquationOrImage(chat: DocumentType) { 7 | const equation = 8 | chat.captchaType === CaptchaType.DIGITS ? generateEquation() : undefined 9 | const image = 10 | chat.captchaType === CaptchaType.IMAGE ? await getImageCaptcha() : undefined 11 | return { equation, image } as { 12 | equation?: Equation 13 | image?: { png: any; text: string } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/newcomers/getCandidate.ts: -------------------------------------------------------------------------------- 1 | import { Candidate, Equation } from '@models/Chat' 2 | import { User, Message } from 'telegram-typings' 3 | import { Context } from 'telegraf' 4 | 5 | export function getCandidate( 6 | ctx: Context, 7 | user: User, 8 | notificationMessage?: Message, 9 | equation?: Equation, 10 | image?: { 11 | png: any 12 | text: string 13 | } 14 | ): Candidate { 15 | return { 16 | id: user.id, 17 | timestamp: new Date().getTime(), 18 | captchaType: ctx.dbchat.captchaType, 19 | messageId: notificationMessage ? notificationMessage.message_id : undefined, 20 | equationQuestion: equation ? (equation.question as string) : undefined, 21 | equationAnswer: equation ? (equation.answer as string) : undefined, 22 | entryChatId: ctx.chat.id, 23 | entryMessageId: ctx.message ? ctx.message.message_id : undefined, 24 | imageText: image ? image.text : undefined, 25 | username: user.username, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/newcomers/greetUser.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'telegram-typings' 2 | import { isFunction } from 'lodash' 3 | import { User } from 'telegraf/typings/telegram-types' 4 | import { Context, Extra } from 'telegraf' 5 | import { constructMessageWithEntities } from '@helpers/newcomers/constructMessageWithEntities' 6 | import { getName, getUsername } from '@helpers/getUsername' 7 | import { addMessageToDelete } from '@models/MessageToDelete' 8 | 9 | export async function greetUser(ctx: Context, unsafeUser?: User | Function) { 10 | // Get the user (it can be function if used as middleware in telegraf) 11 | let user = 12 | unsafeUser && !isFunction(unsafeUser) ? (unsafeUser as User) : ctx.from 13 | // Check if greeting is required 14 | if (!ctx.dbchat.greetsUsers || !ctx.dbchat.greetingMessage) { 15 | return 16 | } 17 | // Get marked up message 18 | const message = constructMessageWithEntities( 19 | ctx.dbchat.greetingMessage.message, 20 | user, 21 | { 22 | $title: (await ctx.getChat()).title, 23 | $username: getUsername(user), 24 | $fullname: getName(user), 25 | } 26 | ) 27 | // Add the @username of the greeted user at the end of the message if no $username was provided 28 | const originalMessageText = ctx.dbchat.greetingMessage.message.text 29 | if ( 30 | !originalMessageText.includes('$username') && 31 | !originalMessageText.includes('$fullname') 32 | ) { 33 | const username = getUsername(user) 34 | const initialLength = `${message.text}\n\n`.length 35 | message.text = `${message.text}\n\n${username}` 36 | if (!message.entities) { 37 | message.entities = [] 38 | } 39 | message.entities.push({ 40 | type: 'text_mention', 41 | offset: initialLength, 42 | length: username.length, 43 | user, 44 | }) 45 | } 46 | // Send the message 47 | let messageSent: Message 48 | try { 49 | message.chat = undefined 50 | messageSent = await ctx.telegram.sendCopy(ctx.dbchat.id, message, { 51 | ...(ctx.dbchat.greetingButtons 52 | ? Extra.webPreview(false).markup((m) => 53 | m.inlineKeyboard( 54 | ctx.dbchat.greetingButtons 55 | .split('\n') 56 | .map((s) => { 57 | const components = s.split(' - ') 58 | return m.urlButton(components[0], components[1]) 59 | }) 60 | .map((v) => [v]) 61 | ) 62 | ) 63 | : Extra.webPreview(false)), 64 | entities: message.entities, 65 | }) 66 | } catch (err) { 67 | message.entities = [] 68 | message.chat = undefined 69 | messageSent = await ctx.telegram.sendCopy(ctx.dbchat.id, message, { 70 | ...(ctx.dbchat.greetingButtons 71 | ? Extra.webPreview(false).markup((m) => 72 | m.inlineKeyboard( 73 | ctx.dbchat.greetingButtons 74 | .split('\n') 75 | .map((s) => { 76 | const components = s.split(' - ') 77 | return m.urlButton(components[0], components[1]) 78 | }) 79 | .map((v) => [v]) 80 | ) 81 | ) 82 | : Extra.webPreview(false)), 83 | entities: message.entities, 84 | }) 85 | } 86 | 87 | // Delete greeting message if requested 88 | if (ctx.dbchat.deleteGreetingTime && messageSent) { 89 | const deleteTime = new Date() 90 | deleteTime.setSeconds( 91 | deleteTime.getSeconds() + ctx.dbchat.deleteGreetingTime 92 | ) 93 | addMessageToDelete(messageSent.chat.id, messageSent.message_id, deleteTime) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/helpers/newcomers/handleLeftChatMember.ts: -------------------------------------------------------------------------------- 1 | import { CappedKickedUserModel } from '@models/CappedKickedUser' 2 | import { ChatModel } from '@models/Chat' 3 | import { deleteMessageSafe } from '@helpers/deleteMessageSafe' 4 | import { Context } from 'telegraf' 5 | 6 | export async function handleLeftChatMember(ctx: Context) { 7 | // Check if this user got kicked 8 | const userWasKicked = !!(await CappedKickedUserModel.findOne({ 9 | chatId: ctx.dbchat.id, 10 | userId: ctx.message.left_chat_member.id, 11 | })) 12 | // Delete left message if required 13 | if ( 14 | ctx.dbchat.deleteEntryMessages || 15 | ctx.dbchat.underAttack || 16 | (ctx.dbchat.deleteEntryOnKick && userWasKicked) 17 | ) { 18 | deleteMessageSafe(ctx) 19 | return 20 | } 21 | if (ctx.dbchat.deleteEntryOnKick) { 22 | ChatModel.updateOne( 23 | { _id: ctx.dbchat._id, 'candidates.id': ctx.message.left_chat_member.id }, 24 | { $set: { 'candidates.$.leaveMessageId': ctx.message.message_id } } 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/newcomers/handleNewChatMembers.ts: -------------------------------------------------------------------------------- 1 | import { ChatMember } from 'telegram-typings' 2 | import { EntryMessageModel, removeEntryMessages } from '@models/EntryMessage' 3 | import { Candidate } from '@models/Chat' 4 | import { bot } from '@helpers/bot' 5 | import { Context } from 'telegraf' 6 | import { modifyGloballyRestricted } from '@helpers/globallyRestricted' 7 | import { sendHelpSafe } from '@commands/help' 8 | import { report } from '@helpers/report' 9 | import { greetUser } from '@helpers/newcomers/greetUser' 10 | import { modifyRestrictedUsers } from '@helpers/restrictedUsers' 11 | import { isVerifiedUser } from '@models/VerifiedUser' 12 | import { checkCAS } from '@helpers/cas' 13 | import { kickChatMember } from '@helpers/newcomers/kickChatMember' 14 | import { generateEquationOrImage } from '@helpers/newcomers/generateEquationOrImage' 15 | import { notifyCandidate } from '@helpers/newcomers/notifyCandidate' 16 | import { getCandidate } from '@helpers/newcomers/getCandidate' 17 | import { restrictChatMember } from '@helpers/newcomers/restrictChatMember' 18 | import { modifyCandidates } from '@helpers/candidates' 19 | import { removeMessages } from '@models/CappedMessage' 20 | import { deleteMessageSafe } from '@helpers/deleteMessageSafe' 21 | 22 | export async function handleNewChatMember(ctx: Context) { 23 | // Check if no attack mode 24 | if (ctx.dbchat.noAttack) { 25 | return 26 | } 27 | // Get new member 28 | const newChatMember = (ctx.update as any).chat_member 29 | .new_chat_member as ChatMember 30 | // Get list of ids 31 | const memberId = newChatMember.user.id 32 | // Add to globaly restricted list 33 | await modifyGloballyRestricted([memberId], true) 34 | // Start the newcomers logic 35 | try { 36 | // If an admin adds the members, do nothing 37 | const adder = await ctx.getChatMember( 38 | (ctx.update as any).chat_member.from.id 39 | ) 40 | if (['creator', 'administrator'].includes(adder.status)) { 41 | return 42 | } 43 | // Filter new members 44 | const membersToCheck = [newChatMember.user] 45 | // Placeholder to add all candidates in batch 46 | const candidatesToAdd = [] as Candidate[] 47 | // Loop through the members 48 | for (const member of membersToCheck) { 49 | // Check if an old user 50 | if (ctx.dbchat.skipOldUsers) { 51 | if (member.id > 0 && member.id < 1000000000) { 52 | greetUser(ctx, member) 53 | if (ctx.dbchat.restrict) { 54 | modifyRestrictedUsers(ctx.dbchat, true, [member]) 55 | } 56 | continue 57 | } 58 | } 59 | // Check if a verified user 60 | if (ctx.dbchat.skipVerifiedUsers) { 61 | if (await isVerifiedUser(member.id)) { 62 | greetUser(ctx, member) 63 | if (ctx.dbchat.restrict) { 64 | modifyRestrictedUsers(ctx.dbchat, true, [member]) 65 | } 66 | continue 67 | } 68 | } 69 | // Delete all messages that they've sent so far 70 | removeMessages(ctx.chat.id, member.id) // don't await here 71 | // Check if under attack 72 | if (ctx.dbchat.underAttack) { 73 | kickChatMember(ctx.dbchat, member) 74 | continue 75 | } 76 | // Check if id is over 1 000 000 000 77 | if (ctx.dbchat.banNewTelegramUsers && member.id > 1000000000) { 78 | kickChatMember(ctx.dbchat, member) 79 | if (ctx.dbchat.deleteEntryOnKick) { 80 | removeEntryMessages(ctx.chat.id, memberId) 81 | } 82 | continue 83 | } 84 | // Check if CAS banned 85 | if (ctx.dbchat.cas && !(await checkCAS(member.id))) { 86 | kickChatMember(ctx.dbchat, member) 87 | if (ctx.dbchat.deleteEntryOnKick) { 88 | removeEntryMessages(ctx.chat.id, memberId) 89 | } 90 | continue 91 | } 92 | // Check if already a candidate 93 | if (ctx.dbchat.candidates.map((c) => c.id).includes(member.id)) { 94 | continue 95 | } 96 | // Generate captcha if required 97 | const { equation, image } = await generateEquationOrImage(ctx.dbchat) 98 | // Notify candidate and save the message 99 | let message 100 | try { 101 | message = await notifyCandidate(ctx, member, equation, image) 102 | } catch (err) { 103 | report(err, notifyCandidate.name) 104 | } 105 | // Create a candidate 106 | const candidate = getCandidate(ctx, member, message, equation, image) 107 | // Restrict candidate if required 108 | if (ctx.dbchat.restrict) { 109 | restrictChatMember(ctx.dbchat, member) 110 | } 111 | // Save candidate to the placeholder list 112 | candidatesToAdd.push(candidate) 113 | } 114 | // Add candidates to the list 115 | await modifyCandidates(ctx.dbchat, true, candidatesToAdd) 116 | // Restrict candidates if required 117 | await modifyRestrictedUsers(ctx.dbchat, true, candidatesToAdd) 118 | // Delete all messages that they've sent so far 119 | for (const member of candidatesToAdd) { 120 | removeMessages(ctx.chat.id, member.id) // don't await here 121 | } 122 | } catch (err) { 123 | console.error('onNewChatMembers', err) 124 | } finally { 125 | // Remove from globaly restricted list 126 | modifyGloballyRestricted([memberId], false) 127 | } 128 | } 129 | 130 | export async function handleNewChatMemberMessage(ctx: Context) { 131 | // Send help message if added this bot to the group 132 | const addedUsernames = ctx.message.new_chat_members 133 | .map((member) => member.username) 134 | .filter((username) => !!username) 135 | if (addedUsernames.includes((bot as any).botInfo.username)) { 136 | await sendHelpSafe(ctx) 137 | return 138 | } 139 | // Check if no attack mode 140 | if (ctx.dbchat.noAttack) { 141 | return 142 | } 143 | // Check if needs to delete message right away 144 | if (ctx.dbchat.deleteEntryMessages || ctx.dbchat.underAttack) { 145 | deleteMessageSafe(ctx) 146 | return 147 | } 148 | // Save for later if needs deleting 149 | if (ctx.dbchat.deleteEntryOnKick) { 150 | for (const newMember of ctx.message.new_chat_members) { 151 | await new EntryMessageModel({ 152 | message_id: ctx.message.message_id, 153 | chat_id: ctx.chat.id, 154 | from_id: newMember.id, 155 | }).save() 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/helpers/newcomers/index.ts: -------------------------------------------------------------------------------- 1 | import { checkIfGroup } from '@middlewares/checkIfGroup' 2 | import { isGroup } from '@helpers/isGroup' 3 | import { ChatMember } from 'telegram-typings' 4 | import { notifyCandidate } from '@helpers/newcomers/notifyCandidate' 5 | import { generateEquationOrImage } from '@helpers/newcomers/generateEquationOrImage' 6 | import Telegraf, { Context } from 'telegraf' 7 | import { checkSuperAdmin } from '@middlewares/checkSuperAdmin' 8 | import { greetUser } from '@helpers/newcomers/greetUser' 9 | import { handleLeftChatMember } from '@helpers/newcomers/handleLeftChatMember' 10 | import { 11 | handleNewChatMember, 12 | handleNewChatMemberMessage, 13 | } from '@helpers/newcomers/handleNewChatMembers' 14 | import { handleButtonPress } from '@helpers/newcomers/checkButton' 15 | import { checkPassingCaptchaWithText } from './checkPassingCaptchaWithText' 16 | 17 | export function setupNewcomers(bot: Telegraf) { 18 | // Admin command to check greetings 19 | bot.command('greetMe', checkSuperAdmin, greetUser) 20 | // Admin command to check captcha 21 | bot.command('captchaMe', checkSuperAdmin, async (ctx) => { 22 | const { equation, image } = await generateEquationOrImage(ctx.dbchat) 23 | return notifyCandidate(ctx, ctx.from, equation, image) 24 | }) 25 | // Keep track of new member messages to delete them 26 | bot.on('new_chat_members', checkIfGroup, handleNewChatMemberMessage) 27 | // Keep track of leave messages and delete them if necessary 28 | bot.on('left_chat_member', handleLeftChatMember) 29 | // Check newcomers passing captcha with text 30 | bot.use(checkPassingCaptchaWithText) 31 | // Check newcomers passing captcha with button 32 | bot.action(/\d+~\d+/, handleButtonPress) 33 | } 34 | 35 | export function checkMemberChange(ctx: Context, next: Function) { 36 | // Check if this is a group 37 | if (!isGroup(ctx)) { 38 | return next() 39 | } 40 | // Check if it's a chat_member update 41 | const anyUpdate = ctx.update as any 42 | if (!anyUpdate.chat_member) { 43 | return next() 44 | } 45 | // Get users 46 | const oldChatMember = anyUpdate.chat_member.old_chat_member as ChatMember 47 | const newChatMember = anyUpdate.chat_member.new_chat_member as ChatMember 48 | // Check if joined 49 | if (oldChatMember.status === 'left' && newChatMember.status === 'member') { 50 | return handleNewChatMember(ctx) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/helpers/newcomers/kickCandidates.ts: -------------------------------------------------------------------------------- 1 | import { removeEntryMessages } from '@models/EntryMessage' 2 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 3 | import { bot } from '@helpers/bot' 4 | import { Chat, Candidate } from '@models/Chat' 5 | import { report } from '@helpers/report' 6 | import { addKickedUser } from '@helpers/newcomers/addKickedUser' 7 | import { modifyCandidates } from '@helpers/candidates' 8 | import { modifyRestrictedUsers } from '@helpers/restrictedUsers' 9 | 10 | const chatMembersBeingKicked = {} as { 11 | [index: number]: { [index: number]: boolean } 12 | } 13 | 14 | export async function kickCandidates(chat: Chat, candidates: Candidate[]) { 15 | // Loop through candidates 16 | for (const candidate of candidates) { 17 | // Check if they are already being kicked 18 | if ( 19 | chatMembersBeingKicked[chat.id] && 20 | chatMembersBeingKicked[chat.id][candidate.id] 21 | ) { 22 | console.log( 23 | `${candidate.id} in ${chat.id} is already being kicked, skipping` 24 | ) 25 | continue 26 | } 27 | // Try kicking the candidate 28 | try { 29 | await addKickedUser(chat, candidate.id) 30 | kickChatMemberProxy( 31 | chat.id, 32 | candidate.id, 33 | chat.banUsers ? 0 : parseInt(`${new Date().getTime() / 1000 + 45}`) 34 | ) 35 | } catch (err) { 36 | report(err, addKickedUser.name) 37 | } 38 | // Try deleting their entry messages 39 | if (chat.deleteEntryOnKick) { 40 | removeEntryMessages(candidate.entryChatId, candidate.id) 41 | deleteMessageSafeWithBot(candidate.entryChatId, candidate.leaveMessageId) 42 | } 43 | // Try deleting the captcha message 44 | deleteMessageSafeWithBot(chat.id, candidate.messageId) 45 | } 46 | // Remove from candidates 47 | await modifyCandidates(chat, false, candidates) 48 | // Remove from restricted 49 | await modifyRestrictedUsers(chat, false, candidates) 50 | } 51 | 52 | async function kickChatMemberProxy( 53 | id: number, 54 | candidateId: number, 55 | duration: number 56 | ) { 57 | try { 58 | if (!chatMembersBeingKicked[id]) { 59 | chatMembersBeingKicked[id] = {} 60 | } 61 | chatMembersBeingKicked[id][candidateId] = true 62 | await bot.telegram.kickChatMember(id, candidateId, duration) 63 | } catch (err) { 64 | report(err, kickChatMemberProxy.name) 65 | } finally { 66 | if (chatMembersBeingKicked[id] && chatMembersBeingKicked[id][candidateId]) { 67 | delete chatMembersBeingKicked[id][candidateId] 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/helpers/newcomers/kickChatMember.ts: -------------------------------------------------------------------------------- 1 | import { bot } from '@helpers/bot' 2 | import { DocumentType } from '@typegoose/typegoose' 3 | import { User } from 'telegraf/typings/telegram-types' 4 | import { Chat } from '@models/Chat' 5 | import { addKickedUser } from '@helpers/newcomers/addKickedUser' 6 | import { report } from '@helpers/report' 7 | import { modifyCandidates } from '@helpers/candidates' 8 | import { modifyRestrictedUsers } from '@helpers/restrictedUsers' 9 | 10 | export async function kickChatMember(chat: DocumentType, user: User) { 11 | // Try kicking the member 12 | try { 13 | await addKickedUser(chat, user.id) 14 | await bot.telegram.kickChatMember( 15 | chat.id, 16 | user.id, 17 | chat.banUsers ? 0 : parseInt(`${new Date().getTime() / 1000 + 45}`) 18 | ) 19 | } catch (err) { 20 | report(err, kickChatMember.name) 21 | } 22 | // Remove from candidates 23 | await modifyCandidates(chat, false, [user]) 24 | // Remove from restricted 25 | await modifyRestrictedUsers(chat, false, [user]) 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/newcomers/notifyCandidate.ts: -------------------------------------------------------------------------------- 1 | import { ExtraReplyMessage } from 'telegraf/typings/telegram-types' 2 | import { cloneDeep } from 'lodash' 3 | import { Equation, CaptchaType, SubscriptionStatus } from '@models/Chat' 4 | import { User } from 'telegram-typings' 5 | import { Context, Extra, Markup } from 'telegraf' 6 | import { strings } from '@helpers/strings' 7 | import { constructMessageWithEntities } from '@helpers/newcomers/constructMessageWithEntities' 8 | import { getName, getUsername } from '@helpers/getUsername' 9 | import { 10 | languageForPromo, 11 | promoExceptions, 12 | promoAdditions, 13 | } from '@helpers/promo' 14 | 15 | export async function notifyCandidate( 16 | ctx: Context, 17 | candidate: User, 18 | equation?: Equation, 19 | image?: { png: Buffer; text: string } 20 | ) { 21 | const chat = ctx.dbchat 22 | const captchaMessage = ctx.dbchat.captchaMessage 23 | ? cloneDeep(ctx.dbchat.captchaMessage) 24 | : undefined 25 | const warningMessage = strings(chat, `${chat.captchaType}_warning`) 26 | let extra = 27 | chat.captchaType !== CaptchaType.BUTTON 28 | ? Extra.webPreview(false) 29 | : Extra.webPreview(false).markup((m) => 30 | m.inlineKeyboard([ 31 | m.callbackButton( 32 | chat.buttonText || strings(chat, 'captcha_button'), 33 | `${chat.id}~${candidate.id}` 34 | ), 35 | ]) 36 | ) 37 | if ( 38 | chat.customCaptchaMessage && 39 | captchaMessage && 40 | (chat.captchaType !== CaptchaType.DIGITS || 41 | captchaMessage.message.text.includes('$equation')) 42 | ) { 43 | const text = captchaMessage.message.text 44 | if ( 45 | text.includes('$username') || 46 | text.includes('$title') || 47 | text.includes('$equation') || 48 | text.includes('$seconds') || 49 | text.includes('$fullname') 50 | ) { 51 | const messageToSend = constructMessageWithEntities( 52 | captchaMessage.message, 53 | candidate, 54 | { 55 | $username: getUsername(candidate), 56 | $fullname: getName(candidate), 57 | $title: (await ctx.getChat()).title, 58 | $equation: equation ? (equation.question as string) : '', 59 | $seconds: `${chat.timeGiven}`, 60 | }, 61 | (process.env.PREMIUM !== 'true' && 62 | !promoExceptions.includes(ctx.chat.id)) || 63 | (process.env.PREMIUM === 'true' && 64 | ctx.dbchat.subscriptionStatus !== SubscriptionStatus.active), 65 | languageForPromo(chat) 66 | ) 67 | if (image) { 68 | extra = extra.HTML(true) 69 | let formattedText = (Markup as any).formatHTML( 70 | messageToSend.text, 71 | messageToSend.entities 72 | ) 73 | return ctx.replyWithPhoto({ source: image.png } as any, { 74 | caption: formattedText, 75 | ...(extra as ExtraReplyMessage), 76 | }) 77 | } else { 78 | messageToSend.chat = undefined 79 | return ctx.telegram.sendCopy(chat.id, messageToSend, { 80 | ...(extra as ExtraReplyMessage), 81 | entities: messageToSend.entities, 82 | }) 83 | } 84 | } else { 85 | extra = extra.HTML(true) 86 | const message = cloneDeep(captchaMessage.message) 87 | const formattedText = (Markup as any).formatHTML( 88 | message.text, 89 | message.entities 90 | ) 91 | const promoAddition = promoAdditions[languageForPromo(chat)]( 92 | Math.random() 93 | ) 94 | message.text = 95 | promoExceptions.includes(ctx.chat.id) || 96 | (process.env.PREMIUM === 'true' && 97 | ctx.dbchat.subscriptionStatus === SubscriptionStatus.active) 98 | ? `${getUsername(candidate)}\n\n${formattedText}` 99 | : `${getUsername(candidate)}\n\n${formattedText}\n${promoAddition}` 100 | try { 101 | message.chat = undefined 102 | const sentMessage = await ctx.telegram.sendCopy(chat.id, message, { 103 | ...(extra as ExtraReplyMessage), 104 | entities: message.entities, 105 | }) 106 | return sentMessage 107 | } catch (err) { 108 | message.entities = [] 109 | message.chat = undefined 110 | const sentMessage = await ctx.telegram.sendCopy(chat.id, message, { 111 | ...(extra as ExtraReplyMessage), 112 | entities: message.entities, 113 | }) 114 | return sentMessage 115 | } 116 | } 117 | } else { 118 | extra = extra.HTML(true) 119 | if (image) { 120 | const promoAddition = promoAdditions[languageForPromo(chat)]( 121 | Math.random() 122 | ) 123 | return ctx.replyWithPhoto({ source: image.png } as any, { 124 | caption: 125 | promoExceptions.includes(ctx.chat.id) || 126 | (process.env.PREMIUM === 'true' && 127 | ctx.dbchat.subscriptionStatus === SubscriptionStatus.active) 128 | ? `${getUsername( 129 | candidate 130 | )}${warningMessage} (${chat.timeGiven} ${strings( 131 | chat, 132 | 'seconds' 133 | )})` 134 | : `${getUsername( 135 | candidate 136 | )}${warningMessage} (${chat.timeGiven} ${strings( 137 | chat, 138 | 'seconds' 139 | )})\n${promoAddition}`, 140 | parse_mode: 'HTML', 141 | }) 142 | } else { 143 | const promoAddition = promoAdditions[languageForPromo(chat)]( 144 | Math.random() 145 | ) 146 | return ctx.replyWithMarkdown( 147 | promoExceptions.includes(ctx.chat.id) || 148 | (process.env.PREMIUM === 'true' && 149 | ctx.dbchat.subscriptionStatus === SubscriptionStatus.active) 150 | ? `${ 151 | chat.captchaType === CaptchaType.DIGITS 152 | ? `(${equation.question}) ` 153 | : '' 154 | }${getUsername( 155 | candidate 156 | )}${warningMessage} (${chat.timeGiven} ${strings( 157 | chat, 158 | 'seconds' 159 | )})` 160 | : `${ 161 | chat.captchaType === CaptchaType.DIGITS 162 | ? `(${equation.question}) ` 163 | : '' 164 | }${getUsername( 165 | candidate 166 | )}${warningMessage} (${chat.timeGiven} ${strings( 167 | chat, 168 | 'seconds' 169 | )})\n${promoAddition}`, 170 | extra 171 | ) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/helpers/newcomers/restrictChatMember.ts: -------------------------------------------------------------------------------- 1 | import { bot } from '@helpers/bot' 2 | import { DocumentType } from '@typegoose/typegoose' 3 | import { User } from 'telegraf/typings/telegram-types' 4 | import { Chat } from '@models/Chat' 5 | import { report } from '@helpers/report' 6 | 7 | export async function restrictChatMember(chat: DocumentType, user: User) { 8 | try { 9 | const gotUser = (await bot.telegram.getChatMember(chat.id, user.id)) as any 10 | if ( 11 | gotUser.can_send_messages && 12 | gotUser.can_send_media_messages && 13 | gotUser.can_send_other_messages && 14 | gotUser.can_add_web_page_previews 15 | ) { 16 | const tomorrow = (new Date().getTime() + 24 * 60 * 60 * 1000) / 1000 17 | await (bot.telegram as any).restrictChatMember(chat.id, user.id, { 18 | until_date: tomorrow, 19 | can_send_messages: true, 20 | can_send_media_messages: false, 21 | can_send_polls: false, 22 | can_send_other_messages: false, 23 | can_add_web_page_previews: false, 24 | can_change_info: false, 25 | can_invite_users: false, 26 | can_pin_messages: false, 27 | }) 28 | } 29 | } catch (err) { 30 | report(err, restrictChatMember.name) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/helpers/promo.ts: -------------------------------------------------------------------------------- 1 | import { DocumentType } from '@typegoose/typegoose' 2 | import { Chat, Language } from '@models/Chat' 3 | 4 | export function languageForPromo(chat: DocumentType) { 5 | if (chat.language === Language.RUSSIAN) { 6 | return 'ru' 7 | } 8 | return 'en' // All other languagess are not supported yet 9 | } 10 | 11 | const ruPromoAdditionsArray = [ 12 | { 13 | prelinks: 'При поддержке', 14 | links: [ 15 | { 16 | prefix: ' ', 17 | text: '1inch', 18 | postfix: ' ', 19 | link: 'https://jn3rg.app.link/c10FJKdtnpb' 20 | } 21 | ], 22 | postlinks: '', 23 | }, 24 | ]; 25 | 26 | const enPromoAdditionsArray = [ 27 | { 28 | prelinks: 'Powered by', 29 | links: [ 30 | { 31 | prefix: ' ', 32 | text: '1inch', 33 | postfix: ' ', 34 | link: 'https://jn3rg.app.link/c10FJKdtnpb' 35 | } 36 | ], 37 | postlinks: '', 38 | }, 39 | ]; 40 | 41 | function promoFromStruct (promo) { 42 | return promo.links.reduce( 43 | (s, item) => s + item.prefix + '' + item.text + '' + item.postfix, 44 | promo.prelinks, 45 | ) + promo.postlinks; 46 | } 47 | 48 | function promoFromStructWithoutHtml (promo) { 49 | const text = promo.links.reduce( 50 | (s, item) => s + item.prefix + item.text + item.postfix, 51 | promo.prelinks, 52 | ) + promo.postlinks; 53 | 54 | let s = promo.prelinks.length; 55 | return { 56 | text, 57 | links: promo.links.map(item => { 58 | s += (item.prefix.length + item.text.length + item.postfix.length); 59 | return { 60 | offset: s - (item.text.length + item.postfix.length), 61 | length: item.text.length, 62 | link: item.link, 63 | }; 64 | }), 65 | } 66 | } 67 | 68 | export const promoAdditions = { 69 | ru: (rand) => promoFromStruct(ruPromoAdditionsArray[Math.trunc(rand * ruPromoAdditionsArray.length)]), 70 | en: (rand) => promoFromStruct(enPromoAdditionsArray[Math.trunc(rand * enPromoAdditionsArray.length)]), 71 | } 72 | 73 | export const promoAdditionsWithoutHtml = { 74 | ru: (rand) => promoFromStructWithoutHtml(ruPromoAdditionsArray[Math.trunc(rand * ruPromoAdditionsArray.length)]), 75 | en: (rand) => promoFromStructWithoutHtml(enPromoAdditionsArray[Math.trunc(rand * enPromoAdditionsArray.length)]), 76 | } 77 | 78 | export const promoExceptions = [ 79 | -1001007166727, 80 | 81 | -1001295782139, 82 | -1001233073874, 83 | -1001060565714, 84 | -1001070350591, 85 | -1001098630768, 86 | -1001145658234, 87 | -1001271442507, 88 | -1001286547060, 89 | -1001093535082, 90 | 91 | -1001214141592, 92 | 93 | -1001372515447, 94 | 95 | -1001078017687, 96 | -1001224633906, 97 | -1001267580592, 98 | 99 | -1001217329168, 100 | -1001424820550, 101 | 102 | -1001061479007, 103 | 104 | -1001166354679, 105 | -1001456580426, 106 | -1001207646926, 107 | -1001396223082, 108 | 109 | -1001576849880, 110 | ] 111 | -------------------------------------------------------------------------------- /src/helpers/report.ts: -------------------------------------------------------------------------------- 1 | import { checkIfErrorDismissable } from '@helpers/error' 2 | import { bot } from '@helpers/bot' 3 | import * as mongoose from 'mongoose' 4 | import {Mongoose, Schema} from "mongoose"; 5 | 6 | let errorsToReport = [] 7 | 8 | async function bulkReport() { 9 | const tempErrorsToReport = errorsToReport 10 | errorsToReport = [] 11 | const reportChatId = process.env.REPORT_CHAT_ID 12 | if (!reportChatId) { 13 | return 14 | } 15 | if (tempErrorsToReport.length > 0) { 16 | const reportText = tempErrorsToReport.reduce( 17 | (prev, cur) => `${prev}${cur}\n`, 18 | '' 19 | ) 20 | const chunks = reportText.match(/[\s\S]{1,4000}/g) 21 | for (const chunk of chunks) { 22 | try { 23 | await bot.telegram.sendMessage(reportChatId, chunk) 24 | } catch (err) { 25 | console.error(err) 26 | } 27 | } 28 | } 29 | } 30 | 31 | setInterval(bulkReport, 60 * 1000) 32 | 33 | export function report(error: Error, reason?: string) { 34 | if (checkIfErrorDismissable(error)) { 35 | return 36 | } 37 | console.error(reason, error) 38 | errorsToReport.push(`${reason ? `${reason}\n` : ''}${error.message}`) 39 | } 40 | 41 | mongoose.plugin(log) 42 | 43 | function log(schema: Schema) { 44 | let handleError = (error, doc, next) => { 45 | if (error?.errmsg || error?.message) { 46 | errorsToReport.push(`global db error\n${error.errmsg || error.message}`) 47 | return next(error); 48 | } 49 | next(); 50 | }; 51 | schema.post('validate', handleError); 52 | schema.post('save', handleError); 53 | schema.post('update', handleError); 54 | schema.post('insertMany', handleError); 55 | schema.post('find', handleError); 56 | schema.post('findOne', handleError); 57 | schema.post('findOneAndUpdate', handleError); 58 | schema.post('findOneAndRemove', handleError); 59 | } 60 | -------------------------------------------------------------------------------- /src/helpers/restrictedUsers.ts: -------------------------------------------------------------------------------- 1 | import { report } from '@helpers/report' 2 | import { Chat, Candidate, ChatModel } from '@models/Chat' 3 | import { User } from 'telegraf/typings/telegram-types' 4 | 5 | export async function modifyRestrictedUsers( 6 | chat: Chat, 7 | add: boolean, 8 | candidatesAndUsers: Array 9 | ) { 10 | if (!candidatesAndUsers.length) { 11 | return 12 | } 13 | try { 14 | if (add) { 15 | await ChatModel.updateOne( 16 | { _id: chat._id }, 17 | { 18 | $push: { 19 | restrictedUsers: candidatesAndUsers.map((v: Candidate) => { 20 | v.restrictTime = chat.restrictTime || 24 21 | return v 22 | }), 23 | }, 24 | } 25 | ) 26 | } else { 27 | const candidatesIds = candidatesAndUsers.map((c) => c.id) 28 | await ChatModel.updateOne( 29 | { _id: chat._id }, 30 | { $pull: { restrictedUsers: { id: { $in: candidatesIds } } } }, 31 | { multi: true } 32 | ) 33 | } 34 | } catch (err) { 35 | report(err, modifyRestrictedUsers.name) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/helpers/saveChatProperty.ts: -------------------------------------------------------------------------------- 1 | import { Chat, ChatModel } from '@models/Chat' 2 | import { DocumentType } from '@typegoose/typegoose' 3 | 4 | export function saveChatProperty(chat: DocumentType, property: string) { 5 | const change = {} 6 | change[property] = chat[property] 7 | return ChatModel.updateOne({ _id: chat._id }, { $set: change }) 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/strings.ts: -------------------------------------------------------------------------------- 1 | import { Chat } from '@models/Chat' 2 | import { localizations } from '@helpers/localizations' 3 | 4 | export function strings(chat: Chat, key: string) { 5 | return ( 6 | localizations[key][chat.language] || 7 | localizations[key]['en'] || 8 | `🤔 Localization not found, please, contact @borodutch. 9 | 10 | Локализация не найдена, пожалуйста, напишите @borodutch.` 11 | ) 12 | } 13 | 14 | export * from '@helpers/localizations' 15 | -------------------------------------------------------------------------------- /src/helpers/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe' 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 4 | apiVersion: '2020-08-27', 5 | }) 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config({ path: `${__dirname}/../.env` }) 4 | import { Context } from 'telegraf' 5 | import { report } from '@helpers/report' 6 | import { bot } from '@helpers/bot' 7 | import { isMaster, fork } from 'cluster' 8 | import { cpus } from 'os' 9 | import { startServer } from './server' 10 | 11 | // Generate cluster workers 12 | const workers = [] 13 | if (isMaster) { 14 | console.info(`Master ${process.pid} is running`) 15 | for (let i = 0; i < cpus().length; i += 1) { 16 | const worker = fork() 17 | workers.push(worker) 18 | } 19 | if (process.env.PREMIUM === 'true') { 20 | startServer() 21 | } 22 | } else { 23 | const handler = require('./updateHandler') 24 | console.info(`Worker ${process.pid} started`) 25 | process.on('message', (update) => { 26 | handler.handleUpdate(update) 27 | }) 28 | } 29 | 30 | // Start bot 31 | if (isMaster) { 32 | bot.use((ctx) => { 33 | handleCtx(ctx) 34 | }) 35 | bot 36 | .launch({ 37 | polling: { 38 | allowedUpdates: [ 39 | 'callback_query', 40 | 'chosen_inline_result', 41 | 'edited_message', 42 | 'inline_query', 43 | 'message', 44 | 'poll', 45 | 'poll_answer', 46 | 'chat_member', 47 | ] as any, 48 | }, 49 | }) 50 | .then(() => { 51 | console.info('Bot on the main thread is up and running') 52 | }) 53 | .catch(report) 54 | } 55 | 56 | // Handle update 57 | let clusterNumber = 0 58 | function handleCtx(ctx: Context) { 59 | if (clusterNumber >= workers.length) { 60 | clusterNumber = 0 61 | } 62 | const worker = workers[clusterNumber] 63 | if (worker) { 64 | clusterNumber += 1 65 | worker.send(ctx.update) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/kickChecker.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config({ path: `${__dirname}/../.env` }) 4 | import '@models' 5 | import { report } from '@helpers/report' 6 | import { findChatsWithCandidates } from '@models/Chat' 7 | import { kickCandidates } from '@helpers/newcomers/kickCandidates' 8 | import { modifyRestrictedUsers } from '@helpers/restrictedUsers' 9 | 10 | let checking = false 11 | 12 | // Check candidates 13 | setInterval(async () => { 14 | console.log( 15 | 'Trying to check candidates, current checking status is', 16 | checking 17 | ) 18 | if (!checking) { 19 | check() 20 | } 21 | }, 15 * 1000) 22 | 23 | async function check() { 24 | checking = true 25 | try { 26 | console.log('Getting chats with candidates') 27 | const start = Date.now() 28 | const chats = await findChatsWithCandidates(Number(process.env.CHAT_LIMIT) || 200) 29 | const end = Date.now() 30 | console.log(`Found ${chats.length} chats with candidates in ${end - start} ms`) 31 | for (const chat of chats) { 32 | // Check candidates 33 | const candidatesToDelete = [] 34 | for (const candidate of chat.candidates) { 35 | if ( 36 | new Date().getTime() - candidate.timestamp < 37 | chat.timeGiven * 1000 38 | ) { 39 | continue 40 | } 41 | candidatesToDelete.push(candidate) 42 | } 43 | if (candidatesToDelete.length) { 44 | console.log( 45 | `Kicking ${candidatesToDelete.length} candidates at ${chat.id}` 46 | ) 47 | try { 48 | await kickCandidates(chat, candidatesToDelete) 49 | } catch (err) { 50 | report(err, 'kickCandidatesAfterCheck') 51 | } 52 | } 53 | // Check restricted users 54 | const restrictedToDelete = [] 55 | for (const candidate of chat.restrictedUsers) { 56 | if ( 57 | new Date().getTime() - candidate.timestamp > 58 | (candidate.restrictTime || 24) * 60 * 60 * 1000 59 | ) { 60 | restrictedToDelete.push(candidate) 61 | } 62 | } 63 | if (restrictedToDelete.length) { 64 | try { 65 | await modifyRestrictedUsers(chat, false, restrictedToDelete) 66 | } catch (err) { 67 | report(err, 'removeRestrictAfterCheck') 68 | } 69 | } 70 | } 71 | } catch (err) { 72 | report(err, 'checking candidates') 73 | } finally { 74 | console.log('Finished checking chats with candidates') 75 | checking = false 76 | } 77 | } 78 | 79 | console.log('Kicker is up and running') 80 | -------------------------------------------------------------------------------- /src/messageDeleter.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config({ path: `${__dirname}/../.env` }) 4 | import '@models' 5 | import { report } from '@helpers/report' 6 | import { findMessagesToDelete } from '@models/MessageToDelete' 7 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 8 | 9 | let checking = false 10 | 11 | // Check candidates 12 | setInterval(async () => { 13 | if (!checking) { 14 | check() 15 | } 16 | }, 5 * 1000) 17 | 18 | async function check() { 19 | checking = true 20 | try { 21 | const messages = await findMessagesToDelete() 22 | await Promise.all( 23 | messages.map( 24 | (message) => 25 | new Promise(async (res) => { 26 | try { 27 | await deleteMessageSafeWithBot( 28 | message.chat_id, 29 | message.message_id 30 | ) 31 | await message.remove() 32 | } catch { 33 | // Do nothing 34 | } finally { 35 | res() 36 | } 37 | }) 38 | ) 39 | ) 40 | } catch (err) { 41 | report(err, 'deleting messages') 42 | } finally { 43 | checking = false 44 | } 45 | } 46 | 47 | console.log('Deleter is up and running') 48 | -------------------------------------------------------------------------------- /src/middlewares/attachChatMember.ts: -------------------------------------------------------------------------------- 1 | import { report } from '@helpers/report' 2 | import { isGroup } from '@helpers/isGroup' 3 | import { Context } from 'telegraf' 4 | 5 | export async function attachChatMember(ctx: Context, next) { 6 | if (ctx.update.message?.date && ctx.update.message?.text === '/help') { 7 | console.log( 8 | 'Got to attachChatMember on help', 9 | Date.now() / 1000 - ctx.update.message?.date 10 | ) 11 | } 12 | // If not a group, no need to get the member 13 | if (!isGroup(ctx)) { 14 | ctx.isAdministrator = true 15 | return next() 16 | } 17 | try { 18 | const chatMemberFromTelegram = await ctx.getChatMember(ctx.from.id) 19 | ctx.isAdministrator = ['creator', 'administrator'].includes( 20 | chatMemberFromTelegram.status 21 | ) 22 | } catch (err) { 23 | // If anything above fails, just assume it's not an admin 24 | ctx.isAdministrator = false 25 | report(err, ctx.getChatMember.name) 26 | } finally { 27 | return next() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/middlewares/attachUser.ts: -------------------------------------------------------------------------------- 1 | import { findChat } from '@models/Chat' 2 | import { Context } from 'telegraf' 3 | 4 | export async function attachUser(ctx: Context, next) { 5 | if (ctx.update.message?.date && ctx.update.message?.text === '/help') { 6 | console.log( 7 | 'Got to attachUser on help', 8 | Date.now() / 1000 - ctx.update.message?.date 9 | ) 10 | } 11 | // Just drop the update if there is no chat 12 | if (!ctx.chat) { 13 | return 14 | } 15 | const chat = await findChat(ctx.chat.id) 16 | if (ctx.update.message?.date && ctx.update.message?.text === '/help') { 17 | console.log( 18 | 'Got to attachUser on help, found user', 19 | Date.now() / 1000 - ctx.update.message?.date 20 | ) 21 | } 22 | ctx.dbchat = chat 23 | return next() 24 | } 25 | -------------------------------------------------------------------------------- /src/middlewares/checkBlockList.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | 3 | const blocklist = [-1001410821804] 4 | 5 | export async function checkBlockList(ctx: Context, next: Function) { 6 | if (blocklist.includes(ctx.chat?.id)) { 7 | return 8 | } 9 | return next() 10 | } 11 | -------------------------------------------------------------------------------- /src/middlewares/checkIfFromReplier.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | import { strings } from '@helpers/strings' 3 | 4 | export async function checkIfFromReplier(ctx: Context, next: () => any) { 5 | if ( 6 | ctx.callbackQuery && 7 | ctx.callbackQuery.message && 8 | ctx.callbackQuery.message.reply_to_message 9 | ) { 10 | const message = ctx.callbackQuery.message 11 | // Anonymous admin 12 | if ( 13 | message.reply_to_message && 14 | message.reply_to_message.from && 15 | message.reply_to_message.from.username && 16 | message.reply_to_message.from.username === 'GroupAnonymousBot' 17 | ) { 18 | next() 19 | return 20 | } 21 | if (ctx.callbackQuery.from.id !== message.reply_to_message.from.id) { 22 | try { 23 | await ctx.answerCbQuery(strings(ctx.dbchat, 'only_author_can_reply')) 24 | } catch { 25 | // Do nothing 26 | } 27 | return 28 | } 29 | } 30 | next() 31 | } 32 | -------------------------------------------------------------------------------- /src/middlewares/checkIfGroup.ts: -------------------------------------------------------------------------------- 1 | import { isGroup } from '@helpers/isGroup' 2 | import { Context } from 'telegraf' 3 | 4 | export async function checkIfGroup(ctx: Context, next: Function) { 5 | if (!isGroup(ctx)) { 6 | return 7 | } 8 | return next() 9 | } 10 | -------------------------------------------------------------------------------- /src/middlewares/checkLock.ts: -------------------------------------------------------------------------------- 1 | import { isGroup } from '@helpers/isGroup' 2 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 3 | import { Context } from 'telegraf' 4 | 5 | export async function checkLock(ctx: Context, next: () => any) { 6 | // If loccked, private messages or channel, then continue 7 | if (!ctx.dbchat.adminLocked || !isGroup(ctx)) { 8 | return next() 9 | } 10 | // If super admin, then continue 11 | if (ctx.from.id === parseInt(process.env.ADMIN)) { 12 | return next() 13 | } 14 | // If from the group anonymous bot, then continue 15 | if (ctx.from?.username === 'GroupAnonymousBot') { 16 | return next() 17 | } 18 | // If from admin, then continue 19 | if (ctx.isAdministrator) { 20 | return next() 21 | } 22 | // Otherwise, remove the message 23 | await deleteMessageSafeWithBot(ctx.chat.id, ctx.message.message_id) 24 | } 25 | -------------------------------------------------------------------------------- /src/middlewares/checkNoChannelLinks.ts: -------------------------------------------------------------------------------- 1 | import { isGroup } from '@helpers/isGroup' 2 | import { deleteMessageSafe } from '@helpers/deleteMessageSafe' 3 | import { Context } from 'telegraf' 4 | import tall from 'tall' 5 | 6 | const disallowedUrlParts = ['http://t.me/', 'https://t.me/'] 7 | 8 | export async function checkNoChannelLinks(ctx: Context, next: Function) { 9 | if (ctx.update.message?.date && ctx.update.message?.text === '/help') { 10 | console.log( 11 | 'Got to checkNoChannelLinks on help', 12 | Date.now() / 1000 - ctx.update.message?.date 13 | ) 14 | } 15 | // Get the message 16 | const message = ctx.editedMessage || ctx.message 17 | // If there is no message, just continue 18 | if (!message) { 19 | return next() 20 | } 21 | // If there is no need to check for links, just continue 22 | if (!ctx.dbchat.noChannelLinks) { 23 | return next() 24 | } 25 | // If sent from private chat or channel, just continue 26 | if (!isGroup(ctx)) { 27 | return next() 28 | } 29 | // If there are no url entities, just continue 30 | const allEntities = (message.entities || []).concat( 31 | message.caption_entities || [] 32 | ) 33 | if ( 34 | !allEntities.length || 35 | !allEntities.reduce( 36 | (p, c) => c.type === 'url' || c.type === 'text_link' || p, 37 | false 38 | ) 39 | ) { 40 | return next() 41 | } 42 | // If sent from admins, just ignore 43 | const adminIds = [777000, parseInt(process.env.ADMIN)] 44 | if (adminIds.includes(ctx.from.id) || ctx.isAdministrator) { 45 | return next() 46 | } 47 | // Create a placeholder if the message needs deletion 48 | let needsToBeDeleted = false 49 | // Check all entities 50 | for await (let entity of allEntities) { 51 | // Skip unnecessary entities 52 | if (entity.type !== 'url' && entity.type !== 'text_link') { 53 | continue 54 | } 55 | // Get url 56 | let url: string 57 | if (entity.type == 'text_link' && entity.url) { 58 | url = entity.url 59 | } else { 60 | url = (message.text || message.caption).substring( 61 | entity.offset, 62 | entity.offset + entity.length 63 | ) 64 | } 65 | // If the link is a telegram link, mark the message for deletion 66 | if (checkIfUrlIncludesDisallowedParts(url, ctx.chat.username)) { 67 | needsToBeDeleted = true 68 | break 69 | } 70 | // Try to unshorten the link 71 | try { 72 | // Add http just in case 73 | url = 74 | url.includes('https://') || url.includes('http://') 75 | ? url 76 | : 'http://' + url 77 | // Unshorten the url 78 | const unshortenedUrl = await tall(url) 79 | // If the link is a telegram link, mark the message for deletion 80 | if ( 81 | checkIfUrlIncludesDisallowedParts(unshortenedUrl, ctx.chat.username) 82 | ) { 83 | needsToBeDeleted = true 84 | break 85 | } 86 | } catch (err) { 87 | // Do nothing 88 | } 89 | } 90 | // If one of the links in the message is a telegram link, delete the message 91 | if (needsToBeDeleted) { 92 | deleteMessageSafe(ctx) 93 | return 94 | } 95 | // Or just continue 96 | return next() 97 | } 98 | 99 | function checkIfUrlIncludesDisallowedParts(url: string, chatUsername: string) { 100 | for (const part of disallowedUrlParts) { 101 | if (url.includes(part) && !url.includes(`://t.me/${chatUsername}`)) { 102 | return true 103 | } 104 | } 105 | return false 106 | } 107 | -------------------------------------------------------------------------------- /src/middlewares/checkRestrict.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | import { isGloballyRestricted } from '@helpers/globallyRestricted' 3 | import { deleteMessageSafe } from '@helpers/deleteMessageSafe' 4 | import { MessageEntity } from 'typegram' 5 | 6 | export async function checkRestrict(ctx: Context, next: () => any) { 7 | if (ctx.update.message?.date && ctx.update.message?.text === '/help') { 8 | console.log( 9 | 'Got to checkRestrict on help', 10 | Date.now() / 1000 - ctx.update.message?.date 11 | ) 12 | } 13 | // Get the message 14 | const message = ctx.editedMessage || ctx.message 15 | // Continue if there is no message 16 | if (!message) { 17 | return next() 18 | } 19 | // Continue if the restrict is off 20 | if (!ctx.dbchat.restrict) { 21 | return next() 22 | } 23 | // Don't restrict super admin 24 | if (ctx.from.id === parseInt(process.env.ADMIN)) { 25 | return next() 26 | } 27 | // Just delete the message if globally restricted 28 | if (isGloballyRestricted(ctx.from.id)) { 29 | deleteMessageSafe(ctx) 30 | return 31 | } 32 | // Check if this user is restricted 33 | const restricted = ctx.dbchat.restrictedUsers 34 | .map((u) => u.id) 35 | .includes(ctx.from.id) 36 | // If a restricted user tries to send restricted type, just delete it 37 | if ( 38 | restricted && 39 | ((message.entities && 40 | message.entities.length && 41 | entitiesContainMedia(message.entities)) || 42 | (message.caption_entities && 43 | message.caption_entities.length && 44 | entitiesContainMedia(message.caption_entities)) || 45 | message.forward_from || 46 | message.forward_date || 47 | message.forward_from_chat || 48 | message.document || 49 | message.sticker || 50 | message.photo || 51 | message.video_note || 52 | message.video || 53 | message.game) 54 | ) { 55 | deleteMessageSafe(ctx) 56 | return 57 | } 58 | // Or just continue 59 | return next() 60 | } 61 | 62 | const allowedEntities = [ 63 | 'hashtag', 64 | 'cashtag', 65 | 'bold', 66 | 'italic', 67 | 'underline', 68 | 'strikethrough', 69 | ] 70 | function entitiesContainMedia(entities: MessageEntity[]) { 71 | for (const entity of entities) { 72 | if (!allowedEntities.includes(entity.type)) { 73 | return true 74 | } 75 | } 76 | return false 77 | } 78 | -------------------------------------------------------------------------------- /src/middlewares/checkSubscription.ts: -------------------------------------------------------------------------------- 1 | import { sendSubscriptionButtons } from '@commands/subscription' 2 | import { SubscriptionStatus } from '@models/Chat' 3 | import { Context } from 'telegraf' 4 | 5 | export function checkSubscription(ctx: Context, next) { 6 | if ( 7 | ctx.chat.type !== 'private' && 8 | ctx.dbchat.subscriptionStatus === SubscriptionStatus.inactive 9 | ) { 10 | return sendSubscriptionButtons(ctx) 11 | } 12 | return next() 13 | } 14 | -------------------------------------------------------------------------------- /src/middlewares/checkSuperAdmin.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | 3 | export function checkSuperAdmin(ctx: Context, next) { 4 | if (ctx.from.id !== parseInt(process.env.ADMIN, 10)) { 5 | return 6 | } 7 | return next() 8 | } 9 | -------------------------------------------------------------------------------- /src/middlewares/checkTime.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | 3 | export async function checkTime(ctx: Context, next: () => any) { 4 | if (ctx.update.message?.date && ctx.update.message?.text === '/help') { 5 | console.log( 6 | 'Got to checkTime on help', 7 | Date.now() / 1000 - ctx.update.message?.date 8 | ) 9 | } 10 | switch (ctx.updateType) { 11 | case 'message': 12 | if (new Date().getTime() / 1000 - ctx.message.date < 5 * 60) { 13 | return next() 14 | } 15 | break 16 | case 'callback_query': 17 | if ( 18 | ctx.callbackQuery.message && 19 | new Date().getTime() / 1000 - ctx.callbackQuery.message.date < 5 * 60 20 | ) { 21 | return next() 22 | } 23 | break 24 | default: 25 | return next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/middlewares/logTimeReceived.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | import { appendFile } from 'fs' 3 | 4 | export function logTimeReceived(ctx: Context, next: Function) { 5 | if (ctx.update.message && ctx.update.message.date) { 6 | logCtx(ctx) 7 | } 8 | return next() 9 | } 10 | 11 | function logCtx(ctx: Context) { 12 | return new Promise((res) => { 13 | res() 14 | try { 15 | appendFile( 16 | `${__dirname}/../../updates.log`, 17 | `\n${Math.floor(Date.now() / 1000)} — ${ctx.update.update_id} — ${ 18 | Math.floor(Date.now() / 1000) - ctx.update.message.date 19 | }s`, 20 | (err) => { 21 | if (err) { 22 | console.error(err) 23 | } 24 | } 25 | ) 26 | } catch { 27 | // Do nothing 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/middlewares/messageSaver.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'telegraf' 2 | import { Worker } from 'worker_threads' 3 | 4 | export async function messageSaver(ctx: Context, next) { 5 | try { 6 | const message = ctx.update.edited_message || ctx.update.message 7 | if (message && message.message_id && message.from?.id && message.chat.id) { 8 | if ( 9 | (message.entities && message.entities.length) || 10 | (message.caption_entities && message.caption_entities.length) || 11 | message.forward_from || 12 | message.forward_date || 13 | message.forward_from_chat || 14 | message.document || 15 | message.sticker || 16 | message.photo || 17 | message.video_note || 18 | message.video || 19 | message.game 20 | ) { 21 | saveMessage(ctx) 22 | } 23 | } 24 | } catch { 25 | // Do nothing 26 | } 27 | return next() 28 | } 29 | 30 | const messageSaverWorker = new Worker(__dirname + '/messageSaverWorker.js') 31 | 32 | async function saveMessage(ctx: Context) { 33 | if (ctx.update.message?.new_chat_members) { 34 | return 35 | } 36 | const message = ctx.update.edited_message || ctx.update.message 37 | messageSaverWorker.postMessage(message) 38 | } 39 | -------------------------------------------------------------------------------- /src/middlewares/messageSaverWorker.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config({ path: `${__dirname}/../../.env` }) 4 | import '@models' 5 | 6 | import { CappedMessageModel } from '@models/CappedMessage' 7 | import { parentPort } from 'worker_threads' 8 | 9 | parentPort.on('message', async (message) => { 10 | await new CappedMessageModel({ 11 | message_id: message.message_id, 12 | from_id: message.from.id, 13 | chat_id: message.chat.id, 14 | }).save() 15 | }) 16 | -------------------------------------------------------------------------------- /src/models/CappedKickedUser.ts: -------------------------------------------------------------------------------- 1 | import { getModelForClass, prop } from '@typegoose/typegoose' 2 | 3 | export class CappedKickedUser { 4 | @prop({ required: true, index: true }) 5 | chatId: number 6 | @prop({ required: true, index: true }) 7 | userId: number 8 | } 9 | 10 | export const CappedKickedUserModel = getModelForClass(CappedKickedUser, { 11 | schemaOptions: { timestamps: true }, 12 | }) 13 | -------------------------------------------------------------------------------- /src/models/CappedMessage.ts: -------------------------------------------------------------------------------- 1 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 2 | import { getModelForClass, prop } from '@typegoose/typegoose' 3 | 4 | export class CappedMessage { 5 | @prop({ required: true, index: true }) 6 | message_id: number 7 | @prop({ required: true, index: true }) 8 | from_id: number 9 | @prop({ required: true, index: true }) 10 | chat_id: number 11 | 12 | // mongodb timestamp 13 | createdAt: Date 14 | } 15 | 16 | export const CappedMessageModel = getModelForClass(CappedMessage, { 17 | schemaOptions: { timestamps: true }, 18 | }) 19 | 20 | export async function removeMessages(chatId: number, fromId: number) { 21 | const messages = await CappedMessageModel.find({ 22 | chat_id: chatId, 23 | from_id: fromId, 24 | }) 25 | messages.forEach(async (message) => { 26 | await deleteMessageSafeWithBot(chatId, message.message_id) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/models/Chat.ts: -------------------------------------------------------------------------------- 1 | import { prop, getModelForClass } from '@typegoose/typegoose' 2 | import { Message, ChatMember } from 'telegram-typings' 3 | 4 | export enum SubscriptionStatus { 5 | inactive = 'inactive', 6 | active = 'active', 7 | lifetime = 'lifetime', 8 | } 9 | 10 | export enum Language { 11 | ENGLISH = 'en', 12 | RUSSIAN = 'ru', 13 | ITALIAN = 'it', 14 | ESTONIAN = 'et', 15 | UKRAINIAN = 'uk', 16 | PORTUGUESE = 'br', 17 | TURKISH = 'tr', 18 | SPANISH = 'es', 19 | CHINESE = 'zh', 20 | NORWEGIAN = 'no', 21 | GERMAN = 'de', 22 | TAIWAN = 'tw', 23 | FRENCH = 'fr', 24 | INDONESIAN = 'id', 25 | KOREAN = 'ko', 26 | AMHARIC = 'am', 27 | CZECH = 'cz', 28 | ARABIC = 'ar', 29 | JAPANESE = 'ja', 30 | ROMANIAN = 'ro', 31 | SLOVAK = 'sk', 32 | CATALAN = 'ca', 33 | CANTONESE = 'yue', 34 | BULGARIAN = 'bg', 35 | } 36 | 37 | export enum CaptchaType { 38 | SIMPLE = 'simple', 39 | DIGITS = 'digits', 40 | BUTTON = 'button', 41 | IMAGE = 'image', 42 | } 43 | 44 | export class Equation { 45 | question: String 46 | answer: String 47 | } 48 | 49 | export class Candidate { 50 | @prop({ required: true }) 51 | id: number 52 | @prop({ required: true }) 53 | timestamp: number 54 | @prop({ required: true, enum: CaptchaType }) 55 | captchaType: CaptchaType 56 | @prop() 57 | messageId?: number 58 | @prop() 59 | username?: string 60 | @prop() 61 | restrictTime?: number 62 | 63 | @prop() 64 | equationQuestion?: string 65 | @prop() 66 | equationAnswer?: string 67 | @prop() 68 | imageText?: string 69 | 70 | @prop() 71 | entryMessageId?: number 72 | @prop() 73 | leaveMessageId?: number 74 | @prop() 75 | entryChatId?: number 76 | } 77 | 78 | export class MessageWrapper { 79 | @prop({ required: true }) 80 | message: Message 81 | } 82 | 83 | export class MemberWrapper { 84 | @prop({ required: true }) 85 | id: number 86 | @prop({ required: true }) 87 | timestamp: number 88 | @prop({ required: true }) 89 | member: ChatMember 90 | } 91 | 92 | export class Chat { 93 | @prop({ required: true, index: true, unique: true }) 94 | id: number 95 | @prop({ required: true, enum: Language, default: Language.ENGLISH }) 96 | language: Language 97 | @prop({ required: true, enum: CaptchaType, default: CaptchaType.DIGITS }) 98 | captchaType: CaptchaType 99 | @prop({ required: true, default: 60 }) 100 | timeGiven: number 101 | @prop({ required: true, default: false }) 102 | adminLocked: boolean 103 | @prop({ required: true, default: true }) 104 | restrict: boolean 105 | @prop({ required: true, default: false }) 106 | noChannelLinks: boolean 107 | @prop({ required: true, default: false }) 108 | deleteEntryMessages: boolean 109 | @prop({ type: Candidate, default: [], index: true }) 110 | candidates: Candidate[] 111 | @prop({ type: Candidate, default: [], index: true }) 112 | restrictedUsers: Candidate[] 113 | @prop({ required: true, default: false }) 114 | greetsUsers: boolean 115 | @prop() 116 | greetingMessage?: MessageWrapper 117 | @prop({ required: true, default: false }) 118 | customCaptchaMessage: boolean 119 | @prop() 120 | captchaMessage?: MessageWrapper 121 | @prop({ required: true, default: true }) 122 | strict: boolean 123 | @prop() 124 | deleteGreetingTime?: number 125 | @prop({ required: true, default: false }) 126 | banUsers: boolean 127 | @prop({ required: true, default: false }) 128 | deleteEntryOnKick: boolean 129 | @prop({ required: true, default: true }) 130 | cas: boolean 131 | @prop({ required: true, default: false }) 132 | underAttack: boolean 133 | @prop({ required: true, default: false }) 134 | noAttack: boolean 135 | @prop() 136 | buttonText?: string 137 | @prop({ required: true, default: false }) 138 | allowInvitingBots: boolean 139 | @prop() 140 | greetingButtons?: string 141 | @prop({ required: true, default: false }) 142 | skipOldUsers: boolean 143 | @prop({ required: true, default: false }) 144 | skipVerifiedUsers: boolean 145 | @prop({ required: true, default: false }) 146 | banForFastRepliesToPosts: boolean 147 | @prop({ type: MemberWrapper, required: true, default: [] }) 148 | members: MemberWrapper[] 149 | @prop({ required: true, default: 24 }) 150 | restrictTime: number 151 | @prop({ required: true, default: false }) 152 | banNewTelegramUsers: boolean 153 | @prop({ enum: SubscriptionStatus, default: SubscriptionStatus.inactive }) 154 | subscriptionStatus: SubscriptionStatus 155 | @prop() 156 | subscriptionId?: string 157 | 158 | // mongo 159 | _id?: string 160 | } 161 | 162 | // Get Chat model 163 | export const ChatModel = getModelForClass(Chat, { 164 | schemaOptions: { timestamps: true }, 165 | }) 166 | 167 | // Get or create chat 168 | export async function findChat(id: number) { 169 | let chat = await ChatModel.findOne({ id }) 170 | if (!chat) { 171 | try { 172 | chat = await new ChatModel({ id }).save() 173 | } catch (err) { 174 | chat = await ChatModel.findOne({ id }) 175 | } 176 | } 177 | return chat 178 | } 179 | 180 | export async function findChatsWithCandidates(limit: number) { 181 | const chats: Chat[] = [] 182 | let lastId = '000000000000000000000000' 183 | while (true) { 184 | const chatList = await ChatModel.find( 185 | { 186 | $and: [ 187 | { _id: { $gt: lastId } }, 188 | { 189 | $or: [ 190 | { candidates: { $gt: [] } }, 191 | { restrictedUsers: { $gt: [] } }, 192 | ], 193 | }, 194 | ], 195 | }, 196 | { 197 | candidates: 1, 198 | restrictedUsers: 1, 199 | _id: 1, 200 | id: 1, 201 | deleteEntryOnKick: 1, 202 | banUsers: 1, 203 | timeGiven: 1, 204 | } 205 | ).limit(limit) 206 | chats.push(...chatList) 207 | 208 | if (chatList.length < limit) { 209 | return chats 210 | } 211 | 212 | lastId = chatList[chatList.length - 1]._id 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/models/EntryMessage.ts: -------------------------------------------------------------------------------- 1 | import { report } from '@helpers/report' 2 | import { deleteMessageSafeWithBot } from '@helpers/deleteMessageSafe' 3 | import { getModelForClass, prop, index } from '@typegoose/typegoose' 4 | 5 | @index({ createdAt: 1 }, { expireAfterSeconds: 60 * 60 }) 6 | export class EntryMessage { 7 | @prop({ required: true, index: true }) 8 | message_id: number 9 | @prop({ required: true, index: true }) 10 | from_id: number 11 | @prop({ required: true, index: true }) 12 | chat_id: number 13 | } 14 | 15 | export const EntryMessageModel = getModelForClass(EntryMessage, { 16 | schemaOptions: { timestamps: true }, 17 | }) 18 | 19 | export async function removeEntryMessages(chatId: number, fromId: number) { 20 | const messages = await EntryMessageModel.find({ 21 | chat_id: chatId, 22 | from_id: fromId, 23 | }) 24 | messages.forEach(async (message) => { 25 | await deleteMessageSafeWithBot(chatId, message.message_id) 26 | try { 27 | await message.remove() 28 | } catch (err) { 29 | report(err, 'removeEntryMessages') 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/models/MessageToDelete.ts: -------------------------------------------------------------------------------- 1 | import { getModelForClass, prop } from '@typegoose/typegoose' 2 | 3 | export class MessageToDelete { 4 | @prop({ required: true, index: true }) 5 | message_id: number 6 | @prop({ required: true, index: true }) 7 | chat_id: number 8 | @prop({ required: true }) 9 | deleteAt: Date 10 | } 11 | 12 | export const MessageToDeleteModel = getModelForClass(MessageToDelete, { 13 | schemaOptions: { timestamps: true }, 14 | }) 15 | 16 | export function findMessagesToDelete() { 17 | return MessageToDeleteModel.find({ 18 | deleteAt: { $lte: new Date() }, 19 | }) 20 | } 21 | 22 | export function addMessageToDelete( 23 | chatId: number, 24 | messageId: number, 25 | deleteAt = new Date() 26 | ) { 27 | return new MessageToDeleteModel({ 28 | chat_id: chatId, 29 | message_id: messageId, 30 | deleteAt: deleteAt, 31 | }).save() 32 | } 33 | -------------------------------------------------------------------------------- /src/models/VerifiedUser.ts: -------------------------------------------------------------------------------- 1 | import { prop, getModelForClass } from '@typegoose/typegoose' 2 | 3 | export class VerifiedUser { 4 | @prop({ required: true, index: true, unique: true }) 5 | id: number 6 | } 7 | 8 | export const VerifiedUserModel = getModelForClass(VerifiedUser, { 9 | schemaOptions: { timestamps: true }, 10 | }) 11 | 12 | export async function addVerifiedUser(id: number) { 13 | let user = await VerifiedUserModel.findOne({ id }) 14 | if (!user) { 15 | try { 16 | await new VerifiedUserModel({ id }).save() 17 | } catch { 18 | // Do nothing 19 | } 20 | } 21 | } 22 | 23 | export async function isVerifiedUser(id: number) { 24 | let user = await VerifiedUserModel.findOne({ id }) 25 | return !!user 26 | } 27 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose' 2 | import { setGlobalOptions, Severity } from '@typegoose/typegoose' 3 | 4 | mongoose.connect(process.env.MONGO, { 5 | useCreateIndex: true, 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | socketTimeoutMS: 5000, 9 | }) 10 | 11 | setGlobalOptions({ 12 | options: { 13 | allowMixed: Severity.ALLOW, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import * as Koa from 'koa' 3 | import * as bodyParser from 'koa-bodyparser' 4 | import { bootstrapControllers } from 'amala' 5 | import * as cors from '@koa/cors' 6 | import * as Router from 'koa-router' 7 | 8 | const app = new Koa() 9 | 10 | export async function startServer() { 11 | try { 12 | const router = new Router() 13 | await bootstrapControllers({ 14 | app, 15 | router, 16 | basePath: '/', 17 | controllers: [__dirname + '/controllers/*'], 18 | disableVersioning: true, 19 | bodyParser: false, 20 | }) 21 | app.use(cors({ origin: '*' })) 22 | app.use(bodyParser()) 23 | app.use(router.routes()) 24 | app.use(router.allowedMethods()) 25 | // Start rest 26 | app.listen(1347).on('listening', () => { 27 | console.log('HTTP is listening on 1347') 28 | }) 29 | } catch (err) { 30 | console.log('Koa app starting error: ', err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/telegraf.d.ts: -------------------------------------------------------------------------------- 1 | import { Chat } from '@models/Chat' 2 | import * as tt from '../../node_modules/telegraf/typings/telegram-types.d' 3 | import { DocumentType } from '@typegoose/typegoose' 4 | import { ChatMember } from '../../node_modules/telegraf/typings/telegram-types.d' 5 | 6 | declare module 'telegraf' { 7 | export class Context { 8 | public dbchat: DocumentType 9 | public chatMember?: ChatMember 10 | public isAdministrator: boolean 11 | replyWithMarkdown( 12 | markdown: string, 13 | extra?: tt.ExtraEditMessage | Extra 14 | ): Promise 15 | } 16 | export interface Composer { 17 | action( 18 | action: string | string[] | RegExp, 19 | middleware: Middleware, 20 | ...middlewares: Array> 21 | ): Composer 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/updateHandler.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config({ path: `${__dirname}/../.env` }) 4 | import '@models' 5 | import { attachUser } from '@middlewares/attachUser' 6 | import { bot } from '@helpers/bot' 7 | import { setupHelp } from '@commands/help' 8 | import { setupLanguage } from '@commands/language' 9 | import { setupCaptcha } from '@commands/captcha' 10 | import { checkMemberChange, setupNewcomers } from '@helpers/newcomers' 11 | import { setupTimeLimit } from '@commands/timeLimit' 12 | import { setupLock } from '@commands/lock' 13 | import { checkTime } from '@middlewares/checkTime' 14 | import { setupRestrict } from '@commands/restrict' 15 | import { checkRestrict } from '@middlewares/checkRestrict' 16 | import { setupNoChannelLinks } from '@commands/noChannelLinks' 17 | import { checkNoChannelLinks } from '@middlewares/checkNoChannelLinks' 18 | import { setupDeleteEntryMessages } from '@commands/deleteEntryMessages' 19 | import { setupGreeting } from '@commands/greeting' 20 | import { setupTrust } from '@commands/trust' 21 | import { setupStrict } from '@commands/strict' 22 | import { setupCaptchaMessage } from '@commands/captchaMessage' 23 | import { setupTestLocales } from '@commands/testLocales' 24 | import { setupDeleteGreetingTime } from '@commands/deleteGreetingTime' 25 | import { setupBanUsers } from '@commands/banUsers' 26 | import { setupDeleteEntryOnKick } from '@commands/deleteEntryOnKick' 27 | import { setupCAS } from '@commands/cas' 28 | import { setupBan } from '@commands/ban' 29 | import { setupUnderAttack } from '@commands/underAttack' 30 | import { setupNoAttack } from '@commands/noAttack' 31 | import { setupViewConfig } from '@commands/viewConfig' 32 | import { setupButtonText } from '@commands/buttonText' 33 | import { 34 | setupAllowInvitingBots, 35 | checkAllowInvitingBots, 36 | } from '@commands/allowInvitingBots' 37 | import { setupAdmin } from '@commands/admin' 38 | import { setupGreetingButtons } from '@commands/greetingButtons' 39 | import { setupSkipOldUsers } from '@commands/skipOldUsers' 40 | import { setupSkipVerifiedUsers } from '@commands/skipVerifiedUsers' 41 | import { setupSetConfig } from '@commands/setConfig' 42 | import { report } from '@helpers/report' 43 | import { attachChatMember } from '@middlewares/attachChatMember' 44 | import { checkBlockList } from '@middlewares/checkBlockList' 45 | import { isMaster } from 'cluster' 46 | import { setupBanForFastRepliesToPosts } from '@commands/banForFastRepliesToPosts' 47 | import { setupRestrictTime } from '@commands/restrictTime' 48 | import { setupBanNewTelegramUsers } from '@commands/banNewTelegramUsers' 49 | import { messageSaver } from '@middlewares/messageSaver' 50 | import { setup1inchInfo } from '@commands/1inch' 51 | import { checkSubscription } from '@middlewares/checkSubscription' 52 | import { setupSubscription } from '@commands/subscription' 53 | 54 | // Ignore all messages that are too old 55 | bot.use(checkTime) 56 | // Check block list 57 | bot.use(checkBlockList) 58 | // Add chat to context 59 | bot.use(attachUser) 60 | // Check premium 61 | if (process.env.PREMIUM === 'true') { 62 | bot.use(checkSubscription) 63 | } 64 | // Check if chat_member update 65 | bot.use(checkMemberChange) 66 | // Remove bots right when they get added 67 | bot.use(checkAllowInvitingBots) 68 | // Add chat member to context 69 | bot.use(attachChatMember) 70 | // Check if restricted 71 | bot.use(checkRestrict) 72 | // Check if channel links are present 73 | bot.use(checkNoChannelLinks) 74 | // Save messages that need saving 75 | bot.use(messageSaver) 76 | // Commands 77 | setupHelp(bot) 78 | setupLanguage(bot) 79 | setupCaptcha(bot) 80 | setupTimeLimit(bot) 81 | setupLock(bot) 82 | setupRestrict(bot) 83 | setupNoChannelLinks(bot) 84 | setupDeleteEntryMessages(bot) 85 | setupGreeting(bot) 86 | setupTrust(bot) 87 | setupStrict(bot) 88 | setupCaptchaMessage(bot) 89 | setupTestLocales(bot) 90 | setupDeleteGreetingTime(bot) 91 | setupBanUsers(bot) 92 | setupDeleteEntryOnKick(bot) 93 | setupCAS(bot) 94 | setupBan(bot) 95 | setupUnderAttack(bot) 96 | setupNoAttack(bot) 97 | setupViewConfig(bot) 98 | setupButtonText(bot) 99 | setupAllowInvitingBots(bot) 100 | setupAdmin(bot) 101 | setupGreetingButtons(bot) 102 | setupSkipOldUsers(bot) 103 | setupSkipVerifiedUsers(bot) 104 | setupSetConfig(bot) 105 | setupBanForFastRepliesToPosts(bot) 106 | setupRestrictTime(bot) 107 | setupBanNewTelegramUsers(bot) 108 | if (process.env.PREMIUM === 'true') { 109 | setupSubscription(bot) 110 | } 111 | // Newcomers logic 112 | setupNewcomers(bot) 113 | setup1inchInfo(bot) 114 | 115 | // Catch 116 | bot.catch(report) 117 | 118 | if (!isMaster) { 119 | // Start bot 120 | bot.telegram 121 | .getMe() 122 | .then((botInfo) => { 123 | ;(bot as any).botInfo = botInfo 124 | ;(bot as any).options.username = botInfo.username 125 | console.info(`Update handler on ${process.pid} started`) 126 | }) 127 | .catch(report) 128 | } 129 | 130 | module.exports = bot 131 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "lib": ["es2015"], 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "baseUrl": "src", 10 | "paths": { 11 | "@commands/*": ["commands/*"], 12 | "@helpers/*": ["helpers/*"], 13 | "@middlewares/*": ["middlewares/*"], 14 | "@models/*": ["models/*"] 15 | }, 16 | "emitDecoratorMetadata": true, 17 | "experimentalDecorators": true 18 | }, 19 | "include": ["src/**/*"] 20 | } 21 | --------------------------------------------------------------------------------