├── index.js
├── .env.example
├── crowdin.yml
├── middlewares
├── index.js
├── scenes.js
└── stats.js
├── README.md
├── example.config.json
├── database
├── models
│ ├── index.js
│ ├── stats.js
│ ├── user.js
│ ├── post.js
│ └── channel.js
├── connection.js
├── index.js
└── update-channels.js
├── .editorconfig
├── helpers
├── index.js
├── find-emojis.js
├── channel-get.js
├── group-get.js
├── user-get.js
└── update-keyboard.js
├── handlers
├── index.js
├── help.js
├── counter.js
├── language.js
├── rate.js
├── post.js
└── channels.js
├── .eslintrc.json
├── Dockerfile
├── ecosystem.config.js
├── docker-compose.yml
├── package.json
├── .gitignore
├── bot.js
├── locales
├── en.yaml
├── tr.yaml
├── id.yaml
├── az.yaml
├── pt.yaml
├── uk.yaml
└── ru.yaml
└── LICENSE
/index.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({ path: './.env' })
2 | require('./bot')
3 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=dev
2 | MONGODB_URI="mongodb://localhost:27017/reactBot"
3 | BOT_TOKEN=""
4 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | files:
2 | - source: /locales/en.yaml
3 | translation: /locales/%two_letters_code%.yaml
4 |
--------------------------------------------------------------------------------
/middlewares/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stats: require('./stats'),
3 | scenes: require('./scenes')
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # reacombot
2 |
3 | [](https://wakatime.com/badge/github/LyoSU/reacombot)
4 |
--------------------------------------------------------------------------------
/example.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "globalStickerSet": {
3 | "ownerId": 66478514,
4 | "name": "created_by_LyTestBot",
5 | "save_sticker_count": 1
6 | }
7 | }
--------------------------------------------------------------------------------
/database/models/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | User: require('./user'),
3 | Channel: require('./channel'),
4 | Post: require('./post'),
5 | Stats: require('./stats')
6 | }
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = space
9 | indent_size = 2
10 |
--------------------------------------------------------------------------------
/helpers/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | getUser: require('./user-get'),
3 | getChannel: require('./channel-get'),
4 | getGroup: require('./group-get'),
5 | keyboardUpdate: require('./update-keyboard'),
6 | findEmojis: require('./find-emojis')
7 | }
8 |
--------------------------------------------------------------------------------
/database/connection.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const connection = mongoose.createConnection(process.env.MONGODB_URI, {
4 | poolSize: 10,
5 | maxTimeMS: 3,
6 | useUnifiedTopology: true,
7 | useNewUrlParser: true
8 | })
9 |
10 | module.exports = connection
11 |
--------------------------------------------------------------------------------
/handlers/index.js:
--------------------------------------------------------------------------------
1 | module.exports = (bot) => {
2 | const handlers = [
3 | 'channels',
4 | 'language',
5 | 'post',
6 | 'rate',
7 | 'counter',
8 | 'help'
9 | ]
10 |
11 | handlers.forEach((handler) => {
12 | bot.use(require(`./${handler}`))
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/database/models/stats.js:
--------------------------------------------------------------------------------
1 | const { Schema } = require('mongoose')
2 |
3 | const statsSchema = Schema({
4 | rps: Number,
5 | responseTime: Number,
6 | date: {
7 | type: Date,
8 | index: true
9 | }
10 | }, {
11 | capped: { size: 1000 * 1000 * 100, max: 100000 }
12 | })
13 |
14 | module.exports = statsSchema
15 |
--------------------------------------------------------------------------------
/helpers/find-emojis.js:
--------------------------------------------------------------------------------
1 | const createEmojiRegex = require('emoji-regex')
2 |
3 | const emojiRegex = createEmojiRegex()
4 |
5 | module.exports = (str) => {
6 | const emojis = str.matchAll(emojiRegex)
7 | const result = []
8 | for (const emoji of emojis) {
9 | result.push(emoji[0])
10 | }
11 |
12 | return result
13 | }
14 |
--------------------------------------------------------------------------------
/database/index.js:
--------------------------------------------------------------------------------
1 | const collections = require('./models')
2 | const connection = require('./connection')
3 |
4 | const db = {
5 | connection
6 | }
7 |
8 | Object.keys(collections).forEach((collectionName) => {
9 | db[collectionName] = connection.model(collectionName, collections[collectionName])
10 | })
11 |
12 | module.exports = {
13 | db
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "extends": "standard",
7 | "globals": {
8 | "Atomics": "readonly",
9 | "SharedArrayBuffer": "readonly"
10 | },
11 | "parserOptions": {
12 | "ecmaVersion": 2018,
13 | "sourceType": "module"
14 | },
15 | "rules": {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nikolaik/python-nodejs:python3.8-nodejs12 AS builder
2 |
3 | ENV NODE_WORKDIR /app
4 | WORKDIR $NODE_WORKDIR
5 |
6 | ADD . $NODE_WORKDIR
7 |
8 | RUN apt-get update && apt-get install -y build-essential gcc wget git libvips && rm -rf /var/lib/apt/lists/*
9 |
10 | RUN ls -l node_modules/
11 |
12 | RUN npm install && npm install sharp@0.23.4 # TODO: sharp crashes if installed via npm install from installed via package.json
--------------------------------------------------------------------------------
/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [{
3 | name: 'reactBot',
4 | script: './index.js',
5 | max_memory_restart: '1000M',
6 | // instances: 1,
7 | // exec_mode: 'cluster',
8 | watch: true,
9 | ignore_watch: ['node_modules', 'assets', 'helpers/tdlib/data/db'],
10 | env: {
11 | NODE_ENV: 'development'
12 | },
13 | env_production: {
14 | NODE_ENV: 'production'
15 | }
16 | }]
17 | }
18 |
--------------------------------------------------------------------------------
/database/update-channels.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({ path: './.env' })
2 | const {
3 | db
4 | } = require('.')
5 | const {
6 | getChannel
7 | } = require('../helpers')
8 |
9 | const updateChannels = () => {
10 | const channels = db.Channel.find().cursor()
11 |
12 | channels.on('data', async (channel) => {
13 | const ch = await getChannel({ id: channel.channelId })
14 | await ch.save()
15 | console.log(ch.title)
16 | })
17 | }
18 |
19 | updateChannels()
20 |
--------------------------------------------------------------------------------
/database/models/user.js:
--------------------------------------------------------------------------------
1 | const { Schema } = require('mongoose')
2 |
3 | const userSchema = Schema({
4 | telegramId: {
5 | type: Number,
6 | index: true,
7 | unique: true,
8 | required: true
9 | },
10 | firstName: {
11 | type: String,
12 | index: true
13 | },
14 | lastName: {
15 | type: String,
16 | index: true
17 | },
18 | fullName: {
19 | type: String,
20 | index: true
21 | },
22 | username: {
23 | type: String
24 | },
25 | settings: {
26 | locale: String
27 | }
28 | }, {
29 | timestamps: true
30 | })
31 |
32 | module.exports = userSchema
33 |
--------------------------------------------------------------------------------
/handlers/help.js:
--------------------------------------------------------------------------------
1 | const Composer = require('telegraf/composer')
2 | const Markup = require('telegraf/markup')
3 |
4 | const help = async ctx => {
5 | await ctx.replyWithHTML(ctx.i18n.t('help'), Markup.keyboard([
6 | [
7 | ctx.i18n.t('menu.channels')
8 | ]
9 | ]).resize().extra({ disable_web_page_preview: true }))
10 | }
11 |
12 | const composer = new Composer()
13 |
14 | composer.use(Composer.privateChat((ctx, next) => {
15 | if (ctx.state.sendHelp) return help(ctx, next)
16 | else return next()
17 | }))
18 | composer.on('message', Composer.privateChat(help))
19 |
20 | module.exports = composer
21 |
--------------------------------------------------------------------------------
/middlewares/scenes.js:
--------------------------------------------------------------------------------
1 | const Stage = require('telegraf/stage')
2 | const { match } = require('telegraf-i18n')
3 |
4 | module.exports = (...stages) => {
5 | const stage = new Stage([].concat(...stages))
6 |
7 | stage.use((ctx, next) => {
8 | if (!ctx.session.scene) ctx.session.scene = {}
9 | return next()
10 | })
11 |
12 | const cancel = async (ctx, next) => {
13 | ctx.session.scene = null
14 | await ctx.scene.leave()
15 | return next()
16 | }
17 |
18 | stage.command(['help', 'start', 'channels', 'cancel', match('menu.channels')], cancel)
19 | stage.hears(match('menu.channels'), cancel)
20 |
21 | return stage
22 | }
23 |
--------------------------------------------------------------------------------
/handlers/counter.js:
--------------------------------------------------------------------------------
1 | const Composer = require('telegraf/composer')
2 |
3 | const composer = new Composer()
4 |
5 | composer.on('message', Composer.groupChat(async (ctx, next) => {
6 | if (ctx.message.reply_to_message && ctx.message.reply_to_message.forward_from_chat && ctx.message.reply_to_message.from.id === 777000) {
7 | const channel = await ctx.db.Channel.findOne({ channelId: ctx.message.reply_to_message.forward_from_chat.id })
8 | const post = await ctx.db.Post.findOne({ channel, channelMessageId: ctx.message.reply_to_message.forward_from_message_id })
9 | if (!post) return next()
10 |
11 | post.commentsCount += 1
12 | post.keyboardNextUpdate = new Date()
13 | await post.save()
14 | }
15 | return next()
16 | }))
17 |
18 | module.exports = composer
19 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | bot:
5 | build:
6 | context: .
7 | env_file: .env
8 | restart: always
9 | logging:
10 | driver: "json-file"
11 | options:
12 | max-size: "10m"
13 | max-file: "3"
14 | networks:
15 | - quotly
16 | command: node index.js
17 | mongo:
18 | restart: always
19 | image: mongo:4
20 | volumes:
21 | - mongo-volume:/data/db
22 | ports:
23 | - 127.0.0.1:27017:27017
24 | networks:
25 | - quotly
26 |
27 | volumes:
28 | mongo-volume:
29 | driver: 'local'
30 |
31 | networks:
32 | quotly:
33 | external: true
34 |
--------------------------------------------------------------------------------
/database/models/post.js:
--------------------------------------------------------------------------------
1 | const { Schema } = require('mongoose')
2 |
3 | const schema = Schema({
4 | channel: {
5 | type: Schema.Types.ObjectId,
6 | ref: 'Channel',
7 | index: true
8 | },
9 | channelMessageId: {
10 | type: Number,
11 | required: true
12 | },
13 | groupMessageId: {
14 | type: Number
15 | },
16 | rate: {
17 | votes: [{
18 | type: Object,
19 | name: String,
20 | vote: [{
21 | type: Schema.Types.ObjectId,
22 | ref: 'User'
23 | }]
24 | }],
25 | score: {
26 | type: Number,
27 | index: true
28 | }
29 | },
30 | keyboard: Array,
31 | commentsEnable: {
32 | type: Boolean,
33 | default: true
34 | },
35 | commentsCount: {
36 | type: Number,
37 | default: 0
38 | },
39 | keyboardNextUpdate: {
40 | type: Date,
41 | index: true
42 | }
43 | }, {
44 | timestamps: true
45 | })
46 |
47 | module.exports = schema
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reacombot",
3 | "version": "0.3.12",
4 | "description": "Telegram reacom bot",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node index.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/LyoSU/reacombot.git"
13 | },
14 | "author": "LyoSU",
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/LyoSU/reacombot/issues"
18 | },
19 | "homepage": "https://github.com/LyoSU/reacombot#readme",
20 | "dependencies": {
21 | "@pm2/io": "^4.3.5",
22 | "dotenv": "^8.6.0",
23 | "emoji-regex": "^10.2.1",
24 | "mongoose": "^5.13.2",
25 | "telegraf": "^3.39.0",
26 | "telegraf-i18n": "^6.6.0",
27 | "telegraf-ratelimit": "^2.0.0"
28 | },
29 | "devDependencies": {
30 | "eslint": "^6.8.0",
31 | "eslint-config-standard": "^14.1.0",
32 | "eslint-plugin-import": "^2.23.4",
33 | "eslint-plugin-node": "^10.0.0",
34 | "eslint-plugin-promise": "^4.3.1",
35 | "eslint-plugin-standard": "^4.0.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/models/channel.js:
--------------------------------------------------------------------------------
1 | const { Schema } = require('mongoose')
2 |
3 | const schema = Schema({
4 | channelId: {
5 | type: Number,
6 | index: true,
7 | unique: true,
8 | required: true
9 | },
10 | groupId: {
11 | type: Number,
12 | index: true
13 | },
14 | title: String,
15 | username: String,
16 | available: {
17 | type: Boolean,
18 | default: true
19 | },
20 | settings: {
21 | emojis: {
22 | type: String,
23 | default: '👍👎'
24 | },
25 | type: {
26 | type: String,
27 | enum: ['always', 'one', 'never', 'request'],
28 | default: 'always'
29 | },
30 | commentsType: {
31 | type: String,
32 | enum: ['always', 'one', 'never'],
33 | default: 'always'
34 | },
35 | showStart: {
36 | type: String,
37 | enum: ['top', 'bottom'],
38 | default: 'bottom'
39 | },
40 | keyboard: Array
41 | },
42 | administrators: [{
43 | user: {
44 | type: Number,
45 | index: true
46 | },
47 | status: String
48 | }]
49 | }, {
50 | timestamps: true
51 | })
52 |
53 | module.exports = schema
54 |
--------------------------------------------------------------------------------
/helpers/channel-get.js:
--------------------------------------------------------------------------------
1 | const Telegram = require('telegraf/telegram')
2 | const {
3 | db
4 | } = require('../database')
5 |
6 | const telegram = new Telegram(process.env.BOT_TOKEN)
7 |
8 | module.exports = async chat => {
9 | let newChannel = false
10 | let channel = await db.Channel.findOne({ channelId: chat.id })
11 |
12 | if (!channel) {
13 | newChannel = true
14 | channel = new db.Channel()
15 | channel.channelId = chat.id
16 | }
17 |
18 | if (chat.title) channel.title = chat.title
19 | if (chat.username) channel.username = chat.username
20 | channel.settings = channel.settings || new db.Channel().settings
21 |
22 | const chatAdministrators = await telegram.getChatAdministrators(chat.id).catch(console.error)
23 | if (!chatAdministrators) {
24 | channel.available = false
25 | } else {
26 | channel.administrators = []
27 |
28 | for (const admin of chatAdministrators) {
29 | channel.administrators.push({
30 | user: admin.user.id,
31 | status: admin.status
32 | })
33 | }
34 | }
35 |
36 | channel.updatedAt = new Date()
37 | if (newChannel) await channel.save()
38 |
39 | return channel
40 | }
41 |
--------------------------------------------------------------------------------
/helpers/group-get.js:
--------------------------------------------------------------------------------
1 | module.exports = async ctx => {
2 | let group
3 |
4 | if (!ctx.session.channelInfo.info) group = await ctx.db.Group.findOne({ group_id: ctx.chat.id })
5 | else group = ctx.session.channelInfo.info
6 |
7 | if (!group) {
8 | group = new ctx.db.Group()
9 | group.group_id = ctx.chat.id
10 | }
11 |
12 | group.title = ctx.chat.title
13 | group.username = ctx.chat.username
14 | group.settings = group.settings || new ctx.db.Group().settings
15 |
16 | if (!group.username && !group.invite_link) {
17 | // group.invite_link = await ctx.telegram.exportChatInviteLink(ctx.chat.id).catch(() => {})
18 | }
19 |
20 | if (ctx.i18n.languageCode === '-') {
21 | let lang = 'en'
22 | if (ctx.from.language_code) lang = ctx.from.language_code
23 | ctx.i18n.locale(lang)
24 | }
25 |
26 | group.updatedAt = new Date()
27 | ctx.session.channelInfo.info = group
28 | if (ctx.session.channelInfo.info.settings.locale) ctx.i18n.locale(ctx.session.channelInfo.info.settings.locale)
29 | else if (ctx.i18n.languageCode) {
30 | ctx.session.channelInfo.info.settings.locale = ctx.i18n.shortLanguageCode ? ctx.i18n.shortLanguageCode : ctx.i18n.languageCode
31 | await group.save()
32 | }
33 |
34 | return true
35 | }
36 |
--------------------------------------------------------------------------------
/helpers/user-get.js:
--------------------------------------------------------------------------------
1 | module.exports = async ctx => {
2 | let user
3 | let newUser = false
4 |
5 | if (!ctx.session.userInfo) {
6 | user = await ctx.db.User.findOne({ telegramId: ctx.from.id })
7 | } else {
8 | user = ctx.session.userInfo
9 | }
10 |
11 | const now = Math.floor(new Date().getTime() / 1000)
12 |
13 | if (!user) {
14 | newUser = true
15 | user = new ctx.db.User()
16 | user.telegramId = ctx.from.id
17 | user.first_act = now
18 | }
19 | user.firstName = ctx.from.first_name
20 | user.lastName = ctx.from.last_name
21 | user.fullName = `${ctx.from.first_name}${ctx.from.last_name ? ` ${ctx.from.last_name}` : ''}`
22 | user.username = ctx.from.username
23 | user.updatedAt = new Date()
24 |
25 | if (ctx.chat.type === 'private') user.status = 'member'
26 |
27 | if (newUser) await user.save()
28 |
29 | ctx.session.userInfo = user
30 |
31 | if (ctx.session.userInfo.settings.locale) {
32 | ctx.i18n.locale(ctx.session.userInfo.settings.locale)
33 | } else if (ctx.i18n.languageCode !== '-' && ctx.chat.type === 'private') {
34 | ctx.session.userInfo.settings.locale = ctx.i18n.shortLanguageCode
35 | } else if (ctx.i18n.languageCode === '-' && ctx.chat.type !== 'private') {
36 | ctx.i18n.locale('en')
37 | }
38 |
39 | return true
40 | }
41 |
--------------------------------------------------------------------------------
/handlers/language.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const Composer = require('telegraf/composer')
4 | const Markup = require('telegraf/markup')
5 | const I18n = require('telegraf-i18n')
6 |
7 | const composer = new Composer()
8 |
9 | const i18n = new I18n({
10 | directory: path.resolve(__dirname, '../locales'),
11 | defaultLanguage: 'ru',
12 | defaultLanguageOnMissing: true
13 | })
14 |
15 | const setLanguage = async (ctx, next) => {
16 | const localseFile = fs.readdirSync('./locales/')
17 |
18 | const locales = {}
19 |
20 | localseFile.forEach((fileName) => {
21 | const localName = fileName.split('.')[0]
22 | locales[localName] = {
23 | flag: i18n.t(localName, 'language_name')
24 | }
25 | })
26 |
27 | if (ctx.updateType === 'callback_query') {
28 | if (locales[ctx.match[1]]) {
29 | ctx.state.answerCbQuery = [locales[ctx.match[1]].flag]
30 |
31 | ctx.session.userInfo.settings.locale = ctx.match[1]
32 | ctx.i18n.locale(ctx.match[1])
33 | ctx.state.sendHelp = true
34 | await next(ctx)
35 | }
36 | } else {
37 | const button = []
38 |
39 | Object.keys(locales).map((key) => {
40 | button.push(Markup.callbackButton(locales[key].flag, `set_language:${key}`))
41 | })
42 |
43 | ctx.reply('🇷🇺 Выберите язык\n🇺🇸 Choose language\n\nHelp with translation: https://crwd.in/reacombot', {
44 | reply_markup: Markup.inlineKeyboard(button, {
45 | columns: 2
46 | })
47 | })
48 | }
49 | }
50 |
51 | composer.on('message', Composer.privateChat((ctx, next) => {
52 | if (ctx.i18n.languageCode === '-') return setLanguage(ctx, next)
53 | return next()
54 | }))
55 |
56 | composer.command('lang', setLanguage)
57 | composer.action(/set_language:(.*)/, setLanguage)
58 |
59 | module.exports = composer
60 |
--------------------------------------------------------------------------------
/handlers/rate.js:
--------------------------------------------------------------------------------
1 | const Composer = require('telegraf/composer')
2 | const rateLimit = require('telegraf-ratelimit')
3 | const {
4 | keyboardUpdate
5 | } = require('../helpers')
6 |
7 | const composer = new Composer()
8 |
9 | composer.action(/^(rate):(.*)/, rateLimit({
10 | window: 1000,
11 | limit: 1
12 | }), async ctx => {
13 | let resultText = ''
14 | const rateName = ctx.match[2]
15 |
16 | const { message } = ctx.callbackQuery
17 |
18 | const channel = await ctx.db.Channel.findOne({ channelId: message.chat.id })
19 | const post = await ctx.db.Post.findOne({ channel, channelMessageId: message.message_id })
20 |
21 | if (!post) return
22 |
23 | post.rate.votes.map(rate => {
24 | const indexRate = rate.vote.indexOf(ctx.from.id)
25 |
26 | if (indexRate > -1) rate.vote.splice(indexRate, 1)
27 | if (rateName === rate.name) {
28 | if (indexRate > -1) {
29 | resultText = ctx.i18n.t('rate.vote.back', { me: ctx.me })
30 | } else {
31 | resultText = ctx.i18n.t('rate.vote.rated', { rateName, me: ctx.me })
32 | rate.vote.push(ctx.from.id)
33 | }
34 | }
35 | })
36 |
37 | post.markModified('rate')
38 |
39 | if (post.rate.votes.length === 2) post.rate.score = post.rate.votes[0].vote.length - post.rate.votes[1].vote.length
40 |
41 | await post.save()
42 |
43 | const updateResult = await keyboardUpdate(channel.channelId, post.channelMessageId)
44 |
45 | if (updateResult.error && updateResult.error !== 'timeout') {
46 | const reactListArray = post.rate.votes.map(rate => {
47 | return `${rate.name} — ${rate.vote.length}`
48 | })
49 |
50 | ctx.state.answerCbQuery = [resultText + ctx.i18n.t('rate.vote.rated_limit', { rateName, reactList: reactListArray.join('\n') }), true]
51 | } else {
52 | ctx.state.answerCbQuery = [resultText]
53 | }
54 | })
55 |
56 | module.exports = composer
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 |
107 | !.gitkeep
108 | config.json
109 | .DS_Store
110 |
111 | helpers/tdlib/data
112 |
--------------------------------------------------------------------------------
/bot.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const Telegraf = require('telegraf')
3 | const session = require('telegraf/session')
4 | const I18n = require('telegraf-i18n')
5 | const io = require('@pm2/io')
6 | const {
7 | db
8 | } = require('./database')
9 | const {
10 | stats
11 | } = require('./middlewares')
12 | const {
13 | getUser,
14 | getChannel
15 | } = require('./helpers')
16 |
17 | const rpsIO = io.meter({
18 | name: 'req/sec',
19 | unit: 'update'
20 | })
21 |
22 | const bot = new Telegraf(process.env.BOT_TOKEN, {
23 | telegram: { webhookReply: false },
24 | handlerTimeout: 1
25 | })
26 |
27 | ;(async () => {
28 | console.log(await bot.telegram.getMe())
29 | })()
30 |
31 | bot.use((ctx, next) => {
32 | next().catch((error) => {
33 | console.log('Oops', error)
34 | })
35 | return true
36 | })
37 |
38 | bot.use(stats)
39 |
40 | bot.use((ctx, next) => {
41 | ctx.db = db
42 | return next()
43 | })
44 |
45 | bot.use((ctx, next) => {
46 | rpsIO.mark()
47 | ctx.telegram.oCallApi = ctx.telegram.callApi
48 | ctx.telegram.callApi = (method, data = {}) => {
49 | const startMs = new Date()
50 | return ctx.telegram.oCallApi(method, data).then((result) => {
51 | console.log(`end method ${method}:`, new Date() - startMs)
52 | return result
53 | })
54 | }
55 | return next()
56 | })
57 |
58 | bot.command('json', ({ replyWithHTML, message }) => replyWithHTML('' + JSON.stringify(message, null, 2) + ''))
59 |
60 | const i18n = new I18n({
61 | directory: path.resolve(__dirname, 'locales'),
62 | defaultLanguage: '-'
63 | })
64 |
65 | bot.use(i18n.middleware())
66 |
67 | bot.use(session({
68 | getSessionKey: (ctx) => {
69 | if (ctx.from && ctx.chat) {
70 | return `${ctx.from.id}:${ctx.chat.id}`
71 | } else if (ctx.from && ctx.inlineQuery) {
72 | return `${ctx.from.id}:${ctx.from.id}`
73 | } else if (ctx.chat && ctx.chat.type === 'channel') {
74 | return `channel:${ctx.chat.id}`
75 | }
76 | return null
77 | }
78 | }))
79 |
80 | bot.use(async (ctx, next) => {
81 | if (ctx.from) await getUser(ctx)
82 | if (ctx.channelPost) {
83 | ctx.session.channelInfo = await getChannel(ctx.chat)
84 | ctx.session.channelInfo.available = true
85 | }
86 |
87 | if (ctx.callbackQuery) {
88 | ctx.state.answerCbQuery = []
89 | }
90 | return next(ctx).then(() => {
91 | if (ctx.session && ctx.session.userInfo) ctx.session.userInfo.save()
92 | if (ctx.session && ctx.session.channelInfo) ctx.session.channelInfo.save()
93 | if (ctx.callbackQuery) return ctx.answerCbQuery(...ctx.state.answerCbQuery)
94 | })
95 | })
96 |
97 | require('./handlers')(bot)
98 |
99 | bot.use((ctx, next) => {
100 | ctx.state.emptyRequest = true
101 | return next()
102 | })
103 |
104 | db.connection.once('open', async () => {
105 | console.log('Connected to MongoDB')
106 |
107 | await bot.launch().then(() => {
108 | console.log('bot start polling')
109 | })
110 | })
111 |
--------------------------------------------------------------------------------
/middlewares/stats.js:
--------------------------------------------------------------------------------
1 | const io = require('@pm2/io')
2 | const {
3 | db
4 | } = require('../database')
5 |
6 | const stats = {
7 | rpsAvrg: 0,
8 | responseTimeAvrg: 0,
9 | times: {}
10 | }
11 |
12 | const noEmptyStats = {
13 | rpsAvrg: 0,
14 | responseTimeAvrg: 0,
15 | times: {}
16 | }
17 |
18 | const rtOP = io.metric({
19 | name: 'response time',
20 | unit: 'ms'
21 | })
22 |
23 | const usersCountIO = io.metric({
24 | name: 'Users count',
25 | unit: 'user'
26 | })
27 |
28 | const groupsCountIO = io.metric({
29 | name: 'Group count',
30 | unit: 'group'
31 | })
32 |
33 | setInterval(() => {
34 | if (Object.keys(noEmptyStats.times).length > 1) {
35 | const time = Object.keys(noEmptyStats.times).shift()
36 |
37 | const rps = noEmptyStats.times[time].length
38 | if (noEmptyStats.rpsAvrg > 0) noEmptyStats.rpsAvrg = (noEmptyStats.rpsAvrg + rps) / 2
39 | else noEmptyStats.rpsAvrg = rps
40 |
41 | const sumResponseTime = noEmptyStats.times[time].reduce((a, b) => a + b, 0)
42 | const lastResponseTimeAvrg = (sumResponseTime / noEmptyStats.times[time].length) || 0
43 | if (noEmptyStats.responseTimeAvrg > 0) noEmptyStats.responseTimeAvrg = (noEmptyStats.responseTimeAvrg + lastResponseTimeAvrg) / 2
44 | else noEmptyStats.responseTimeAvrg = lastResponseTimeAvrg
45 |
46 | console.log('📩 rps last:', rps)
47 | console.log('📩 rps avrg:', noEmptyStats.rpsAvrg)
48 | console.log('📩 response time avrg last:', lastResponseTimeAvrg)
49 | console.log('📩 response time avrg total:', noEmptyStats.responseTimeAvrg)
50 |
51 | delete noEmptyStats.times[time]
52 | }
53 | }, 1000)
54 |
55 | setInterval(() => {
56 | if (Object.keys(stats.times).length > 1) {
57 | const time = Object.keys(stats.times).shift()
58 |
59 | const rps = stats.times[time].length
60 | if (stats.rpsAvrg > 0) stats.rpsAvrg = (stats.rpsAvrg + rps) / 2
61 | else stats.rpsAvrg = rps
62 |
63 | const sumResponseTime = stats.times[time].reduce((a, b) => a + b, 0)
64 | const lastResponseTimeAvrg = (sumResponseTime / stats.times[time].length) || 0
65 | if (stats.responseTimeAvrg > 0) stats.responseTimeAvrg = (stats.responseTimeAvrg + lastResponseTimeAvrg) / 2
66 | else stats.responseTimeAvrg = lastResponseTimeAvrg
67 |
68 | console.log('🔄 rps last:', rps)
69 | console.log('🔄 rps avrg:', stats.rpsAvrg)
70 | console.log('🔄 response time avrg last:', lastResponseTimeAvrg)
71 | console.log('🔄 response time avrg total:', stats.responseTimeAvrg)
72 |
73 | rtOP.set(stats.responseTimeAvrg)
74 |
75 | db.Stats.create({
76 | rps,
77 | responseTime: lastResponseTimeAvrg,
78 | date: new Date()
79 | })
80 |
81 | delete stats.times[time]
82 | }
83 | }, 1000)
84 |
85 | setInterval(async () => {
86 | const usersCount = await db.User.count({
87 | updatedAt: {
88 | $gte: new Date(Date.now() - 24 * 60 * 60 * 1000)
89 | }
90 | })
91 |
92 | usersCountIO.set(usersCount)
93 | }, 60 * 1000)
94 |
95 | module.exports = async (ctx, next) => {
96 | const startMs = new Date()
97 |
98 | ctx.stats = {
99 | rps: stats.rpsAvrg,
100 | rta: stats.responseTimeAvrg
101 | }
102 |
103 | return next().then(() => {
104 | const now = Math.floor(new Date() / 1000)
105 |
106 | if (ctx.state.emptyRequest === false) {
107 | if (!noEmptyStats.times[now]) noEmptyStats.times[now] = []
108 | noEmptyStats.times[now].push(new Date() - startMs)
109 | }
110 |
111 | if (!stats.times[now]) stats.times[now] = []
112 | stats.times[now].push(new Date() - startMs)
113 | })
114 | }
115 |
--------------------------------------------------------------------------------
/locales/en.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | language_name: '🇺🇸 English'
3 | help: |
4 | Hey! 👋
5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel.
6 |
7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post.
8 |
9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian.
10 | • Do you like the bot? You can help him at this link: donate.lyo.su
11 |
12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian)
13 |
14 | source: github.com/LyoSU/reacombot
15 | menu:
16 | channels: '📣 My channels'
17 | channels:
18 | select: |
19 | Select channel:
20 | not_found: |
21 | 😞 You have not added the bot to any of the channels yet
22 |
23 | If you are sure that you did this, try to make sure that i have permission to edit messages and send any post to the channel.
24 | updated: |
25 | Channel information has been updated!
26 | control:
27 | info: |
28 | Channel selected: ${channel.title}
29 |
30 | • What do you want to change?
31 | menu:
32 | emoji: '${channel.settings.emojis} Emojis'
33 | type: '✍️ When to add?'
34 | comments_type: '💬 Comment button'
35 | links: '🔗 Links keyboard'
36 | no_available: |
37 | The channel is no longer available.
38 | types:
39 | info: |
40 | Choose when the bot can attach reactions to messages:
41 |
42 | • Always: Reactions will be attached to every post whenever possible
43 | • One time: Reactions will only be attached to one new post on the channel. Then this setting will change to "never" and you will need to change it again here
44 | • Never: Bot will never attach reactions to your posts
45 | • On request: Will add when the last line of the message starts with the ! and then there are emojis.
46 | menu:
47 | always: Always
48 | one: One time
49 | never: Never
50 | request: On request
51 | comments_types:
52 | info: |
53 | Choose where to display comments:
54 |
55 | • Always: The comment button will always be
56 | • One time: Only the next post will have a button. Then this setting will change to "never" and you will need to change it again here
57 | • Never: Don't show comment button
58 | menu:
59 | always: Always
60 | one: One time
61 | never: Never
62 | emojis:
63 | send_emoji: |
64 | Send one or more emojis:
65 |
66 | Current: ${channel.settings.emojis}
67 | One emoji — one button
68 | links:
69 | send_links: |
70 | Send keyboard list:
71 |
72 | Example:
73 |
My site - https://lyo.su/ | Telegram - https://telegram.org/ 74 | Dev blog - https://t.me/LyBlog75 | 76 | Send any text to delete. 77 | 78 | The current buttons will be displayed below 79 | success: Keyboard changed! 80 | back: < Back 81 | rate: 82 | vote: 83 | rated: | 84 | You ${rateName} this 85 | back: | 86 | You took your reaction back 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Counters in the post will be updated soon. 91 | wait: | 92 | 🕒 Comments will be posted here soon... 93 | 94 | If this did not happen for a long time, most likely the bot was not added to the attached group. 95 | error: 96 | cant_edited: | 97 | 😓 Failed to add reaction to post 98 | 99 | I may not have permission to edit posts. 100 | -------------------------------------------------------------------------------- /locales/tr.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language_name: '🇹🇷 Türkçe' 3 | help: | 4 | Hey! 👋 5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel. 6 | 7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post. 8 | 9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian. 10 | • Do you like the bot? You can help him at this link: donate.lyo.su 11 | 12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian) 13 | 14 | source: github.com/LyoSU/reacombot 15 | menu: 16 | channels: '📣 My channels' 17 | channels: 18 | select: | 19 | Select channel: 20 | not_found: | 21 | 😞 You have not added the bot to any of the channels yet 22 | 23 | If you are sure that you did this, try to make sure that i have permission to edit messages and send any post to the channel. 24 | updated: | 25 | Channel information has been updated! 26 | control: 27 | info: | 28 | Channel selected: ${channel.title} 29 | 30 | • What do you want to change? 31 | menu: 32 | emoji: '${channel.settings.emojis} Emojis' 33 | type: '✍️ When to add?' 34 | comments_type: '💬 Comment button' 35 | links: '🔗 Links keyboard' 36 | no_available: | 37 | The channel is no longer available. 38 | types: 39 | info: | 40 | Choose when the bot can attach reactions to messages: 41 | 42 | • Always: Reactions will be attached to every post whenever possible 43 | • One time: Reactions will only be attached to one new post on the channel. Then this setting will change to "never" and you will need to change it again here 44 | • Never: Bot will never attach reactions to your posts 45 | • On request: Will add when the last line of the message starts with the ! and then there are emojis. 46 | menu: 47 | always: Always 48 | one: One time 49 | never: Never 50 | request: On request 51 | comments_types: 52 | info: | 53 | Choose where to display comments: 54 | 55 | • Always: The comment button will always be 56 | • One time: Only the next post will have a button. Then this setting will change to "never" and you will need to change it again here 57 | • Never: Don't show comment button 58 | menu: 59 | always: Always 60 | one: One time 61 | never: Never 62 | emojis: 63 | send_emoji: | 64 | Send one or more emojis: 65 | 66 | Current:
${channel.settings.emojis}
67 | One emoji — one button
68 | links:
69 | send_links: |
70 | Send keyboard list:
71 |
72 | Example:
73 | My site - https://lyo.su/ | Telegram - https://telegram.org/ 74 | Dev blog - https://t.me/LyBlog75 | 76 | Send any text to delete. 77 | 78 | The current buttons will be displayed below 79 | success: Keyboard changed! 80 | back: < Back 81 | rate: 82 | vote: 83 | rated: | 84 | You ${rateName} this 85 | back: | 86 | You took your reaction back 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Counters in the post will be updated soon. 91 | wait: | 92 | 🕒 Comments will be posted here soon... 93 | 94 | If this did not happen for a long time, most likely the bot was not added to the attached group. 95 | error: 96 | cant_edited: | 97 | 😓 Failed to add reaction to post 98 | 99 | I may not have permission to edit posts. 100 | -------------------------------------------------------------------------------- /locales/id.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language_name: '🇮🇩 Indonesia' 3 | help: | 4 | Hey! 👋 5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel. 6 | 7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post. 8 | 9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian. 10 | • Do you like the bot? You can help him at this link: donate.lyo.su 11 | 12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian) 13 | 14 | source: github.com/LyoSU/reacombot 15 | menu: 16 | channels: '📣 My channels' 17 | channels: 18 | select: | 19 | Select channel: 20 | not_found: | 21 | 😞 You have not added the bot to any of the channels yet 22 | 23 | If you are sure that you did this, try to make sure that i have permission to edit messages and send any post to the channel. 24 | updated: | 25 | Channel information has been updated! 26 | control: 27 | info: | 28 | Channel selected: ${channel.title} 29 | 30 | • What do you want to change? 31 | menu: 32 | emoji: '${channel.settings.emojis} Emojis' 33 | type: '✍️ When to add?' 34 | comments_type: '💬 Comment button' 35 | links: '🔗 Links keyboard' 36 | no_available: | 37 | The channel is no longer available. 38 | types: 39 | info: | 40 | Choose when the bot can attach reactions to messages: 41 | 42 | • Always: Reactions will be attached to every post whenever possible 43 | • One time: Reactions will only be attached to one new post on the channel. Then this setting will change to "never" and you will need to change it again here 44 | • Never: Bot will never attach reactions to your posts 45 | • On request: Will add when the last line of the message starts with the ! and then there are emojis. 46 | menu: 47 | always: Always 48 | one: One time 49 | never: Never 50 | request: On request 51 | comments_types: 52 | info: | 53 | Choose where to display comments: 54 | 55 | • Always: The comment button will always be 56 | • One time: Only the next post will have a button. Then this setting will change to "never" and you will need to change it again here 57 | • Never: Don't show comment button 58 | menu: 59 | always: Always 60 | one: One time 61 | never: Never 62 | emojis: 63 | send_emoji: | 64 | Send one or more emojis: 65 | 66 | Current:
${channel.settings.emojis}
67 | One emoji — one button
68 | links:
69 | send_links: |
70 | Send keyboard list:
71 |
72 | Example:
73 | My site - https://lyo.su/ | Telegram - https://telegram.org/ 74 | Dev blog - https://t.me/LyBlog75 | 76 | Send any text to delete. 77 | 78 | The current buttons will be displayed below 79 | success: Keyboard changed! 80 | back: < Back 81 | rate: 82 | vote: 83 | rated: | 84 | You ${rateName} this 85 | back: | 86 | You took your reaction back 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Counters in the post will be updated soon. 91 | wait: | 92 | 🕒 Comments will be posted here soon... 93 | 94 | If this did not happen for a long time, most likely the bot was not added to the attached group. 95 | error: 96 | cant_edited: | 97 | 😓 Failed to add reaction to post 98 | 99 | I may not have permission to edit posts. 100 | -------------------------------------------------------------------------------- /locales/az.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language_name: '🇦🇿 Azərbaycanca' 3 | help: | 4 | Hey! 👋 5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel. 6 | 7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post. 8 | 9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian. 10 | • Do you like the bot? You can help him at this link: donate.lyo.su 11 | 12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian) 13 | 14 | source: github.com/LyoSU/reacombot 15 | menu: 16 | channels: '📣 Kanallarım' 17 | channels: 18 | select: | 19 | Kanalı seçin: 20 | not_found: | 21 | 😞 Botu hələ ki heç bir kanala əlavə etməmisiniz 22 | 23 | Bunu etdiyinizə əminsinizsə, mesajları "edit" etmək və kanala hər hansı bir yazı göndərmək üçün icazəmin olmasına əmin olun. 24 | updated: | 25 | Kanal məlumatları yeniləndi! 26 | control: 27 | info: | 28 | Kanal seçildi: ${channel.title} 29 | 30 | • Nəyi dəyişdirmək istəyirsiniz? 31 | menu: 32 | emoji: '${channel.settings.emojis} emoji' 33 | type: '✍️ Nə vaxt əlavə etməli?' 34 | comments_type: '💬 Şərh düyməsi' 35 | links: '🔗 İstinad klaviaturası' 36 | no_available: | 37 | Kanal artıq əlçatan deyil. 38 | types: 39 | info: | 40 | Botun mesajlara reaksiyalarını nə vaxt əlavə edə biləcəyini seçin: 41 | 42 | • Həmişə: Reaksiyalar mümkün olduqda hər yazıya əlavə olunacaq 43 | • Bir dəfə: Reaksiyalar yalnız kanaldakı yeni bir yazıya əlavə olunacaq. Sonra bu ayar "heç vaxt" olaraq dəyişəcək və yenidən burada dəyişdirməlisiniz 44 | • Heç vaxt: Bot heç vaxt yazılarınıza reaksiyalar əlavə etməyəcək. 45 | • İstəyə görə: Mesajın son sətri "ilə" başladıqda əlavə edəcək! və sonra emojilər var. 46 | menu: 47 | always: Həmişə 48 | one: Bir dəfəlik 49 | never: Heç vaxt 50 | request: Xahişi ilə 51 | comments_types: 52 | info: | 53 | Şərhlərin göstəriləcəyi yeri seçin: 54 | 55 | • Həmişə: Şərh düyməsi hər zaman 56 | olacaq • Bir dəfə: Yalnız növbəti yazıda bir düymə olacaq. Sonra bu parametr "heç vaxt" olaraq dəyişəcək və burada yenidən dəyişdirməlisiniz 57 | • Heç vaxt: şərh düyməsini göstərməyin 58 | menu: 59 | always: Həmişə 60 | one: Bir dəfəlik 61 | never: Heç vaxt 62 | emojis: 63 | send_emoji: | 64 | Bir və ya daha çox emoji göndərin: 65 | 66 | Cari:
${channel.settings.emojis}
67 | Bir emoji - bir düymə
68 | links:
69 | send_links: |
70 | Klaviatura siyahısını göndərin:
71 |
72 | Nümunə:
73 | Saytım - https://lyo.su/ | Telegram - https://telegram.org/ 74 | Dev blog - https://t.me/LyBlog75 | 76 | Silmək üçün istənilən mətni göndərin. 77 | 78 | Hazırkı düymələr aşağıda ğöstəriləcək 79 | success: Klaviatura dəyişdirildi! 80 | back: < Geri 81 | rate: 82 | vote: 83 | rated: | 84 | Sən bunu ${rateName} 85 | back: | 86 | Reaksiyanı gaytardın 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Yazıdakı sayğaclar tezliklə yenilənəcək. 91 | wait: | 92 | 🕒 Şərhlər tezliklə burada yerləşdiriləcək... 93 | 94 | Bu uzun müddət baş verməsəydi, böyük ehtimalla bot əlavə olunmuş qrupa əlavə edilmədi. 95 | error: 96 | cant_edited: | 97 | 😓 Son yazıya reaksiya əlavə etmək alınmadı 98 | 99 | Yazıları redaktə etmək üçün icazəm olmaya bilər. 100 | -------------------------------------------------------------------------------- /locales/pt.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language_name: '🇧🇷 Português' 3 | help: | 4 | Hey! 👋 5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel. 6 | 7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post. 8 | 9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian. 10 | • Do you like the bot? You can help him at this link: donate.lyo.su 11 | 12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian) 13 | 14 | source: github.com/LyoSU/reacombot 15 | menu: 16 | channels: '📣 Meus canais' 17 | channels: 18 | select: | 19 | Escolha um canal: 20 | not_found: | 21 | 😞 Você ainda não adicionou o bot a nenhum dos canais 22 | 23 | Se tem certeza de que fez isso, verifique se eu tenho permissão para editar mensagens e enviar qualquer postagem para o canal. 24 | updated: | 25 | Informação do canal foi atualizada! 26 | control: 27 | info: | 28 | Canal selecionado: ${channel.title} 29 | 30 | • O que deseja alterar? 31 | menu: 32 | emoji: '${channel.settings.emojis} Emojis' 33 | type: '✍️ Quando adicionar?' 34 | comments_type: '💬 Botão de comentários' 35 | links: '🔗 Botões de links' 36 | no_available: | 37 | O canal não está mais disponível. 38 | types: 39 | info: | 40 | Escolha quando o bot irá anexar reações às mensagens: 41 | 42 | • Sempre: Reações serão anexadas a cada postagem sempre que possível 43 | • Uma vez: Reações só serão anexadas a uma nova postagem no canal. Depois disso, esta configuração mudará para "nunca" e você precisará alterá-la novamente aqui. 44 | • Nunca: O bot nunca anexará reações às suas postagens. 45 | • A pedido: Será adicionado quando a última linha da mensagem começar com o ! e, depois, existem emojis. 46 | menu: 47 | always: Sempre 48 | one: Uma vez 49 | never: Nunca 50 | request: A pedido 51 | comments_types: 52 | info: | 53 | Escolha onde exibir comentários: 54 | 55 | • Sempre: O botão de comentário sempre será 56 | • Uma vez: Somente a próxima postagem terá um botão. Então esta configuração vai mudar para "nunca" e você precisará mudá-la novamente aqui 57 | • Nunca: não mostrar o botão de comentário 58 | menu: 59 | always: Sempre 60 | one: Uma vez 61 | never: Nunca 62 | emojis: 63 | send_emoji: | 64 | Envie um ou mais emojis: 65 | 66 | Atual:
${channel.settings.emojis}
67 | Um emoji = um botão
68 | links:
69 | send_links: |
70 | Enviar lista de botões:
71 |
72 | Exemplo:
73 | Meu site - https://lyo.su/ | Telegram - https://telegram. rg/ 74 | Blog do desenvolvedor - https://t.me/LyBlog75 | 76 | Envie qualquer texto para excluir. 77 | 78 | Os botões atuais serão exibidos abaixo 79 | success: Botões alterados! 80 | back: < Voltar 81 | rate: 82 | vote: 83 | rated: | 84 | Você ${rateName} isso 85 | back: | 86 | Você tirou sua reação 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Os contadores na postagem serão atualizados em breve. 91 | wait: | 92 | 🕒 Os comentários serão postados aqui em breve... 93 | 94 | Se demorar muito, provavelmente o bot não foi adicionado ao grupo vinculado. 95 | error: 96 | cant_edited: | 97 | 😓 Falha ao adicionar reações à última postagem 98 | 99 | Talvez eu não tenha permissão para editar as postagens. 100 | -------------------------------------------------------------------------------- /handlers/post.js: -------------------------------------------------------------------------------- 1 | const Composer = require('telegraf/composer') 2 | const { 3 | keyboardUpdate, 4 | findEmojis 5 | } = require('../helpers') 6 | 7 | const composer = new Composer() 8 | 9 | composer.on('channel_post', async (ctx, next) => { 10 | if (ctx.session.channelInfo.settings.type === 'never') return next() 11 | if (ctx.session.channelInfo.settings.type === 'one') ctx.session.channelInfo.settings.type = 'never' 12 | 13 | const post = new ctx.db.Post() 14 | 15 | const votesRateArray = [] 16 | 17 | let emojis, newText 18 | let messageType = 'keyboard' 19 | 20 | if (ctx.channelPost.text || ctx.channelPost.caption) { 21 | const text = ctx.channelPost.text || ctx.channelPost.caption 22 | const textLines = text.split('\n') 23 | const lastLine = textLines.pop() 24 | 25 | if (lastLine[0] === '!') { 26 | const emojisFromLine = findEmojis(lastLine) 27 | if (emojisFromLine.length > 0) emojis = emojisFromLine 28 | 29 | newText = textLines.join('\n') 30 | 31 | if (ctx.channelPost.text) messageType = 'text' 32 | else messageType = 'media' 33 | } 34 | } 35 | 36 | if (!emojis && ctx.session.channelInfo.settings.type === 'request') return next() 37 | if (!emojis) emojis = findEmojis(ctx.session.channelInfo.settings.emojis) 38 | 39 | emojis.forEach(emoji => { 40 | votesRateArray.push({ 41 | name: emoji, 42 | vote: [] 43 | }) 44 | }) 45 | 46 | post.channel = ctx.session.channelInfo 47 | post.channelMessageId = ctx.channelPost.message_id 48 | post.rate = { 49 | votes: votesRateArray, 50 | score: 0 51 | } 52 | post.keyboard = post.channel.settings.keyboard 53 | 54 | if (ctx.session.channelInfo.settings.commentsType === 'never') post.commentsEnable = false 55 | if (ctx.session.channelInfo.settings.commentsType === 'one') ctx.session.channelInfo.settings.commentsType = 'never' 56 | 57 | await post.save() 58 | 59 | const updateResult = await keyboardUpdate(post.channel.channelId, post.channelMessageId, { 60 | type: messageType, 61 | text: newText, 62 | entities: ctx.channelPost.entities || ctx.channelPost.caption_entities 63 | }) 64 | 65 | if (updateResult.error && updateResult.error.code === 400 && !ctx.channelPost.forward_from_message_id) { 66 | const botMember = await ctx.tg.getChatMember(post.channel.channelId, ctx.botInfo.id) 67 | if (botMember.can_be_edited === false) { 68 | for (const admin of post.channel.administrators) { 69 | const adminUser = await ctx.db.User.findOne({ telegramId: admin.user }) 70 | 71 | if (adminUser) { 72 | ctx.i18n.locale(adminUser.settings.locale) 73 | await ctx.tg.sendMessage(admin.user, ctx.i18n.t('error.cant_edited', { 74 | postLink: `https://t.me/c/${ctx.chat.id.toString().substr(4)}/${ctx.channelPost.message_id}` 75 | }), { 76 | parse_mode: 'HTML' 77 | }).catch(() => {}) 78 | } 79 | } 80 | } 81 | } 82 | }) 83 | 84 | composer.action('post:wait', async (ctx, next) => { 85 | ctx.state.answerCbQuery = [ctx.i18n.t('wait'), true] 86 | }) 87 | 88 | composer.on('message', async (ctx, next) => { 89 | if (ctx.from.id === 777000 && ctx.message.forward_from_message_id && ctx.message.forward_from_chat.id === ctx.message.sender_chat.id) { 90 | const channel = await ctx.db.Channel.findOne({ channelId: ctx.message.forward_from_chat.id }) 91 | const post = await ctx.db.Post.findOne({ channel, channelMessageId: ctx.message.forward_from_message_id }) 92 | 93 | if (!post || post.commentsEnable === false) return next() 94 | 95 | post.groupMessageId = ctx.message.message_id 96 | await post.save() 97 | 98 | if (channel.groupId !== ctx.message.chat.id) { 99 | await ctx.db.Channel.findByIdAndUpdate(channel, { 100 | groupId: ctx.message.chat.id 101 | }) 102 | } 103 | 104 | await keyboardUpdate(channel.channelId, post.channelMessageId) 105 | } else { 106 | return next() 107 | } 108 | }) 109 | 110 | module.exports = composer 111 | -------------------------------------------------------------------------------- /locales/uk.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language_name: '🇺🇦 Українська' 3 | help: | 4 | Hey! 👋 5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel. 6 | 7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post. 8 | 9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian. 10 | • Do you like the bot? You can help him at this link: donate.lyo.su 11 | 12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian) 13 | 14 | source: github.com/LyoSU/reacombot 15 | menu: 16 | channels: '📣 Мої канали' 17 | channels: 18 | select: | 19 | Оберіть канал: 20 | not_found: | 21 | 😞 Ви не додали бота до будь-якого з каналів 22 | 23 | Якщо ви впевнені, що ви це зробили, спробуйте переконатися, що я маю дозвіл на редагування повідомлень і відправляти будь-які повідомлення на канал. 24 | updated: | 25 | Інформація про канал оновлена! 26 | control: 27 | info: | 28 | Канал обрано: ${channel.title} 29 | 30 | • Що ви хочете змінити? 31 | menu: 32 | emoji: '${channel.settings.emojis} Emojis' 33 | type: '✍️ Коли додавати?' 34 | comments_type: '💬 Кнопка коментарів' 35 | links: '🔗 Клавіатура' 36 | no_available: | 37 | Канал більше не доступний. 38 | types: 39 | info: | 40 | Виберіть, коли бот може додавати реакції до повідомлень: 41 | 42 | • Завжди: реакції будуть додаватися до кожного повідомлення, коли це можливо 43 | • Одноразово: Реакції додаватимуться лише до одного нового повідомлення на каналі. Потім це налаштування зміниться на "ніколи", і вам потрібно буде змінити його тут ще раз 44 | • Ніколи: Бот ніколи не додаватиме реакцій на ваші повідомлення. 45 | • За запитом: Буде додано, коли останній рядок повідомлення починається з. символа ! а потім є смайли. 46 | menu: 47 | always: Завжди 48 | one: Одноразово 49 | never: Ніколи 50 | request: За запитом 51 | comments_types: 52 | info: | 53 | Виберіть, де відображати коментарі: 54 | 55 | • Завжди: кнопка коментаря завжди буде 56 | • Одноразово: лише наступна публікація матиме кнопку. Тоді це налаштування зміниться на "ніколи", і вам доведеться змінити його знову тут 57 | • Ніколи: не показувати кнопку коментаря 58 | menu: 59 | always: Завжди 60 | one: Одноразово 61 | never: Ніколи 62 | emojis: 63 | send_emoji: | 64 | Надішліть один або кілька смайлів: 65 | 66 | Поточні:
${channel.settings.emojis}
67 | Один смайлик - одна кнопка
68 | links:
69 | send_links: |
70 | Надіслати список клавіатури:
71 |
72 | Приклад:
73 | Мій сайт - https://lyo.su/ | Telegram - https://telegram.org/ 74 | Dev blog - https://t.me/LyBlog75 | 76 | Надішліть будь-який текст для видалення. 77 | 78 | Поточні кнопки відображатимуться нижче 79 | success: Клавіатуру змінено! 80 | back: < Назад 81 | rate: 82 | vote: 83 | rated: | 84 | Ви ${rateName} це 85 | back: | 86 | Ви повернули свою реакцію назад 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Лічильники в повідомленні невдовзі будуть змінені. 91 | wait: | 92 | 🕒 Коментарі будуть розміщені найближчим часом... 93 | 94 | Якщо цього не відбувається довгий час, швидше за все, бота не додано до приєднаної групи. 95 | error: 96 | cant_edited: | 97 | 😓 Помилка при додаванні реакції на повідомлення 98 | 99 | Можливо у мене немає дозволу на редагування повідомлень. 100 | -------------------------------------------------------------------------------- /locales/ru.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language_name: '🇷🇺 Русский' 3 | help: | 4 | Hey! 👋 5 | I will add reactions to your posts on the channel along with native comments. It's enough just to add me to your channel, give me permission to edit messages, and also add me to the group that is tied to the channel. 6 | 7 | • If you do not want me to change all messages or want to change emoji, then click the buttons below or send the command /channels to configure. To display a channel in the list, you must first write any post. 8 | 9 | • Need help or have a suggestion? Write to me: @Ly_OBot. Please write immediately what you are talking about and use English or Russian. 10 | • Do you like the bot? You can help him at this link: donate.lyo.su 11 | 12 | 🧑💻 Follow the developer: @LyBlog (Only in Russian) 13 | 14 | source: github.com/LyoSU/reacombot 15 | menu: 16 | channels: '📣 Мои каналы' 17 | channels: 18 | select: | 19 | Выберите канал: 20 | not_found: | 21 | 😞 Вы не добавили бота ни к одному из каналов 22 | 23 | Если вы уверены, что сделали это, попробуйте убедиться, что у меня есть разрешение на редактирование сообщений и отправку любых сообщений в канал. 24 | updated: | 25 | Информация о канале была обновлена! 26 | control: 27 | info: | 28 | Выбран канал: ${channel.title} 29 | 30 | • Что вы хотите изменить? 31 | menu: 32 | emoji: '${channel.settings.emojis} Эмодзи' 33 | type: '✍️ Когда добавить?' 34 | comments_type: '💬 Кнопка комментариев' 35 | links: '🔗 Кнопки с ссылками' 36 | no_available: | 37 | Канал больше не доступен. 38 | types: 39 | info: | 40 | Выберите, когда бот может прикрепить реакцию к сообщениям: 41 | 42 | • Всегда: реакции будут присоединены к каждому сообщению, когда это возможно, 43 | • Однократно: реакции будут прикреплены только к одному новому сообщению на канале. Затем этот параметр изменится на «никогда» и вам нужно будет изменить его снова здесь 44 | • Никогда: бот никогда не будет прикреплять реакции к вашим записям 45 | • On request: Will add when the last line of the message starts with the ! and then there are emojis. 46 | menu: 47 | always: Всегда 48 | one: Однократно 49 | never: Никогда 50 | request: По запросу 51 | comments_types: 52 | info: | 53 | Выберите, где отображать комментарии: 54 | 55 | • Всегда: Кнопка комментариев всегда будет 56 | • Однократно: Только следующий пост будет иметь кнопку. Затем этот параметр изменится на «никогда» и вам нужно будет изменить его снова здесь 57 | • Никогда: Не показывать кнопку комментариев 58 | menu: 59 | always: Всегда 60 | one: Однократно 61 | never: Никогда 62 | emojis: 63 | send_emoji: | 64 | Отправить один или более эмодзи: 65 | 66 | Текущий:
${channel.settings.emojis}
67 | Один эмодзи — одна кнопка
68 | links:
69 | send_links: |
70 | Отправить список клавиатуры:
71 |
72 | Пример:
73 | Мой сайт - https://lyo.su/ | Telegram - https://telegram.org/ 74 | Dev блог - https://t.me/LyBlog75 | 76 | Отправить любой текст для удаления. 77 | 78 | Текущие кнопки будут отображаться ниже 79 | success: Клавиатура изменена! 80 | back: < Назад 81 | rate: 82 | vote: 83 | rated: | 84 | Вы ${rateName} это 85 | back: | 86 | Вы вернули свою реакцию 87 | rated_limit: | 88 | ${reactList} 89 | 90 | Счетчики в сообщении скоро будут обновлены. 91 | wait: | 92 | 🕒 Комментарии будут размещены здесь в ближайшее время... 93 | 94 | Если это не произошло долгое время, скорее всего бот не был добавлен в прикрепленную группу. 95 | error: 96 | cant_edited: | 97 | 😓 Не удалось добавить реакцию на пост 98 | 99 | Возможно, у меня нет разрешения на редактирование сообщений. 100 | -------------------------------------------------------------------------------- /helpers/update-keyboard.js: -------------------------------------------------------------------------------- 1 | const { 2 | db 3 | } = require('../database') 4 | const Telegram = require('telegraf/telegram') 5 | 6 | const telegram = new Telegram(process.env.BOT_TOKEN) 7 | 8 | function sleep (ms) { 9 | return new Promise((resolve) => { 10 | setTimeout(resolve, ms) 11 | }) 12 | } 13 | 14 | function raceAll (promises, timeout) { 15 | return Promise.all(promises.map(p => { 16 | return Promise.race([p, sleep(timeout)]) 17 | })) 18 | } 19 | 20 | const keyboardUpdate = async (channelId, channelMessageId, message) => { 21 | const channel = await db.Channel.findOne({ channelId }) 22 | const post = await db.Post.findOne({ channel, channelMessageId }) 23 | 24 | const votesKeyboardArray = [] 25 | 26 | post.rate.votes.forEach(react => { 27 | votesKeyboardArray.push({ 28 | text: `${react.name} ${react.vote.length > 0 ? react.vote.length : ''}`, 29 | callback_data: `rate:${react.name}` 30 | }) 31 | }) 32 | 33 | if (post.commentsEnable === true) { 34 | if (post.groupMessageId) { 35 | votesKeyboardArray.push({ 36 | text: `💬 ${post.commentsCount > 0 ? post.commentsCount : ''}`, 37 | url: `https://t.me/c/${channel.groupId.toString().substr(4)}/${channel.settings.showStart === 'top' ? 2 : post.groupMessageId + 1000000}?thread=${post.groupMessageId}` 38 | }) 39 | } else { 40 | if (new Date().getTime() > new Date(post.createdAt.getTime() + (1000 * 30)).getTime()) { 41 | post.commentsEnable = false 42 | } else { 43 | votesKeyboardArray.push({ 44 | text: '💬 🕒', 45 | callback_data: 'post:wait' 46 | }) 47 | } 48 | } 49 | } 50 | 51 | let methodUpdate = 'editMessageReplyMarkup' 52 | const optsUpdate = { 53 | chat_id: channelId, 54 | message_id: channelMessageId, 55 | reply_markup: JSON.stringify({ inline_keyboard: [votesKeyboardArray].concat(post.keyboard) }) 56 | } 57 | 58 | if (message) { 59 | if (message.type === 'text') { 60 | methodUpdate = 'editMessageText' 61 | optsUpdate.text = message.text 62 | optsUpdate.entities = message.entities 63 | } 64 | if (message.type === 'media') { 65 | methodUpdate = 'editMessageCaption' 66 | optsUpdate.caption = message.text 67 | optsUpdate.caption_entities = message.entities 68 | } 69 | } 70 | 71 | if (!post.keyboardNextUpdate || new Date().getTime() > post.keyboardNextUpdate.getTime()) { 72 | return Promise.race([ 73 | telegram.callApi(methodUpdate, optsUpdate).then(() => { 74 | db.Post.findByIdAndUpdate(post, { 75 | keyboardNextUpdate: new Date(new Date().getTime() + 200) 76 | }).then(() => {}) 77 | return { edited: true } 78 | }).catch(error => { 79 | if (error.parameters && error.parameters.retry_after) { 80 | db.Post.findByIdAndUpdate(post, { 81 | keyboardNextUpdate: new Date(new Date().getTime() + (1000 * error.parameters.retry_after)) 82 | }).then(() => {}) 83 | } 84 | if (error.code === 400) { 85 | db.Post.findByIdAndUpdate(post, { 86 | keyboardNextUpdate: null 87 | }).then(() => {}) 88 | } 89 | return { edited: false, error } 90 | }), 91 | sleep(350).then(() => { 92 | return { edited: false, error: 'timeout' } 93 | }) 94 | ]) 95 | } else { 96 | return { edited: false, error: 'wait' } 97 | } 98 | } 99 | 100 | async function checkPostForUpdate () { 101 | const findPost = await db.Post.find({ 102 | keyboardNextUpdate: { 103 | $lt: new Date() 104 | } 105 | }).populate('channel') 106 | const promises = [] 107 | for (const post of findPost) { 108 | promises.push(keyboardUpdate(post.channel.channelId, post.channelMessageId)) 109 | } 110 | if (promises.length > 0) { 111 | const result = await raceAll(promises, 5 * 1000).catch(error => { 112 | console.error('update post error:', error) 113 | }) 114 | console.log('result keyboard update:', result) 115 | } 116 | setTimeout(checkPostForUpdate, 1000) 117 | } 118 | setTimeout(checkPostForUpdate, 1000) 119 | 120 | setInterval(() => { 121 | db.Post.updateMany( 122 | { keyboardNextUpdate: { $ne: null } }, 123 | { $set: { keyboardNextUpdate: null } } 124 | ).then(result => { 125 | console.log('keyboard update stopped', result) 126 | }) 127 | }, 1000 * 60 * 60) 128 | 129 | module.exports = keyboardUpdate 130 | -------------------------------------------------------------------------------- /handlers/channels.js: -------------------------------------------------------------------------------- 1 | const Composer = require('telegraf/composer') 2 | const Scene = require('telegraf/scenes/base') 3 | const Markup = require('telegraf/markup') 4 | const { match } = require('telegraf-i18n') 5 | const { 6 | scenes 7 | } = require('../middlewares') 8 | const { 9 | getChannel, 10 | findEmojis 11 | } = require('../helpers') 12 | 13 | const composer = new Composer() 14 | 15 | const channelControl = new Scene('channelControl') 16 | 17 | channelControl.enter(async ctx => { 18 | const channel = await ctx.db.Channel.findById(ctx.scene.state.channelId) 19 | const channelChat = await ctx.tg.getChat(channel.channelId).catch(error => { return { error } }) 20 | 21 | if (channelChat.error) { 22 | channel.available = false 23 | await channel.save() 24 | return ctx.replyWithHTML(ctx.i18n.t('channels.control.no_available')) 25 | } 26 | 27 | const inlineKeyboard = [ 28 | [ 29 | Markup.callbackButton(ctx.i18n.t('channels.control.menu.emoji', { channel }), `channel:${channel.id}:emoji`), 30 | Markup.callbackButton(ctx.i18n.t('channels.control.menu.links', { channel }), `channel:${channel.id}:links`) 31 | ], 32 | [ 33 | Markup.callbackButton(ctx.i18n.t('channels.control.menu.type'), `channel:${channel.id}:type:0`), 34 | Markup.callbackButton(ctx.i18n.t('channels.control.menu.comments_type'), `channel:${channel.id}:comments_type:0`) 35 | ] 36 | ] 37 | 38 | if (ctx.updateType === 'message') { 39 | await ctx.replyWithHTML(ctx.i18n.t('channels.control.info', { 40 | channel 41 | }), { 42 | reply_markup: Markup.inlineKeyboard(inlineKeyboard) 43 | }) 44 | } else if (ctx.updateType === 'callback_query') { 45 | await ctx.editMessageText(ctx.i18n.t('channels.control.info', { 46 | channel 47 | }), { 48 | parse_mode: 'HTML', 49 | disable_web_page_preview: true, 50 | reply_markup: Markup.inlineKeyboard(inlineKeyboard) 51 | }) 52 | } 53 | }) 54 | 55 | const setChannelEmoji = new Scene('setChannelEmoji') 56 | 57 | setChannelEmoji.enter(async ctx => { 58 | const channel = await ctx.db.Channel.findById(ctx.scene.state.channelId) 59 | 60 | await ctx.editMessageText(ctx.i18n.t('channels.control.emojis.send_emoji', { 61 | channel 62 | }), { 63 | parse_mode: 'HTML', 64 | disable_web_page_preview: true 65 | }) 66 | }) 67 | 68 | setChannelEmoji.on('text', async ctx => { 69 | const channel = await ctx.db.Channel.findById(ctx.scene.state.channelId) 70 | 71 | const emojis = findEmojis(ctx.message.text) 72 | channel.settings.emojis = emojis.join('') 73 | if (channel.settings.emojis.length <= 0) channel.settings.emojis = new ctx.db.Channel().settings.emojis 74 | await channel.save() 75 | 76 | return ctx.scene.enter(channelControl.id, { 77 | channelId: ctx.scene.state.channelId 78 | }) 79 | }) 80 | 81 | const setChannelLinks = new Scene('setChannelLinks') 82 | 83 | setChannelLinks.enter(async ctx => { 84 | const channel = await ctx.db.Channel.findById(ctx.scene.state.channelId) 85 | 86 | await ctx.editMessageText(ctx.i18n.t('channels.control.links.send_links'), { 87 | reply_markup: Markup.inlineKeyboard(channel.settings.keyboard), 88 | parse_mode: 'HTML', 89 | disable_web_page_preview: true 90 | }) 91 | }) 92 | 93 | setChannelLinks.on('text', async ctx => { 94 | const channel = await ctx.db.Channel.findById(ctx.scene.state.channelId) 95 | 96 | const inlineKeyboard = [] 97 | 98 | ctx.message.text.split('\n').forEach((line) => { 99 | const linelButton = [] 100 | 101 | line.split('|').forEach((row) => { 102 | const data = row.split(' - ') 103 | if (data[0] && data[1]) { 104 | const name = data[0].trim() 105 | const url = data[1].trim() 106 | 107 | linelButton.push(Markup.urlButton(name, url)) 108 | } 109 | }) 110 | 111 | inlineKeyboard.push(linelButton) 112 | }) 113 | 114 | channel.settings.keyboard = inlineKeyboard 115 | await channel.save() 116 | 117 | await ctx.replyWithHTML(ctx.i18n.t('channels.control.links.success')) 118 | 119 | return ctx.scene.enter(channelControl.id, { 120 | channelId: ctx.scene.state.channelId 121 | }) 122 | }) 123 | 124 | composer.use(scenes( 125 | channelControl, 126 | setChannelEmoji, 127 | setChannelLinks 128 | )) 129 | 130 | composer.action(/channel:(.*):emoji/, async ctx => { 131 | return ctx.scene.enter(setChannelEmoji.id, { 132 | channelId: ctx.match[1] 133 | }) 134 | }) 135 | 136 | composer.action(/channel:(.*):links/, async ctx => { 137 | return ctx.scene.enter(setChannelLinks.id, { 138 | channelId: ctx.match[1] 139 | }) 140 | }) 141 | 142 | composer.action(/channel:(.*):type:(.*)/, async ctx => { 143 | const channel = await ctx.db.Channel.findById(ctx.match[1]) 144 | 145 | const types = ['always', 'one', 'never', 'request'] 146 | 147 | if (types.indexOf(ctx.match[2]) >= 0) channel.settings.type = ctx.match[2] 148 | await channel.save() 149 | 150 | const inlineKeyboard = [] 151 | 152 | inlineKeyboard.push(types.map(type => { 153 | const selectedMark = channel.settings.type === type ? '✅ ' : '' 154 | return Markup.callbackButton(selectedMark + ctx.i18n.t(`channels.control.types.menu.${type}`), `channel:${channel.id}:type:${type}`) 155 | })) 156 | 157 | inlineKeyboard.push([Markup.callbackButton(ctx.i18n.t('channels.back'), `channel:${channel.id}`)]) 158 | 159 | await ctx.editMessageText(ctx.i18n.t('channels.control.types.info', { 160 | channel 161 | }), { 162 | parse_mode: 'HTML', 163 | disable_web_page_preview: true, 164 | reply_markup: Markup.inlineKeyboard(inlineKeyboard) 165 | }) 166 | }) 167 | 168 | composer.action(/channel:(.*):comments_type:(.*)/, async ctx => { 169 | const channel = await ctx.db.Channel.findById(ctx.match[1]) 170 | 171 | const types = ['always', 'one', 'never'] 172 | 173 | if (types.indexOf(ctx.match[2]) >= 0) channel.settings.commentsType = ctx.match[2] 174 | await channel.save() 175 | 176 | const inlineKeyboard = [] 177 | 178 | inlineKeyboard.push(types.map(type => { 179 | const selectedMark = channel.settings.commentsType === type ? '✅ ' : '' 180 | return Markup.callbackButton(selectedMark + ctx.i18n.t(`channels.control.comments_types.menu.${type}`), `channel:${channel.id}:comments_type:${type}`) 181 | })) 182 | 183 | inlineKeyboard.push([Markup.callbackButton(ctx.i18n.t('channels.back'), `channel:${channel.id}`)]) 184 | 185 | await ctx.editMessageText(ctx.i18n.t('channels.control.comments_types.info', { 186 | channel 187 | }), { 188 | parse_mode: 'HTML', 189 | disable_web_page_preview: true, 190 | reply_markup: Markup.inlineKeyboard(inlineKeyboard) 191 | }) 192 | }) 193 | 194 | composer.action(/channel:(.*)/, async ctx => { 195 | return ctx.scene.enter(channelControl.id, { 196 | channelId: ctx.match[1] 197 | }) 198 | }) 199 | 200 | const channels = async ctx => { 201 | const channels = await ctx.db.Channel.find({ 202 | 'administrators.user': ctx.from.id, 203 | available: { $ne: 'false' } 204 | }) 205 | 206 | if (channels.length <= 0) return ctx.replyWithHTML(ctx.i18n.t('channels.not_found')) 207 | 208 | const inlineKeyboard = [] 209 | 210 | channels.forEach(channel => { 211 | inlineKeyboard.push(Markup.callbackButton(channel.title, `channel:${channel.id}`)) 212 | }) 213 | 214 | await ctx.replyWithHTML(ctx.i18n.t('channels.select'), { 215 | reply_markup: Markup.inlineKeyboard(inlineKeyboard, { 216 | columns: 2 217 | }) 218 | }) 219 | } 220 | 221 | composer.hears(match('menu.channels'), Composer.privateChat(channels)) 222 | composer.command('channels', Composer.privateChat(channels)) 223 | 224 | composer.on('forward', Composer.privateChat(async (ctx, next) => { 225 | if (ctx.message.forward_from_chat.type !== 'channel') return next() 226 | await getChannel(ctx.message.forward_from_chat) 227 | await ctx.replyWithHTML(ctx.i18n.t('channels.updated')) 228 | })) 229 | 230 | module.exports = composer 231 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc.