├── .gitignore ├── .babelrc ├── src ├── models │ ├── entities │ │ ├── pollOption.js │ │ ├── photoSize.js │ │ ├── maskPosition.js │ │ ├── messageEntity.js │ │ ├── orderInfo.js │ │ ├── shippingAddress.js │ │ ├── chatPhoto.js │ │ ├── user.js │ │ ├── statBotUser.js │ │ ├── chat.js │ │ └── message.js │ ├── messageTypes │ │ ├── location.js │ │ ├── voice.js │ │ ├── contact.js │ │ ├── invoice.js │ │ ├── venue.js │ │ ├── document.js │ │ ├── videoNote.js │ │ ├── poll.js │ │ ├── video.js │ │ ├── audio.js │ │ ├── animation.js │ │ ├── sticker.js │ │ ├── successfulPayment.js │ │ └── game.js │ └── schemas │ │ ├── chat.schema.js │ │ ├── message.schema.js │ │ └── statBotUser.schema.js ├── databaseAccess │ ├── postMessage.db.js │ ├── getConsentLevel.db.js │ ├── upsertChat.db.js │ ├── getAllTexts.db.js │ ├── upsertUser.db.js │ ├── getMessagesByWeekday.db.js │ ├── getMessagesByUser.db.js │ ├── getMessageTotal.db.js │ └── getMessagesByHour.db.js ├── bot │ ├── messages │ │ ├── messageCollection.js │ │ ├── documentMessage.js │ │ └── textMessage.js │ ├── commands │ │ ├── wordCount.js │ │ ├── totalMessages.js │ │ ├── chatInfo.js │ │ ├── consent.js │ │ ├── totalMessagesExtended.js │ │ ├── myData.js │ │ ├── messagesPerUser.js │ │ └── messagesPerWeekday.js │ ├── translate.js │ ├── handlers │ │ ├── onMessage.js │ │ └── onNewChatMembers.js │ ├── statBot.js │ └── translations.json ├── routes │ ├── users │ │ ├── getUser.route.js │ │ ├── getPersonalData.route.js │ │ └── upsertUserRoute.js │ ├── messages │ │ ├── getWordCount.route.js │ │ ├── getMessagesByHour.route.js │ │ ├── getMessagesByWeekday.route.js │ │ ├── postMessage.route.js │ │ ├── getMessageTotal.route.js │ │ └── getMessagesByUser.route.js │ ├── meta │ │ └── getMembershipEventsRoute.js │ └── index.route.js ├── index.js ├── controllers │ ├── metadata.controller.js │ ├── user.controller.js │ └── message.controller.js └── setupFunctions.js ├── .jsbeautifyrc ├── commands.md ├── .eslintrc.json ├── LICENSE ├── package.json ├── README.md └── scripts └── setupConfig.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode/ 3 | package-lock.json 4 | config.json -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | "babel-plugin-transform-object-rest-spread" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/models/entities/pollOption.js: -------------------------------------------------------------------------------- 1 | let PollOption = { 2 | text: String, 3 | voter_count: Number 4 | }; 5 | 6 | export default PollOption; -------------------------------------------------------------------------------- /src/models/messageTypes/location.js: -------------------------------------------------------------------------------- 1 | let Location = { 2 | longitude: Number, 3 | latitude: Number 4 | }; 5 | 6 | export default Location; -------------------------------------------------------------------------------- /src/models/entities/photoSize.js: -------------------------------------------------------------------------------- 1 | let PhotoSize = { 2 | file_id: String, 3 | width: Number, 4 | height: Number, 5 | file_size: Number 6 | }; 7 | 8 | export default PhotoSize; -------------------------------------------------------------------------------- /src/models/messageTypes/voice.js: -------------------------------------------------------------------------------- 1 | let Voice = { 2 | file_id: String, 3 | duration: Number, 4 | mime_type: String, 5 | file_size: Number 6 | }; 7 | 8 | export default Voice; -------------------------------------------------------------------------------- /src/models/entities/maskPosition.js: -------------------------------------------------------------------------------- 1 | let MaskPosition = { 2 | point: String, 3 | x_shift: Number, 4 | y_shift: Number, 5 | scale: Number 6 | }; 7 | 8 | export default MaskPosition; -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 4, 3 | "indent_char": " ", 4 | "css": { 5 | "indent_size": 2 6 | }, 7 | "brace_style": "collapse,preserve-inline", 8 | "space_after_anon_function": false 9 | } -------------------------------------------------------------------------------- /src/models/messageTypes/contact.js: -------------------------------------------------------------------------------- 1 | let Contact = { 2 | phone_number: String, 3 | first_name: String, 4 | last_name: String, 5 | user_id: Number, 6 | vcard: String 7 | }; 8 | 9 | export default Contact; -------------------------------------------------------------------------------- /src/models/messageTypes/invoice.js: -------------------------------------------------------------------------------- 1 | let Invoice = { 2 | title: String, 3 | description: String, 4 | start_parameter: String, 5 | currency: String, 6 | total_amount: Number 7 | }; 8 | 9 | export default Invoice; -------------------------------------------------------------------------------- /src/models/entities/messageEntity.js: -------------------------------------------------------------------------------- 1 | let MessageEntity = { 2 | type: { 3 | type: String 4 | }, 5 | offset: Number, 6 | length: Number, 7 | url: String, 8 | user: Number 9 | }; 10 | 11 | export default MessageEntity; -------------------------------------------------------------------------------- /src/models/entities/orderInfo.js: -------------------------------------------------------------------------------- 1 | import ShippingAddress from "./shippingAddress"; 2 | 3 | let OrderInfo = { 4 | name: String, 5 | phone_number: String, 6 | email: String, 7 | shipping_address: ShippingAddress 8 | }; 9 | 10 | export default OrderInfo; -------------------------------------------------------------------------------- /src/models/messageTypes/venue.js: -------------------------------------------------------------------------------- 1 | import Location from "./location"; 2 | 3 | let Venue = { 4 | location: Location, 5 | title: String, 6 | address: String, 7 | foursquare_id: String, 8 | foursquare_type: String 9 | }; 10 | 11 | export default Venue; -------------------------------------------------------------------------------- /src/models/entities/shippingAddress.js: -------------------------------------------------------------------------------- 1 | let ShippingAddress = { 2 | country_code: String, 3 | state: String, 4 | city: String, 5 | street_line_1: String, 6 | street_line_2: String, 7 | post_code: String 8 | }; 9 | 10 | export default ShippingAddress; -------------------------------------------------------------------------------- /src/models/messageTypes/document.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photosize"; 2 | 3 | let Document = { 4 | file_id: String, 5 | mime_type: String, 6 | file_size: Number, 7 | file_name: String, 8 | thumb: PhotoSize 9 | }; 10 | 11 | export default Document; -------------------------------------------------------------------------------- /src/models/messageTypes/videoNote.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photosize"; 2 | 3 | let VideoNote = { 4 | file_id: String, 5 | length: Number, 6 | duration: Number, 7 | file_size: Number, 8 | thumb: PhotoSize 9 | }; 10 | 11 | export default VideoNote; -------------------------------------------------------------------------------- /src/models/messageTypes/poll.js: -------------------------------------------------------------------------------- 1 | import PollOption from "../entities/pollOption"; 2 | 3 | let Poll = { 4 | id: String, 5 | question: String, 6 | options: { 7 | type: [PollOption], 8 | default: undefined 9 | }, 10 | is_closed: Boolean 11 | }; 12 | 13 | export default Poll; -------------------------------------------------------------------------------- /src/databaseAccess/postMessage.db.js: -------------------------------------------------------------------------------- 1 | import Message from "../models/schemas/message.schema"; 2 | 3 | export default async function save_message(message) { 4 | try { 5 | await new Message(message).save(); 6 | } catch (error) { 7 | throw new Error(`${error.message} (save_message)`); 8 | } 9 | } -------------------------------------------------------------------------------- /src/models/messageTypes/video.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photosize"; 2 | 3 | let Video = { 4 | file_id: String, 5 | width: Number, 6 | height: Number, 7 | duration: Number, 8 | mime_type: String, 9 | file_size: Number, 10 | thumb: PhotoSize 11 | }; 12 | 13 | export default Video; -------------------------------------------------------------------------------- /src/models/schemas/chat.schema.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import mongoose from "mongoose"; 3 | 4 | // Import internal packages 5 | import Chat from "../entities/chat"; 6 | 7 | let Schema = mongoose.Schema; 8 | 9 | let ChatSchema = new Schema(Chat); 10 | 11 | export default mongoose.model("Chat", ChatSchema); -------------------------------------------------------------------------------- /src/models/messageTypes/audio.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photosize"; 2 | 3 | let Audio = { 4 | file_id: String, 5 | duration: Number, 6 | performer: String, 7 | title: String, 8 | mime_type: String, 9 | file_size: Number, 10 | thumb: PhotoSize 11 | }; 12 | 13 | export default Audio; -------------------------------------------------------------------------------- /src/models/schemas/message.schema.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import mongoose from "mongoose"; 3 | 4 | // Import internal packages 5 | import Message from "../entities/message"; 6 | 7 | let Schema = mongoose.Schema; 8 | 9 | let MessageSchema = new Schema(Message); 10 | 11 | export default mongoose.model("Message", MessageSchema); -------------------------------------------------------------------------------- /src/models/entities/chatPhoto.js: -------------------------------------------------------------------------------- 1 | let ChatPhoto = { 2 | type: { 3 | small_file_id: { 4 | type: String, 5 | required: true 6 | }, 7 | big_file_id: { 8 | type: String, 9 | required: true 10 | }, 11 | }, 12 | required: false 13 | }; 14 | 15 | export default ChatPhoto; -------------------------------------------------------------------------------- /src/models/messageTypes/animation.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photoSize"; 2 | 3 | let Animation = { 4 | file_id: String, 5 | width: Number, 6 | height: Number, 7 | duration: Number, 8 | mime_type: String, 9 | file_name: String, 10 | file_size: Number, 11 | thumb: PhotoSize 12 | }; 13 | 14 | export default Animation; -------------------------------------------------------------------------------- /src/models/schemas/statBotUser.schema.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import mongoose from "mongoose"; 3 | 4 | // Import internal packages 5 | import StatBotUser from "../entities/statBotUser"; 6 | 7 | let Schema = mongoose.Schema; 8 | 9 | let StatBotUserSchema = new Schema(StatBotUser); 10 | 11 | export default mongoose.model("StatBotUser", StatBotUserSchema); -------------------------------------------------------------------------------- /src/databaseAccess/getConsentLevel.db.js: -------------------------------------------------------------------------------- 1 | import StatBotUser from "../models/schemas/statBotUser.schema"; 2 | 3 | export default async function consent_level(user) { 4 | try { 5 | let foundUser = await StatBotUser.findOne({ id: user.id }); 6 | return foundUser.data_collection_consent; 7 | } catch (error) { 8 | throw new Error(`${error.message} (consent_level)`); 9 | } 10 | } -------------------------------------------------------------------------------- /src/models/entities/user.js: -------------------------------------------------------------------------------- 1 | let User = { 2 | id: { 3 | type: Number, 4 | required: true 5 | }, 6 | is_bot: { 7 | type: Boolean, 8 | required: true 9 | }, 10 | first_name: { 11 | type: String, 12 | required: true 13 | }, 14 | last_name: String, 15 | username: String, 16 | language_code: String 17 | }; 18 | 19 | export default User; -------------------------------------------------------------------------------- /src/models/messageTypes/sticker.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photosize"; 2 | import MaskPosition from "../entities/maskPosition"; 3 | 4 | let Sticker = { 5 | file_id: String, 6 | width: Number, 7 | height: Number, 8 | emoji: String, 9 | set_name: String, 10 | mask_position: MaskPosition, 11 | file_size: Number, 12 | thumb: PhotoSize 13 | }; 14 | 15 | export default Sticker; -------------------------------------------------------------------------------- /src/models/messageTypes/successfulPayment.js: -------------------------------------------------------------------------------- 1 | import OrderInfo from "../entities/orderInfo"; 2 | 3 | let SuccessfulPayment = { 4 | currency: String, 5 | total_amount: Number, 6 | invoice_payload: String, 7 | shipping_option_id: String, 8 | order_info: OrderInfo, 9 | telegram_payment_charge_id: String, 10 | provider_payment_charge_id: String 11 | }; 12 | 13 | export default SuccessfulPayment; -------------------------------------------------------------------------------- /src/bot/messages/messageCollection.js: -------------------------------------------------------------------------------- 1 | export default class MessageCollection { 2 | constructor(messages) { 3 | this.messages = messages || []; 4 | } 5 | 6 | addMessage(message) { 7 | this.messages.push(message); 8 | } 9 | 10 | sendAll() { 11 | this.messages.forEach((message, index) => { 12 | setTimeout(() => message.send(), 1000 * index); 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /src/databaseAccess/upsertChat.db.js: -------------------------------------------------------------------------------- 1 | import Chat from "../models/schemas/chat.schema"; 2 | 3 | export default async function upsert_chat(chat) { 4 | try { 5 | await Chat.findOneAndUpdate({ id: chat.id }, chat, { 6 | upsert: true, 7 | useFindAndModify: false, 8 | setDefaultsOnInsert: true 9 | }); 10 | } catch (error) { 11 | throw new Error(`${error.message} (upsert_chat)`); 12 | } 13 | } -------------------------------------------------------------------------------- /src/databaseAccess/getAllTexts.db.js: -------------------------------------------------------------------------------- 1 | import Message from "../models/schemas/message.schema"; 2 | 3 | export default async function all_texts(chatId) { 4 | try { 5 | return await Message.find({ 6 | "chat.id": chatId, 7 | text: { "$exists": true } 8 | }, { 9 | text: 1, 10 | _id: 0 11 | }); 12 | } catch (error) { 13 | throw new Error(`${error.message} (all_texts)`); 14 | } 15 | } -------------------------------------------------------------------------------- /src/databaseAccess/upsertUser.db.js: -------------------------------------------------------------------------------- 1 | import StatBotUser from "../models/schemas/statBotUser.schema"; 2 | 3 | export default async function upsert_user(user) { 4 | try { 5 | await StatBotUser.findOneAndUpdate({ id: user.id }, user, { 6 | upsert: true, 7 | useFindAndModify: false, 8 | setDefaultsOnInsert: true 9 | }); 10 | } catch (error) { 11 | throw new Error(`${error.message} (upsert_user)`); 12 | } 13 | } -------------------------------------------------------------------------------- /src/models/entities/statBotUser.js: -------------------------------------------------------------------------------- 1 | let StatBotUser = { 2 | id: { 3 | type: Number, 4 | required: true 5 | }, 6 | first_name: { 7 | type: String, 8 | required: true 9 | }, 10 | last_name: String, 11 | username: String, 12 | language_code: String, 13 | data_collection_consent: { 14 | type: String, 15 | required: true, 16 | default: "deny" 17 | } 18 | }; 19 | 20 | export default StatBotUser; -------------------------------------------------------------------------------- /src/models/messageTypes/game.js: -------------------------------------------------------------------------------- 1 | import PhotoSize from "../entities/photoSize"; 2 | import MessageEntity from "../entities/messageEntity"; 3 | import Animation from "./animation"; 4 | 5 | let Game = { 6 | title: String, 7 | description: String, 8 | photo: { 9 | type: [PhotoSize], 10 | default: undefined 11 | }, 12 | text: String, 13 | text_entities: { 14 | type: [MessageEntity], 15 | default: undefined 16 | }, 17 | animation: Animation 18 | }; 19 | 20 | export default Game; -------------------------------------------------------------------------------- /src/routes/users/getUser.route.js: -------------------------------------------------------------------------------- 1 | import { get_user } from "../../controllers/user.controller"; 2 | 3 | export default async function get_user_route(req, res) { 4 | let user_id = parseInt(req.params.id); 5 | 6 | if (isNaN(user_id)) { 7 | res.status(400).send("id parameter must be an integer"); 8 | } 9 | try { 10 | let result = await get_user(user_id); 11 | res.status(200).json({ 12 | result: result 13 | }); 14 | } catch (error) { 15 | console.error(error); 16 | res.status(500).send(error); 17 | } 18 | } -------------------------------------------------------------------------------- /commands.md: -------------------------------------------------------------------------------- 1 | chat_info - Gets general info about this chat 2 | consent_full - Update data collection consent 3 | consent_restricted - Update data collection consent 4 | consent_deny - Update data collection consent 5 | messages_per_user - Who writes the most? 6 | messages_per_weekday - What day of the week is busiest? 7 | my_data - Get a private message with the data I have saved about you 8 | total_messages - How many messages where sent in this group? 9 | total_messages_extended - How many messages where sent in this group grouped by type? 10 | word_count - How many words were sent in this group? Per message? -------------------------------------------------------------------------------- /src/routes/messages/getWordCount.route.js: -------------------------------------------------------------------------------- 1 | import { get_word_count } from "../../controllers/message.controller"; 2 | 3 | export default async function get_word_count_route(req, res) { 4 | let chatId = parseInt(req.params.chatId); 5 | 6 | if (isNaN(chatId)) { 7 | res.status(400).send("chatId parameter must be an integer"); 8 | } 9 | try { 10 | let result = await get_word_count(chatId); 11 | 12 | res.status(200).json({ 13 | result: result 14 | }); 15 | } catch (error) { 16 | console.error(error); 17 | res.status(500).send(error); 18 | } 19 | } -------------------------------------------------------------------------------- /src/routes/users/getPersonalData.route.js: -------------------------------------------------------------------------------- 1 | import { get_personal_data } from "../../controllers/user.controller"; 2 | 3 | export default async function get_personal_data_route(req, res) { 4 | let user_id = parseInt(req.params.id); 5 | 6 | if (isNaN(user_id)) { 7 | res.status(400).send("id parameter must be an integer"); 8 | } 9 | try { 10 | let result = await get_personal_data(user_id); 11 | 12 | res.status(200).json({ 13 | result: result 14 | }); 15 | } catch (error) { 16 | console.error(error); 17 | res.status(500).send(error); 18 | } 19 | } -------------------------------------------------------------------------------- /src/databaseAccess/getMessagesByWeekday.db.js: -------------------------------------------------------------------------------- 1 | import Message from "../models/schemas/message.schema"; 2 | 3 | export default async function messages_by_weekday(chat_id) { 4 | let query = [{ $match: { "chat.id": chat_id } }, 5 | { 6 | $group: { 7 | _id: { 8 | weekday: { $dayOfWeek: "$date" }, 9 | }, 10 | count: { $sum: 1 } 11 | } 12 | } 13 | ]; 14 | 15 | try { 16 | return await Message.aggregate(query); 17 | } catch (error) { 18 | throw new Error(`${error.message} (messages_by_weekday)`); 19 | } 20 | } -------------------------------------------------------------------------------- /src/routes/messages/getMessagesByHour.route.js: -------------------------------------------------------------------------------- 1 | import { getMessagesByHour } from "../../controllers/message.controller"; 2 | 3 | export default async function get_messages_by_hour_route(req, res) { 4 | let chat_id = parseInt(req.params.chatId); 5 | 6 | if (isNaN(chat_id)) { 7 | res.status(400).send("chatId parameter must be an integer"); 8 | } 9 | 10 | try { 11 | let result = await getMessagesByHour(chat_id); 12 | 13 | res.status(200).json({ 14 | result: result 15 | }); 16 | } catch (error) { 17 | console.error(error); 18 | res.status(500).send(error); 19 | } 20 | } -------------------------------------------------------------------------------- /src/routes/meta/getMembershipEventsRoute.js: -------------------------------------------------------------------------------- 1 | import { get_membership_events } from "../../controllers/metadata.controller"; 2 | 3 | export default async function get_membership_events_route(req, res) { 4 | let chat_id = parseInt(req.params.chatId); 5 | 6 | if (isNaN(chat_id)) { 7 | res.status(400).send("chatId parameter must be an integer"); 8 | } 9 | 10 | try { 11 | let result = await get_membership_events(chat_id); 12 | 13 | res.status(200).json({ 14 | result: result 15 | }); 16 | } catch (error) { 17 | console.error(error); 18 | res.status(500).send(error); 19 | } 20 | } -------------------------------------------------------------------------------- /src/routes/messages/getMessagesByWeekday.route.js: -------------------------------------------------------------------------------- 1 | import { get_messages_by_weekday } from "../../controllers/message.controller"; 2 | 3 | export default async function get_messages_by_weekday_route(req, res) { 4 | let chat_id = parseInt(req.params.chatId); 5 | 6 | if (isNaN(chat_id)) { 7 | res.status(400).send("chatId parameter must be an integer"); 8 | } 9 | 10 | try { 11 | let result = await get_messages_by_weekday(chat_id); 12 | 13 | res.status(200).json({ 14 | result: result 15 | }); 16 | } catch (error) { 17 | console.error(error); 18 | res.status(500).send(error); 19 | } 20 | } -------------------------------------------------------------------------------- /src/databaseAccess/getMessagesByUser.db.js: -------------------------------------------------------------------------------- 1 | import Message from "../models/schemas/message.schema"; 2 | 3 | export default async function messages_by_user(chat_id) { 4 | let query = [{ $match: { "chat.id": chat_id } }, 5 | { 6 | $group: { 7 | "_id": "$from.id", 8 | "count": { $sum: 1 }, 9 | "user": { $first: "$from" } 10 | } 11 | }, 12 | { 13 | $sort: { "count": -1 } 14 | } 15 | ]; 16 | 17 | try { 18 | return await Message.aggregate(query); 19 | } catch (error) { 20 | throw new Error(`${error.message} (messages_by_user)`); 21 | } 22 | } -------------------------------------------------------------------------------- /src/routes/messages/postMessage.route.js: -------------------------------------------------------------------------------- 1 | import { post_message } from "../../controllers/message.controller"; 2 | 3 | export default function post_message_route(req, res) { 4 | let contentData = []; 5 | req.on("data", chunk => { 6 | contentData.push(chunk); 7 | }); 8 | 9 | req.on("end", async () => { 10 | let content = JSON.parse(contentData); 11 | let message = content.message; 12 | let metadata = content.metadata; 13 | 14 | try { 15 | await post_message(message, metadata) 16 | } catch (error) { 17 | console.error(error); 18 | res.status(500).send(error); 19 | } 20 | }); 21 | } -------------------------------------------------------------------------------- /src/routes/users/upsertUserRoute.js: -------------------------------------------------------------------------------- 1 | import { upsertUser } from "../../controllers/user.controller"; 2 | 3 | export default function upsert_user_route(req, res) { 4 | let contentData = []; 5 | 6 | req.on("data", chunk => { 7 | contentData.push(chunk); 8 | }); 9 | 10 | req.on("end", async () => { 11 | let user = JSON.parse(contentData); 12 | 13 | try { 14 | let result = await upsertUser(user); 15 | 16 | res.status(200).json({ 17 | result: result 18 | }); 19 | } catch (error) { 20 | console.error(error); 21 | res.status(error.status).send(error.message); 22 | } 23 | }); 24 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "indent": [ 17 | "error", 18 | 4 19 | ], 20 | "linebreak-style": [ 21 | "error", 22 | "windows" 23 | ], 24 | "quotes": [ 25 | "error", 26 | "double" 27 | ], 28 | "semi": [ 29 | "error", 30 | "always" 31 | ], 32 | "no-console": "off" 33 | } 34 | } -------------------------------------------------------------------------------- /src/routes/messages/getMessageTotal.route.js: -------------------------------------------------------------------------------- 1 | import { get_message_total, get_message_total_extended } from "../../controllers/message.controller"; 2 | 3 | export default async function get_message_total_route(req, res) { 4 | let chat_id = parseInt(req.params.chatId); 5 | let extended = req.query.extended === "true"; 6 | 7 | if (isNaN(chat_id)) { 8 | res.status(400).send("chatId parameter must be an integer"); 9 | } 10 | 11 | let handler = extended ? get_message_total_extended : get_message_total; 12 | try { 13 | let result = await handler(chat_id); 14 | res.status(200).json({ 15 | result: result 16 | }); 17 | } catch (error) { 18 | console.error(error); 19 | res.status(500).send(error); 20 | } 21 | } -------------------------------------------------------------------------------- /src/models/entities/chat.js: -------------------------------------------------------------------------------- 1 | import ChatPhoto from "./chatPhoto"; 2 | // TODO: Fill data collection times on group enter or exit events 3 | let Chat = { 4 | id: { 5 | type: Number, 6 | required: true 7 | }, 8 | chat_type: { 9 | type: String, 10 | required: true 11 | }, 12 | title: String, 13 | username: String, 14 | first_name: String, 15 | last_name: String, 16 | all_members_are_administrators: Boolean, 17 | photo: ChatPhoto, 18 | description: String, 19 | invite_link: String, 20 | pinned_message: Number, 21 | sticker_set_name: String, 22 | can_set_sticker_set: Boolean, 23 | data_collection_times: [ 24 | { 25 | entry: Number, 26 | exit: Number 27 | } 28 | ] 29 | }; 30 | 31 | export default Chat; -------------------------------------------------------------------------------- /src/bot/commands/wordCount.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | 7 | export default async function word_count(message, stat_bot) { 8 | let chat = message.chat.id; 9 | let log = ["/word_count"]; 10 | 11 | let response = await request({ 12 | uri: `http://localhost:${stat_bot.backend_port}/messages/wordCount/${chat}`, 13 | json: true 14 | }); 15 | 16 | let result = response.result; 17 | 18 | log.push(result); 19 | stat_bot.log(log); 20 | 21 | let reply = new TextMessage(stat_bot.bot, chat, "de", "Markdown"); 22 | 23 | reply.addLineTranslated("word_count", { 24 | total_words: result.total, 25 | avgPerMessage: result.avgPerMessage 26 | }); 27 | 28 | reply.send(); 29 | } -------------------------------------------------------------------------------- /src/bot/messages/documentMessage.js: -------------------------------------------------------------------------------- 1 | import { getBotReplyTranslation } from "../translate"; 2 | 3 | export default class DocumentMessage { 4 | constructor(bot, chat, document) { 5 | this.bot = bot; 6 | this.chat = chat; 7 | this.document = document; 8 | } 9 | 10 | set_caption(caption, parse_mode) { 11 | this.caption = caption; 12 | this.parse_mode = parse_mode; 13 | } 14 | 15 | set_caption_translated(key, language, params, parse_mode) { 16 | this.caption = getBotReplyTranslation(key, language, params); 17 | this.parse_mode = parse_mode; 18 | } 19 | 20 | send() { 21 | let options = { disable_notification: true }; 22 | if (this.parse_mode) options.parse_mode = this.parse_mode; 23 | 24 | this.bot.sendDocument(this.chat, this.document, options); 25 | 26 | console.log("Sent message to " + this.chat.id) 27 | } 28 | } -------------------------------------------------------------------------------- /src/bot/commands/totalMessages.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | 7 | export default async function total_messages(message, stat_bot) { 8 | let chat = message.chat.id; 9 | let log = ["/total_messages"]; 10 | 11 | try { 12 | let response = await request({ 13 | uri: `http://localhost:${stat_bot.backend_port}/messages/total/${chat}`, 14 | json: true 15 | }); 16 | 17 | let total_messages = response.result; 18 | 19 | log.push(total_messages); 20 | stat_bot.log(log); 21 | 22 | let reply = new TextMessage(stat_bot.bot, chat, "de", "Markdown"); 23 | 24 | reply.addLineTranslated("total_messages", { total_messages: total_messages }); 25 | 26 | reply.send(); 27 | 28 | } catch (err) { 29 | console.log(err); 30 | } 31 | } -------------------------------------------------------------------------------- /src/routes/messages/getMessagesByUser.route.js: -------------------------------------------------------------------------------- 1 | import { get_messages_by_user } from "../../controllers/message.controller"; 2 | 3 | export default async function get_messages_by_user_route(req, res) { 4 | let chat_id = parseInt(req.params.chatId); 5 | // let extended = req.query.extended === "true"; 6 | 7 | if (isNaN(chat_id)) { 8 | res.status(400).send("chatId parameter must be an integer"); 9 | } 10 | try { 11 | let result = await get_messages_by_user(chat_id); 12 | let total_messages = result.map(x => x.count).reduce((sum, value) => sum + value); 13 | 14 | for (let i = 0; i < result.length; i++) { 15 | result[i].percentage = +((result[i].count / total_messages) * 100).toFixed(2); 16 | } 17 | 18 | res.status(200).json({ 19 | result: result 20 | }); 21 | } catch (error) { 22 | console.error(error); 23 | res.status(500).send(error); 24 | } 25 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // Import external packages 3 | import colors from "colors/safe"; 4 | 5 | // Import internal packages 6 | import { setup_database, setup_backend, setup_bot } from "./setupFunctions"; 7 | 8 | console.log(); 9 | console.log(colors.bgWhite.black("----- Initializing STAT BOT ----------------------------------")); 10 | 11 | setup(); 12 | 13 | async function setup() { 14 | let setup_database_result = await setup_database(); 15 | let setup_backend_result = await setup_backend(); 16 | let setup_bot_result = await setup_bot(setup_backend_result.port); 17 | 18 | if (setup_database_result.success && setup_backend_result.success && setup_bot_result.success) { 19 | console.log(colors.bgGreen.black("----- Everything up and running! -----------------------------")); 20 | } else { 21 | console.log(colors.bgRed.black("----- Something went wrong. Check config and try again? ------")); 22 | process.exit(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/bot/translate.js: -------------------------------------------------------------------------------- 1 | import translations from "./translations.json"; 2 | 3 | export function getMessageTypeTranslation(key, lang, plural) { 4 | return translations.message_types[key][lang][plural ? "many" : "single"]; 5 | } 6 | 7 | export function getBotReplyTranslation(key, lang, params) { 8 | let lines = translations.bot_replies[key] ? translations.bot_replies[key][lang] : undefined; 9 | 10 | if (lines) { 11 | let text = lines.join("\n"); 12 | 13 | let regex = /p\[(\w+)\]/g; 14 | 15 | var matches = Array.from(text.matchAll(regex), x => [x[0], x[1]]); 16 | 17 | matches.forEach(x => { 18 | text = text.replace(x[0], params[x[1]] || x[0]); 19 | }); 20 | 21 | return text; 22 | } else { 23 | return `Missing translation for ${lang}: ${key}`; 24 | } 25 | } 26 | 27 | export function get_raw_translation(key, lang, params) { 28 | return translations.bot_replies[key] ? translations.bot_replies[key][lang] : undefined; 29 | } -------------------------------------------------------------------------------- /src/controllers/metadata.controller.js: -------------------------------------------------------------------------------- 1 | import config from "../../config.json"; 2 | 3 | import Message from "../models/schemas/message.schema"; 4 | 5 | const OWNID = config.own_id; 6 | const LEFT = "left"; 7 | const JOINED = "joined"; 8 | 9 | // TODO: put database access in db function in separate file 10 | export async function get_membership_events(chatId) { 11 | try { 12 | let relevantMessages = await Message.find({ 13 | $and: [ 14 | { $or: [{ new_chat_members: OWNID }, { left_chat_member: OWNID }] }, 15 | { chat: chatId } 16 | ] 17 | }).sort({ 18 | date: 1 19 | }); 20 | 21 | let relevantEvents = relevantMessages.map(x => { 22 | return { 23 | date: new Date(x.date * 1000), 24 | type: x.left_chat_member ? LEFT : JOINED 25 | }; 26 | }); 27 | 28 | return relevantEvents; 29 | } catch (error) { 30 | throw new Error(error); 31 | } 32 | } -------------------------------------------------------------------------------- /src/databaseAccess/getMessageTotal.db.js: -------------------------------------------------------------------------------- 1 | import Message from "../models/schemas/message.schema"; 2 | 3 | export async function message_total(chat_id) { 4 | try { 5 | return await Message.find({ "chat.id": chat_id }).countDocuments(); 6 | } catch (error) { 7 | throw new Error(`${error.message} (message_total))`); 8 | } 9 | } 10 | 11 | export async function message_total_extended(chat_id) { 12 | let query = [{ $match: { "chat.id": chat_id } }, 13 | { 14 | $group: { 15 | _id: "$message_type", 16 | count: { $sum: 1 } 17 | } 18 | }, { 19 | $sort: { count: -1 } 20 | }, { 21 | $project: { 22 | _id: 0, 23 | type: "$_id", 24 | count: 1 25 | } 26 | } 27 | ]; 28 | 29 | try { 30 | return await Message.aggregate(query); 31 | } catch (error) { 32 | throw new Error(`${error.message} (message_total_extended))`); 33 | } 34 | } -------------------------------------------------------------------------------- /src/databaseAccess/getMessagesByHour.db.js: -------------------------------------------------------------------------------- 1 | import Message from "../models/schemas/message.schema"; 2 | 3 | export default async function messages_by_hour(chat_id) { 4 | let query = [{ $match: { "chat.id": chat_id } }, 5 | { 6 | $group: { 7 | _id: { 8 | hour: { 9 | $hour: { 10 | date: { 11 | $toDate: { 12 | $multiply: [1000, "$date"] 13 | } 14 | }, 15 | timezone: Intl.DateTimeFormat().resolvedOptions().timeZone 16 | } 17 | } 18 | }, 19 | count: { "$sum": 1 } 20 | } 21 | }, 22 | { 23 | $sort: { "_id.hour": 1 } 24 | } 25 | ]; 26 | 27 | try { 28 | return await Message.aggregate(query); 29 | } catch (error) { 30 | throw new Error(`${error.message} (messages_by_hour)`); 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Leon Schreiber 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegram-stat-bot", 3 | "version": "0.0.1", 4 | "description": "Backend server that accepts and processes requests from telegram-stat-bot", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "node_modules/webpack/bin/webpack.js --progress --hide-modules ", 8 | "debug": "nodemon --inspect --watch src --exec node_modules/.bin/babel-node --presets env src/index.js", 9 | "dev": "node scripts/setupConfig.js && \"node_modules/.bin/nodemon\" src/index.js --exec \"node_modules/.bin/babel-node --\"", 10 | "start": "node scripts/setupConfig.js && \"node_modules/.bin/babel-node\" src/index.js" 11 | }, 12 | "author": "Leon Schreiber", 13 | "license": "MIT", 14 | "dependencies": { 15 | "babel-cli": "^6.26.0", 16 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 17 | "babel-preset-env": "^1.7.0", 18 | "colors": "^1.3.3", 19 | "express": "^4.17.1", 20 | "get-port": "^5.0.0", 21 | "inquirer": "^6.5.1", 22 | "mongoose": "^5.6.6", 23 | "node-telegram-bot-api": "^0.30.0", 24 | "npm": "^6.11.2", 25 | "request-promise": "^4.2.4" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^6.2.1", 29 | "nodemon": "^1.19.1" 30 | } 31 | } -------------------------------------------------------------------------------- /src/bot/commands/chatInfo.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import colors from "colors/safe"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | 7 | export default async function chat_info(message, stat_bot) { 8 | let log = ["/chat_info || "]; 9 | log.push(message.chat); 10 | 11 | try { 12 | let reply = new TextMessage(stat_bot.bot, message.chat.id, "de", "Markdown"); 13 | 14 | if (message.chat.type === "private") { 15 | reply.addLineTranslated("private_chat_data", { 16 | id: message.chat.id || " ", 17 | first_name: message.chat.first_name || " ", 18 | last_name: message.chat.last_name || " ", 19 | username: message.chat.username || " ", 20 | }); 21 | } else { 22 | reply.addLineTranslated("group_chat_data", { 23 | id: message.chat.id || " ", 24 | title: message.chat.title || " ", 25 | all_members_are_administrators: message.chat.all_members_are_administrators || " " 26 | }); 27 | } 28 | reply.send(); 29 | } catch (error) { 30 | log.push(colors.bgRed.black("Error")); 31 | log.push(colors.red(error)); 32 | } finally { 33 | stat_bot.log(log); 34 | } 35 | } -------------------------------------------------------------------------------- /src/bot/commands/consent.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | 7 | export default async function consent(message, consent_level, stat_bot) { 8 | let chat = message.chat.id; 9 | 10 | try { 11 | let response = await request({ 12 | method: "PUT", 13 | uri: `http://localhost:${stat_bot.backend_port}/users`, 14 | json: true, 15 | body: { 16 | ...message.from, 17 | data_collection_consent: consent_level 18 | }, 19 | contentType: "application/json" 20 | }); 21 | 22 | let reply = new TextMessage(stat_bot.bot, chat, "de", "Markdown"); 23 | 24 | reply.addLineTranslated("confirm_consent_update", { 25 | user_name: stat_bot.get_user_address(message.from), 26 | previous_value: response.result.data_collection_consent, 27 | new_value: consent_level 28 | }); 29 | 30 | reply.send(); 31 | } catch (error) { 32 | console.error(error.message); 33 | 34 | if (error.statusCode === 422) { 35 | let reply = new TextMessage(stat_bot.bot, chat, "de", "Markdown"); 36 | 37 | reply.addLineTranslated("invalid_consent_update"); 38 | 39 | reply.send(); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/bot/commands/totalMessagesExtended.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | import { getMessageTypeTranslation } from "../translate"; 7 | 8 | export default async function total_messages_extended(message, stat_bot) { 9 | let chat = message.chat.id; 10 | let log = ["/total_messages_extended"]; 11 | 12 | try { 13 | let response = await request({ 14 | uri: `http://localhost:${stat_bot.backend_port}/messages/total/${chat}?extended=true`, 15 | json: true 16 | }); 17 | 18 | console.log(response); 19 | let total_messages_grouped = response.result; 20 | let total_messages = total_messages_grouped.reduce((a, b) => a + (b.count || 0), 0); 21 | let max_digits = Math.max(...total_messages_grouped.map(x => x.count.toString().length)); 22 | 23 | log.push(total_messages_grouped); 24 | stat_bot.log(log); 25 | 26 | let reply = new TextMessage(stat_bot.bot, chat, "de", "Markdown"); 27 | reply.addLineTranslated("total_messages_extended", { total_messages: total_messages }); 28 | 29 | total_messages_grouped.forEach((x) => { 30 | reply.addLine(`\`${(" ".repeat(max_digits) + x.count).slice(-max_digits)} \`` + 31 | `${getMessageTypeTranslation(x.type, "de", x.count > 1)}`); 32 | }); 33 | 34 | reply.send(); 35 | 36 | } catch (err) { 37 | console.log(err); 38 | } 39 | } -------------------------------------------------------------------------------- /src/bot/handlers/onMessage.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | import config from "../../../config.json"; 4 | 5 | export default async function on_message(message, metadata, stat_bot) { 6 | try { 7 | if (config.development) { 8 | if (config.blacklist && config.blacklist.includes(message.chat.id)) { 9 | console.log(`${message.chat.id} (${(message.chat.title || message.chat.username || message.chat.first_name).slice(0, 25)}) tried to contact but was blacklisted`); 10 | console.log(message.text + "\n"); 11 | stat_bot.bot.leaveChat(message.chat.id); 12 | // stat_bot.bot.sendMessage(message.chat.id, "I'm in active development! Please be patient :)"); 13 | } 14 | if (config.whitelist && !config.whitelist.includes(message.chat.id)) { 15 | console.log(message.chat.id + " tried to contact, but couldn't:"); 16 | console.log(message); 17 | stat_bot.bot.sendMessage(message.chat.id, "I'm in active development! Please be patient :)"); 18 | } 19 | } 20 | 21 | await request({ 22 | method: "POST", 23 | uri: `http://localhost:${stat_bot.backend_port}/messages`, 24 | json: true, 25 | body: { 26 | message: message, 27 | metadata: metadata 28 | }, 29 | contentType: "application/json" 30 | }); 31 | } catch (e) { 32 | console.log(e); 33 | } 34 | } -------------------------------------------------------------------------------- /src/bot/messages/textMessage.js: -------------------------------------------------------------------------------- 1 | import { getBotReplyTranslation } from "../translate"; 2 | 3 | export default class TextMessage { 4 | constructor(bot, chat, language, parse_mode, reply_to_message_id) { 5 | this.bot = bot; 6 | this.chat = chat; 7 | this.language = language; 8 | this.parse_mode = parse_mode; 9 | this.reply_to_message_id = reply_to_message_id; 10 | this.lines = []; 11 | } 12 | 13 | addLine(text) { 14 | this.lines.push(text); 15 | } 16 | 17 | addLink(url, display_text) { 18 | let text = display_text ? `[${display_text}](${url})` : url; 19 | this.lines.push(text); 20 | } 21 | 22 | addChart(chart_data) { 23 | let url = `https://quickchart.io/chart?bkg=black&width=500&height=300&c=${encodeURIComponent(JSON.stringify(chart_data).replace(/"/g, "'"))}`; 24 | this.addLink(url, "‎"); 25 | } 26 | 27 | addLineTranslated(key, params) { 28 | this.lines.push(getBotReplyTranslation(key, this.language, params)); 29 | } 30 | 31 | send() { 32 | try { 33 | let text = this.lines.join("\n"); 34 | let options = { 35 | disable_notification: true 36 | }; 37 | 38 | if (this.parse_mode) options.parse_mode = this.parse_mode; 39 | if (this.reply_to_message_id) options.reply_to_message_id = this.reply_to_message_id; 40 | 41 | this.bot.sendMessage(this.chat, text, options); 42 | } catch (error) { 43 | console.log(error); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import StatBotUser from "../models/schemas/statBotUser.schema"; 2 | import Message from "../models/schemas/message.schema"; 3 | 4 | export async function get_user(id) { 5 | try { 6 | return await StatBotUser.find({ id: id }); 7 | } catch (error) { 8 | throw new Error(`${error.message} (get_user)`); 9 | } 10 | } 11 | 12 | export async function get_personal_data(id) { 13 | let user = (await StatBotUser.find({ "id": id }))[0]; 14 | 15 | let messages_per_chat = await Message.aggregate([{ 16 | $match: { "from.id": id } 17 | }, { 18 | $group: { _id: "$chat.id", count: { $sum: 1 } } 19 | }]); 20 | 21 | return { 22 | user, 23 | message_count: messages_per_chat.map(x => x.count).reduce((a, b) => a + b, 0), 24 | chat_count: messages_per_chat.length, 25 | }; 26 | } 27 | 28 | export async function upsertUser(user) { 29 | if (user.data_collection_consent && !["full", "restricted", "deny"].includes(user.data_collection_consent)) { 30 | throw new Error({ 31 | status: 422, 32 | message: "If the data_collection_consent property is set, it must be one of ['full', 'restricted', 'deny']" 33 | }); 34 | } else { 35 | try { 36 | let result = await StatBotUser.findOneAndUpdate({ "id": user.id }, user, { upsert: true }); 37 | return result; 38 | } catch (error) { 39 | throw new Error({ 40 | status: 500, 41 | message: error 42 | }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/routes/index.route.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import express from "express"; 3 | 4 | // Import message routes 5 | import get_message_total_route from "./messages/getMessageTotal.route"; 6 | import get_messages_by_user_route from "./messages/getMessagesByUser.route"; 7 | import get_word_count_route from "./messages/getWordCount.route"; 8 | import get_messages_by_weekday_route from "./messages/getMessagesByWeekday.route"; 9 | import get_messages_by_hour_route from "./messages/getMessagesByHour.route"; 10 | import post_message_route from "./messages/postMessage.route"; 11 | 12 | // Import metadata routes 13 | import get_membership_events_route from "./meta/getMembershipEventsRoute"; 14 | 15 | // Import user routes 16 | import get_user_route from "./users/getUser.route"; 17 | import get_personal_data_route from "./users/getPersonalData.route"; 18 | import upsert_user_route from "./users/upsertUserRoute"; 19 | 20 | const router = express.Router(); 21 | 22 | // Configure message routes 23 | router.get("/messages/total/:chatId", get_message_total_route); 24 | router.get("/messages/byUser/:chatId", get_messages_by_user_route); 25 | router.get("/messages/wordCount/:chatId", get_word_count_route); 26 | router.get("/messages/byWeekday/:chatId", get_messages_by_weekday_route); 27 | router.get("/messages/byHour/:chatId", get_messages_by_hour_route); 28 | router.post("/messages", post_message_route); 29 | 30 | // Configute metadata routes 31 | router.get("/meta/membership/:chatId", get_membership_events_route); 32 | 33 | // Configure user routes 34 | router.get("/users/:id", get_user_route); 35 | router.get("/users/:id/data", get_personal_data_route); 36 | router.put("/users", upsert_user_route); 37 | 38 | // TODO: implement authentication! 39 | export default router; -------------------------------------------------------------------------------- /src/bot/commands/myData.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | import colors from "colors/safe"; 4 | 5 | // Import internal packages 6 | import TextMessage from "../messages/textMessage"; 7 | 8 | export default async function total_messages(message, stat_bot) { 9 | let log = [`/my_data || http://localhost:${stat_bot.backend_port}/users/${message.from.id}`]; 10 | 11 | try { 12 | let response = await request({ 13 | uri: `http://localhost:${stat_bot.backend_port}/users/${message.from.id}/data`, 14 | json: true 15 | }); 16 | 17 | log.push(response.result); 18 | 19 | let user_data = response.result.user; 20 | let message_count = response.result.message_count; 21 | let chat_count = response.result.chat_count; 22 | 23 | let group_reply = new TextMessage(stat_bot.bot, message.chat.id, "de", "Markdown", message.id); 24 | group_reply.addLineTranslated("personal_data_group"); 25 | group_reply.send(); 26 | 27 | let private_reply = new TextMessage(stat_bot.bot, message.from.id, "de", "Markdown"); 28 | private_reply.addLineTranslated("personal_data_private", { 29 | user_address: stat_bot.get_user_address(user_data), 30 | group_name: message.chat.title, 31 | message_count: message_count || " ", 32 | chat_count: chat_count || " ", 33 | first_name: user_data.first_name || " ", 34 | last_name: user_data.last_name || " ", 35 | username: user_data.username || " ", 36 | telegram_id: user_data.id || " ", 37 | consent_level: user_data.data_collection_consent || " ", 38 | language_code: user_data.language_code || " " 39 | }); 40 | private_reply.send(); 41 | 42 | } catch (error) { 43 | log.push(colors.bgRed.black("Error")); 44 | log.push(colors.red(error)); 45 | } finally { 46 | stat_bot.log(log); 47 | } 48 | } -------------------------------------------------------------------------------- /src/bot/commands/messagesPerUser.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | 7 | export default async function messages_per_user(message, stat_bot) { 8 | let log = [`/messages_per_user || http://localhost:${stat_bot.backend_port}/messages/byuser/${message.chat.id}`]; 9 | try { 10 | let response = await request({ 11 | uri: `http://localhost:${stat_bot.backend_port}/messages/byuser/${message.chat.id}`, 12 | json: true 13 | }); 14 | 15 | let messages_per_user = response.result; 16 | 17 | log.push(messages_per_user); 18 | stat_bot.log(log); 19 | 20 | reply(stat_bot, message, messages_per_user); 21 | } catch (err) { 22 | console.log(err); 23 | } 24 | } 25 | 26 | function reply(stat_bot, message, messages_per_user) { 27 | let chat = message.chat.id; 28 | let bot = stat_bot.bot; 29 | let reply = new TextMessage(bot, chat, "de", "Markdown"); 30 | 31 | let chart_data = { 32 | type: "outlabeledPie", 33 | data: { 34 | labels: [...messages_per_user.map(x => stat_bot.get_user_address(x.user))], 35 | datasets: [{ 36 | label: "total message percentage", 37 | data: messages_per_user.map(x => x.percentage) 38 | }] 39 | }, 40 | options: { 41 | plugins: { 42 | legend: false, 43 | outlabels: { 44 | text: "%l %p", 45 | color: "white", 46 | stretch: 35, 47 | font: { 48 | resizable: true, 49 | minSize: 12, 50 | maxSize: 18 51 | } 52 | } 53 | } 54 | } 55 | }; 56 | 57 | reply.addLineTranslated("messages_per_user"); 58 | 59 | reply.addChart(chart_data); 60 | 61 | messages_per_user.forEach((x) => { 62 | reply.addLine(`${messages_per_user.indexOf(x) + 1}. ${stat_bot.get_user_address(x.user)}: \`${x.count}\` messages (\`${x.percentage}%\`)`); 63 | }); 64 | 65 | reply.send(); 66 | } -------------------------------------------------------------------------------- /src/models/entities/message.js: -------------------------------------------------------------------------------- 1 | // Import message types 2 | import Animation from "../messageTypes/animation"; 3 | import Audio from "../messageTypes/audio"; 4 | import Contact from "../messageTypes/contact"; 5 | import Document from "../messageTypes/document"; 6 | import Game from "../messageTypes/game"; 7 | import Location from "../messageTypes/location"; 8 | import Poll from "../messageTypes/poll"; 9 | import Sticker from "../messageTypes/sticker"; 10 | import Venue from "../messageTypes/venue"; 11 | import Video from "../messageTypes/video"; 12 | import VideoNote from "../messageTypes/videoNote"; 13 | import Voice from "../messageTypes/voice"; 14 | 15 | // Import other needed entities 16 | import MessageEntity from "../entities/messageEntity"; 17 | import PhotoSize from "../entities/photoSize"; 18 | import Chat from "../entities/chat"; 19 | import User from "../entities/user"; 20 | 21 | let Message = { 22 | message_id: { type: Number, required: true, unique: true }, 23 | chat: { type: Chat, required: true }, 24 | date: { type: Date, required: true }, 25 | message_type: String, 26 | from: { type: User, required: false }, 27 | forward_from: { type: User, required: false }, 28 | forward_from_chat: { type: Chat, required: false }, 29 | forward_from_message_id: Number, 30 | forward_signature: String, 31 | forward_sender_name: String, 32 | forward_date: Number, 33 | reply_to_message: Number, 34 | edit_date: Number, 35 | media_group_id: String, 36 | author_signature: String, 37 | text: String, 38 | entities: { type: [MessageEntity], default: undefined }, 39 | caption_entities: { type: [MessageEntity], default: undefined }, 40 | audio: Audio, 41 | document: Document, 42 | animation: Animation, 43 | game: Game, 44 | photo: { type: [PhotoSize], default: undefined }, 45 | sticker: Sticker, 46 | video: Video, 47 | voice: Voice, 48 | video_note: VideoNote, 49 | caption: String, 50 | contact: Contact, 51 | location: Location, 52 | venue: Venue, 53 | poll: Poll, 54 | new_chat_members: { type: [User], default: undefined }, 55 | left_chat_member: { type: User, required: false }, 56 | new_chat_title: String, 57 | new_chat_photo: { type: [PhotoSize], default: undefined }, 58 | delete_chat_photo: Boolean, 59 | group_chat_created: Boolean, 60 | supergroup_chat_created: Boolean, 61 | channel_chat_created: Boolean, 62 | migrate_to_chat_id: Number, 63 | migrate_from_chat_id: Number 64 | }; 65 | 66 | export default Message; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Statistics Bot 2 | 3 | Open-source, self-hostable Telegram-Bot for collecting message data and generating interesting statistics and graphs from it. 4 | It is available under https://t.me/statbob_bot or you can host your own personal instance if you prefer to have full control over the saved data and store it on a private server. 5 | 6 | # Self-hosting 7 | You will need a JSON configuration file with some data that's necessary to run the bot and server. You can either create it yourself as described in #config or automatically generate and fill it with your infos by using the config wizard on the first application startup. 8 | 9 | 1. Clone the repository: `git clone https://github.com/Snowfire01/telegram-stat-bot.git` 10 | 2. Install missing npm packages: `npm install` 11 | 3. Start the server and bot: `npm start` 12 | 4. Walk through the config wizard if you don't already have a configuration file 13 | 14 | If you configured everything correctly, everything should be up and running and the bot listening to incoming messages now! 15 | 16 | # Usage 17 | [Work in progress] 18 | 19 | ## Configuration file 20 | 21 | Below is the structure of the config file including optional settings: 22 | 23 | { 24 | "telegram_bot_token": "", 25 | "own_id": , 26 | "language_default": "", 27 | "port": , 28 | "blacklist/whitelist": [ 29 | 30 | ] 31 | } 32 | 33 | **Telegram Bot Token** 34 | Your bot's Telegram API token. If you're unsure what your token is, you can retrieve it from BotFather. Format is `123456789:ABCd0EFgHIJKLm1_23noPQ45RSTUVW6XyZ7`. 35 | 36 | **Own ID** 37 | The user ID of your bot. Corresponds to the 9 digits before the colon in your API token. 38 | 39 | **Language default** 40 | The language your bot is going to speak when it enters a new group or encounters an unknown user. Language settings can later be personalized per group/user individually. 41 | 42 | ***Port (optional)*** 43 | The port under which the backend server with the databasewill be reachable for the bot. 44 | 45 | *If you omit this setting, a random free port will be chosen at every startup.* 46 | 47 | ***Blacklist/Whitelist (optional)*** 48 | You can choose to either maintain a blacklist (specify chats that will be ignored by the bot) or a whitelist (ignore all chats by default and specify chats from which messages should be saved). 49 | 50 | *If you omit this setting, the bot will listen to all chats.* 51 | -------------------------------------------------------------------------------- /src/bot/commands/messagesPerWeekday.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | 7 | export default async function messages_per_user(message, stat_bot) { 8 | let log = [`/messages_per_weekday || http://localhost:${stat_bot.backend_port}/messages/byWeekday/${message.chat.id}`]; 9 | try { 10 | let response = await request({ 11 | uri: `http://localhost:${stat_bot.backend_port}/messages/byWeekday/${message.chat.id}`, 12 | json: true 13 | }); 14 | 15 | let messages_per_weekday = response.result; 16 | 17 | // Sunday is indexed with 0 by defaul. Make it 7 and then sort ascending to get an ordered result 18 | messages_per_weekday = messages_per_weekday.map(x => { 19 | if (x.weekday.numeric === 0) { 20 | x.weekday.numeric = 7; 21 | } 22 | 23 | return x; 24 | }).sort((a, b) => a.weekday.numeric - b.weekday.numeric); 25 | 26 | log.push(messages_per_weekday); 27 | stat_bot.log(log); 28 | 29 | reply(stat_bot, message, messages_per_weekday); 30 | } catch (err) { 31 | console.log(err); 32 | } 33 | } 34 | 35 | function reply(stat_bot, message, messages_per_weekday) { 36 | let chat = message.chat.id; 37 | let bot = stat_bot.bot; 38 | let reply = new TextMessage(bot, chat, "de", "Markdown"); // TODO: Set reply language according to chat language 39 | 40 | let chart_data = { 41 | type: `${(messages_per_weekday.filter(x => x.count > 0).length === 7 ? "line" : "bar")}`, 42 | data: { 43 | labels: [...messages_per_weekday.map(x => x.weekday.readable)], 44 | datasets: [{ 45 | label: "total message percentage", 46 | data: messages_per_weekday.map(x => x.count) 47 | }] 48 | }, 49 | options: { 50 | plugins: { 51 | legend: false, 52 | outlabels: { 53 | text: "%l %p", 54 | color: "white", 55 | stretch: 35, 56 | font: { 57 | resizable: true, 58 | minSize: 12, 59 | maxSize: 18 60 | } 61 | } 62 | } 63 | } 64 | }; 65 | 66 | reply.addLineTranslated("messages_per_weekday"); 67 | 68 | reply.addChart(chart_data); 69 | 70 | messages_per_weekday.forEach((x) => { 71 | reply.addLine(`${x.weekday.readable}: \`${x.count}\` messages (\`${x.percentage}%\`)`); 72 | }); 73 | 74 | reply.send(); 75 | } -------------------------------------------------------------------------------- /src/setupFunctions.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import mongoose from "mongoose"; 3 | import express from "express"; 4 | import bodyParser from "body-parser"; 5 | import getPort from "get-port"; 6 | import colors from "colors/safe"; 7 | 8 | // Import internal packages 9 | import router from "./routes/index.route"; 10 | import StatBot from "./bot/statBot"; 11 | import config from "../config.json"; 12 | 13 | export async function setup_database() { 14 | return new Promise((resolve) => { 15 | mongoose.connect("mongodb://localhost/statbottest", { 16 | useNewUrlParser: true, 17 | useFindAndModify: true, 18 | useCreateIndex: true 19 | }); 20 | 21 | var db = mongoose.connection; 22 | 23 | db.on("error", (error) => { 24 | if (error.name === "MongoNetworkError") { 25 | console.log(`${colors.red("❌")} Couldn't set up database. Connection didn't succeed.`); 26 | resolve({ success: false }); 27 | } else { 28 | console.error.bind(console, "connection error:"); 29 | } 30 | }); 31 | 32 | db.once("open", function () { 33 | console.log(`${colors.green("✔")} connected to database`); 34 | resolve({ success: true }); 35 | }); 36 | }); 37 | } 38 | 39 | export async function setup_backend() { 40 | const port = config.port || await getPort(); 41 | 42 | return new Promise(resolve => { 43 | const app = express(); 44 | app.use(router); 45 | app.use(bodyParser.urlencoded({ extended: true })); 46 | app.use(bodyParser.json()); 47 | 48 | app.listen(port, () => { 49 | console.log(`${colors.green("✔")} server running on port ${port}`); 50 | resolve({ success: true, port: port }); 51 | }).on("error", error => { 52 | if (error.code === "EADDRINUSE") { 53 | console.log(`${colors.red("❌")} Couldn't set up backend on port ${port}. Port already in use.`); 54 | resolve({ success: false, port: port }); 55 | } else { 56 | console.error("error", error.code); 57 | } 58 | }); 59 | 60 | }); 61 | } 62 | 63 | export async function setup_bot(backend_port) { 64 | const stat_bot = new StatBot(config.telegram_bot_token, config.own_id, backend_port); 65 | 66 | try { 67 | await stat_bot.try_start(); 68 | stat_bot.register_handlers(); 69 | console.log(`${colors.green("✔")} Stat bot started and initialized successfully`); 70 | return { success: true }; 71 | } catch (error) { 72 | console.log(`${colors.red("❌")} An error occurred while setting up the telegram bot. Is your token invalid or already in use?`); 73 | return { success: false }; 74 | } 75 | } -------------------------------------------------------------------------------- /src/bot/handlers/onNewChatMembers.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import request from "request-promise"; 3 | 4 | // Import internal packages 5 | import TextMessage from "../messages/textMessage"; 6 | import DocumentMessage from "../messages/documentMessage"; 7 | import MessageCollection from "../messages/messageCollection"; 8 | import config from "../../../config.json"; 9 | 10 | export default async function on_new_chat_members(message, stat_bot) { 11 | if (message.new_chat_members.some(x => x.id == stat_bot.own_id)) { 12 | self_enter_chat(message, stat_bot); 13 | } else if (message.new_chat_members.some(x => !x.is_bot)) { 14 | await user_enter_chat(message, stat_bot); 15 | } 16 | } 17 | 18 | function self_enter_chat(message, stat_bot) { 19 | let chat = message.chat.id; 20 | let bot = stat_bot.bot; 21 | 22 | let gif = new DocumentMessage(bot, chat, "CgADBAADfQEAAprc5VLEBzPUauGO4hYE"); 23 | 24 | let introduction = new TextMessage(bot, chat, config.language_default, "Markdown"); 25 | introduction.addLineTranslated("introduction_group", { group_name: message.chat.title }); 26 | 27 | let ask_for_consent = new TextMessage(bot, chat, config.language_default); 28 | ask_for_consent.addLineTranslated("ask_for_consent_multiple_members"); 29 | 30 | let replies = new MessageCollection([ 31 | gif, 32 | introduction, 33 | ask_for_consent 34 | ]); 35 | 36 | replies.sendAll(); 37 | } 38 | 39 | async function user_enter_chat(message, stat_bot) { 40 | let chat = message.chat.id; 41 | let bot = stat_bot.bot; 42 | 43 | let human_new_members = message.new_chat_members.filter(x => x.is_bot); 44 | 45 | human_new_members = await Promise.all(human_new_members.map(async (item) => { 46 | let response = await request({ 47 | uri: `http://localhost:${stat_bot.backend_port}/users/${item.id}`, 48 | json: true 49 | }); 50 | 51 | return { 52 | ...item, 53 | user_already_in_db: response.result.length > 0 54 | }; 55 | })); 56 | 57 | let introduction = new TextMessage(bot, chat, config.language_default, "Markdown"); 58 | 59 | if (human_new_members.every(x => x.user_already_in_db)) { 60 | if (human_new_members.length > 1) { 61 | introduction.addLineTranslated("introduction_known_members", { new_members: human_new_members.map(x => stat_bot.get_user_address(x)).join(", ") }); 62 | } else { 63 | introduction.addLineTranslated("introduction_known_member", { new_members: human_new_members.map(x => stat_bot.get_user_address(x)).join(", ") }); 64 | } 65 | } else if (human_new_members.some(x => x.user_already_in_db)) { 66 | introduction.addLineTranslated("introduction_some_known_members", { new_members: human_new_members.map(x => stat_bot.get_user_address(x)).join(", ") }); 67 | } else { 68 | introduction.addLineTranslated("introduction_unknown_members", { new_members: human_new_members.map(x => stat_bot.get_user_address(x)).join(", ") }); 69 | } 70 | 71 | let ask_for_consent = new TextMessage(bot, chat, config.language_default, "Markdown"); 72 | ask_for_consent.addLineTranslated(human_new_members.length > 1 ? "ask_for_consent_multiple_members" : "ask_for_consent_single_member"); 73 | 74 | let replies = new MessageCollection([ 75 | introduction, 76 | ask_for_consent 77 | ]); 78 | 79 | replies.sendAll(); 80 | } -------------------------------------------------------------------------------- /src/bot/statBot.js: -------------------------------------------------------------------------------- 1 | // Import external packages 2 | import TelegramBot from "node-telegram-bot-api"; 3 | 4 | // Import bot command and handler fundtions 5 | import chat_info from "./commands/chatInfo"; 6 | import consent from "./commands/consent"; 7 | import messages_per_weekday from "./commands/messagesPerWeekday"; 8 | import messages_per_user from "./commands/messagesPerUser"; 9 | import my_data from "./commands/myData"; 10 | import on_message from "./handlers/onMessage"; 11 | import on_new_chat_members from "./handlers/onNewChatMembers"; 12 | import total_messages from "./commands/totalMessages"; 13 | import total_messages_extended from "./commands/totalMessagesExtended"; 14 | import word_count from "./commands/wordCount"; 15 | 16 | export default class StatBot { 17 | constructor(token, own_id, backend_port) { 18 | this.bot = new TelegramBot(token, { autoStart: false }); 19 | this.own_id = own_id; 20 | this.backend_port = backend_port; 21 | } 22 | 23 | async try_start() { 24 | try { 25 | let update = await this.bot.getUpdates(); 26 | await this.bot.processUpdate(update); 27 | await this.bot.startPolling(); 28 | this.info = await this.bot.getMe(); 29 | } catch (error) { 30 | throw new Error(401); 31 | } 32 | } 33 | 34 | command_regex(command_text) { 35 | return new RegExp(`^/${command_text}(@${this.info.username})?$`, "i"); 36 | } 37 | 38 | register_handlers() { 39 | this.bot.on("message", async (message, metadata) => await on_message(message, metadata, this)); 40 | this.bot.on("new_chat_members", async message => await on_new_chat_members(message, this)); 41 | 42 | this.bot.onText(this.command_regex("consent_deny"), async message => await consent(message, "deny", this)); 43 | this.bot.onText(this.command_regex("consent_restricted"), async message => await consent(message, "restricted", this)); 44 | this.bot.onText(this.command_regex("consent_full"), async message => await consent(message, "full", this)); 45 | this.bot.onText(this.command_regex("chat_info"), async message => await chat_info(message, this)); 46 | this.bot.onText(this.command_regex("messages_per_weekday"), async message => await messages_per_weekday(message, this)); 47 | this.bot.onText(this.command_regex("messages_per_user"), async message => await messages_per_user(message, this)); 48 | this.bot.onText(this.command_regex("my_data"), async message => await my_data(message, this)); 49 | // TODO: special text in case there are zero messages yet (e.g. if nobody gave consent yet) 50 | this.bot.onText(this.command_regex("total_messages"), async message => await total_messages(message, this)); 51 | this.bot.onText(this.command_regex("total_messages_extended"), async message => await total_messages_extended(message, this)); 52 | this.bot.onText(this.command_regex("word_count"), async message => await word_count(message, this)); 53 | } 54 | 55 | get_user_address(user) { 56 | if (user.first_name) { 57 | return `${user.first_name} ${user.last_name || ""}`.trim(); 58 | } else if (user.username) { 59 | return `@${user.username.trim()}`; 60 | } else { 61 | return ""; 62 | } 63 | } 64 | 65 | log(lines) { 66 | console.log(); 67 | console.log("-------------------------------------------------------------"); 68 | console.log(); 69 | lines.forEach(x => console.log(x)); 70 | } 71 | } -------------------------------------------------------------------------------- /scripts/setupConfig.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const inquirer = require("inquirer"); 3 | const fs = require("fs"); 4 | const colors = require("colors/safe"); 5 | 6 | let config_available = fs.existsSync("./config.json"); 7 | 8 | let config = {}; 9 | 10 | if (!config_available) { 11 | inquirer.prompt([{ 12 | type: "confirm", 13 | name: "manual_config", 14 | message: "Seems like you haven't created a config file for the stat bot yet. We can do that together now if you like!", 15 | default: true 16 | }, { 17 | type: "password", 18 | name: "telegram_bot_token", 19 | message: "What is your telegram bot api token? ", 20 | mask: "*", 21 | when: answers => answers["manual_config"] 22 | }, { 23 | type: "list", 24 | name: "language_default", 25 | message: "What language should your bot speak by default? ", 26 | choices: ["en", "de"], 27 | when: answers => answers["manual_config"] 28 | }, { 29 | type: "list", 30 | name: "port_selection", 31 | message: "On which port should the backend operate? ", 32 | choices: ["Always choose a random free one by yourself at startup", "I want to choose my own fixed port"], 33 | when: answers => answers["manual_config"] 34 | }, { 35 | type: "number", 36 | name: "port", 37 | message: "Please enter your port number: ", 38 | when: answers => answers["port_selection"] === "I want to choose my own fixed port" 39 | }, { 40 | type: "list", 41 | name: "white_black_listing", 42 | message: "Do you want to control from which chats messages are stored? ", 43 | choices: ["Yes, I want a whitelist", "Yes, I want a blacklist", "No, save messages from all chats"], 44 | when: answers => answers["manual_config"] 45 | }, { 46 | type: "input", 47 | name: "listed_chats", 48 | message: answers => { 49 | let list_type = answers["white_black_listing"].substring(answers["white_black_listing"].lastIndexOf(" ") + 1, answers["white_black_listing"].length); 50 | return `Please enter the chat you want to include in your ${list_type} as numbers separated by commas. Spaces will be ignored.`; 51 | }, 52 | when: answers => answers["white_black_listing"].startsWith("Yes") 53 | }]).then(answers => { 54 | if (answers["manual_config"]) { 55 | config.telegram_bot_token = answers["telegram_bot_token"]; 56 | config.own_id = +config.telegram_bot_token.substring(0, config.telegram_bot_token.indexOf(":")); 57 | config.language_default = answers["language_default"]; 58 | 59 | if (answers["port_selection"] === "I want to choose my own fixed port") { 60 | config.port = answers["port"]; 61 | } 62 | 63 | if (answers["white_black_listing"].startsWith("Yes")) { 64 | let list_type = answers["white_black_listing"].substring(answers["white_black_listing"].lastIndexOf(" ") + 1, answers["white_black_listing"].length); 65 | config[list_type] = answers["listed_chats"].split(",").map(x => +(x.trim())); 66 | } 67 | } else { 68 | config.telegram_bot_token = ""; 69 | config.own_id = ""; 70 | config.language_default = ""; 71 | console.log("OK, got it. Creating an empty configuration file for you to edit."); 72 | } 73 | 74 | fs.appendFile("./config.json", JSON.stringify(config, null, 4), () => { 75 | console.log(colors.green("Configuration file successfully created!")); 76 | }); 77 | }); 78 | } else { 79 | console.log(colors.green("Configuration file found. Starting server.")); 80 | } -------------------------------------------------------------------------------- /src/controllers/message.controller.js: -------------------------------------------------------------------------------- 1 | import config from "../../config.json"; 2 | import { get_raw_translation } from "../bot/translate"; 3 | import all_texts from "../databaseAccess/getAllTexts.db"; 4 | import consent_level from "../databaseAccess/getConsentLevel.db"; 5 | import messages_by_hour from "../databaseAccess/getMessagesByHour.db"; 6 | import messages_by_user from "../databaseAccess/getMessagesByUser.db"; 7 | import messages_by_weekday from "../databaseAccess/getMessagesByWeekday.db"; 8 | import { message_total, message_total_extended } from "../databaseAccess/getMessageTotal.db"; 9 | import save_message from "../databaseAccess/postMessage.db"; 10 | import upsert_chat from "../databaseAccess/upsertChat.db"; 11 | import upsert_user from "../databaseAccess/upsertUser.db"; 12 | 13 | 14 | export async function get_message_total(chat_id) { 15 | return await message_total(chat_id); 16 | } 17 | 18 | export async function get_message_total_extended(chat_id) { 19 | return await message_total_extended(chat_id); 20 | } 21 | 22 | export async function get_messages_by_user(chat_id) { 23 | return await messages_by_user(chat_id); 24 | } 25 | 26 | export async function get_messages_by_user_extended(chat_id) { 27 | console.log(`Get messages by user extended for ${chat_id} not implemented`); 28 | // TODO: implement extended function 29 | } 30 | 31 | export async function get_messages_by_weekday(chat_id) { 32 | try { 33 | let messagesByWeekday = await messages_by_weekday(chat_id); 34 | 35 | let total_messages = messagesByWeekday.map(x => x.count).reduce((sum, value) => sum + value); 36 | 37 | for (let i = 0; i < messagesByWeekday.length; i++) { 38 | messagesByWeekday[i].percentage = +((messagesByWeekday[i].count / total_messages) * 100).toFixed(2); 39 | // messagesByWeekday[i]._id.weekday -= 1; 40 | } 41 | 42 | let result = []; 43 | 44 | for (let i = 0; i < 7; i++) { 45 | if (messagesByWeekday.some(x => x._id.weekday - 1 === i)) { 46 | let x = messagesByWeekday.filter(y => y._id.weekday - 1 === i)[0]; 47 | result.push({ 48 | weekday: { 49 | numeric: x._id.weekday - 1, 50 | readable: get_raw_translation("weekdays", config.language_default)[x._id.weekday - 1] 51 | }, 52 | count: x.count, 53 | percentage: x.percentage 54 | }); 55 | } else { 56 | result.push({ 57 | weekday: { 58 | numeric: i, 59 | readable: get_raw_translation("weekdays", config.language_default)[i] 60 | }, 61 | count: 0, 62 | percentage: 0 63 | }); 64 | } 65 | } 66 | 67 | return result; 68 | } catch (error) { 69 | throw new Error(error); 70 | } 71 | } 72 | 73 | export async function get_messages_by_hour(chat_id) { 74 | try { 75 | let db_result = await messages_by_hour(chat_id); 76 | let return_value = db_result.map(x => { 77 | return { 78 | hour: x._id.hour, 79 | count: x.count 80 | }; 81 | }); 82 | 83 | return return_value; 84 | } catch (error) { 85 | throw new Error(error); 86 | } 87 | } 88 | 89 | export async function get_word_count(chat_id) { 90 | try { 91 | let db_result = await all_texts(chat_id); 92 | 93 | let word_counts = db_result.map(x => x.text.split(" ").length); 94 | let word_sum = word_counts.reduce((x, y) => x + y, 0); 95 | let words_per_message = +(word_sum / db_result.length).toFixed(2); 96 | 97 | return { 98 | total: word_sum, 99 | avgPerMessage: words_per_message 100 | }; 101 | } catch (error) { 102 | throw new Error(error); 103 | } 104 | } 105 | 106 | export async function post_message(message, metadata) { 107 | try { 108 | message.message_type = metadata.type === "document" ? (message.animation ? "gif" : "document") : metadata.type; 109 | 110 | let save = true; 111 | 112 | if (config.blacklist && config.blacklist.includes(message.chat.id)) save = false; 113 | if (config.whitelist && !config.whitelist.includes(message.chat.id)) save = false; 114 | 115 | if (save) { 116 | let consent = await consent_level(message.from); 117 | 118 | console.log(consent); 119 | if (consent !== "deny") { 120 | await upsert_missing_documents(message); 121 | let preparedMessage = prepareMessageForDb(message, consent_level); 122 | 123 | console.log(); 124 | console.log("-------------------------------------------------------------"); 125 | console.log(); 126 | console.log("Received message from bot:"); 127 | console.log(preparedMessage); 128 | 129 | await save_message(preparedMessage); 130 | } 131 | } 132 | } catch (error) { 133 | throw new Error(error); 134 | } 135 | } 136 | 137 | function prepareMessageForDb(message, consent_level) { 138 | let preparedMessage = Object.assign({}, message); 139 | 140 | preparedMessage.chat.chat_type = message.chat.type; 141 | delete preparedMessage.chat.type; 142 | 143 | // Convert the unix timestamp from the telegram message object to an actual date 144 | if (message.date) preparedMessage.date = new Date(message.date * 1000); 145 | if (message.reply_to_message) preparedMessage.reply_to_message = message.reply_to_message.message_id; 146 | 147 | if (message.entities && message.entities.length === 0) delete preparedMessage.entities; 148 | if (message.caption_entities && message.caption_entities.length === 0) delete preparedMessage.caption_entities; 149 | if (message.photo && message.photo.length === 0) delete preparedMessage.photo; 150 | if (message.new_chat_members && message.new_chat_members.length === 0) delete preparedMessage.new_chat_members; 151 | if (message.new_chat_photo && message.new_chat_photo.length === 0) delete preparedMessage.new_chat_photo; 152 | if (message.game && message.game.text_entities && message.game.text_entities.length === 0) delete preparedMessage.game.text_entities; 153 | if (message.game && message.game.photo && message.game.photo.length === 0) delete preparedMessage.game.photo; 154 | 155 | if (consent_level === "restricted") { 156 | delete preparedMessage.text; 157 | 158 | if (message.audio) { 159 | delete preparedMessage.audio.thumb.file_id; 160 | delete preparedMessage.audio.file_id; 161 | } 162 | if (message.document) { 163 | delete preparedMessage.document.thumb.file_id; 164 | delete preparedMessage.document.file_id; 165 | } 166 | if (message.animation) { 167 | delete preparedMessage.animation.thumb.file_id; 168 | delete preparedMessage.animation.file_id; 169 | } 170 | if (message.photo) { 171 | delete preparedMessage.photo.thumb.file_id; 172 | delete preparedMessage.photo.file_id; 173 | } 174 | if (message.sticker) { 175 | delete preparedMessage.sticker.thumb.file_id; 176 | delete preparedMessage.sticker.file_id; 177 | } 178 | if (message.video) { 179 | delete preparedMessage.video.thumb.file_id; 180 | delete preparedMessage.video.file_id; 181 | } 182 | if (message.voice) { 183 | delete preparedMessage.voice.thumb.file_id; 184 | delete preparedMessage.voice.file_id; 185 | } 186 | if (message.video_note) { 187 | delete preparedMessage.video_note.thumb.file_id; 188 | delete preparedMessage.video_note.file_id; 189 | } 190 | if (message.caption) { 191 | delete preparedMessage.caption.thumb.file_id; 192 | delete preparedMessage.caption.file_id; 193 | } 194 | if (message.contact) { 195 | delete preparedMessage.contact.thumb.file_id; 196 | delete preparedMessage.contact.file_id; 197 | } 198 | if (message.location) { 199 | delete preparedMessage.location.thumb.file_id; 200 | delete preparedMessage.location.file_id; 201 | } 202 | if (message.venue) { 203 | delete preparedMessage.venue.thumb.file_id; 204 | delete preparedMessage.venue.file_id; 205 | } 206 | if (message.poll) { 207 | delete preparedMessage.poll.thumb.file_id; 208 | delete preparedMessage.poll.file_id; 209 | } 210 | } 211 | 212 | return preparedMessage; 213 | } 214 | 215 | async function upsert_missing_documents(message) { 216 | if (message.chat) upsert_chat(message.chat); 217 | if (message.forward_from_chat) upsert_chat(message.forward_from_chat); 218 | 219 | if (message.from) await upsert_user(message.from); 220 | if (message.new_chat_members) message.new_chat_members.forEach(async user => await upsert_user(user)); 221 | } -------------------------------------------------------------------------------- /src/bot/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "bot_replies": { 3 | "total_messages": { 4 | "de": [ 5 | "Seit ich hier bin wurden insgesamt `p[total_messages]` Nachrichten geschrieben." 6 | ], 7 | "en": [ 8 | "Since I'm here, `p[total_messages]` messages have been sent in total." 9 | ] 10 | }, 11 | "total_messages_extended": { 12 | "de": [ 13 | "Seit ich hier bin wurden insgesamt `p[total_messages]` Nachrichten geschrieben. Darunter waren" 14 | ], 15 | "en": [ 16 | "Since I'm here, `p[total_messages]` messages have been sent in total. These include" 17 | ] 18 | }, 19 | "messages_per_user": { 20 | "de": [ 21 | "Wer schreibt in der Gruppe wie viel?" 22 | ], 23 | "en": [ 24 | "Who writes how much in the group?" 25 | ] 26 | }, 27 | "messages_per_weekday": { 28 | "de": [ 29 | "An welchem Wochentag wird am meisten geschrieben?" 30 | ], 31 | "en": [ 32 | "Which day of the week is the busiest?" 33 | ] 34 | }, 35 | "introduction_group": { 36 | "de": [ 37 | "Schön, dass ich bei euch sein darf, p[group_name] 👍", 38 | "", 39 | "Ich bin Bob der Statistiker und wenn ihr wollt, sammle ich für euch eure Nachrichten und erstelle interessante Statistiken und Grafiken daraus." 40 | ], 41 | "en": [ 42 | "Glad that I can be with you, p[group_name] 👍", 43 | "", 44 | "I'm Bob the Statistician and if you like, I collect your messages and create interesting statistics and charts for you." 45 | ] 46 | }, 47 | "introduction_unknown_members": { 48 | "de": [ 49 | "Hallo, p[new_members]! 😊", 50 | "Ein herzliches Willkommen in der Gruppe von mir. Ich bin Bob der Statistiker 👋", 51 | "Ich sammle hier Nachrichten und erstelle interessante Statistiken und Grafiken für euch daraus." 52 | ], 53 | "en": [ 54 | "Hello, p[new_members]! 😊", 55 | "Warm welcome to the group from me. I'm Bob the statistician 👋", 56 | "I collect messages and create interesting graphs and statistics for you." 57 | ] 58 | }, 59 | "introduction_some_known_members": { 60 | "de": [ 61 | "Hallo, p[new_members]! 😊", 62 | "Ein herzliches Willkommen in der Gruppe von mir. Einige von euch kennen mich ja schon! Freut mich, euch hier wiederzusehen 👋", 63 | "Für die unbekannten Gesichter: Ich bin Bob der Statistiker.", 64 | "Ich sammle Nachrichten und erstelle interessante Statistiken und Grafiken für euch daraus." 65 | ], 66 | "en": [ 67 | "Hello, p[new_members]! 😊", 68 | "Warm welcome to the group from me. I see some of you aready know me! Glad to meet you again 👋", 69 | "To all the unknown faces: I'm Bob the statistician.", 70 | "I collect messages and create interesting graphs and statistics for you." 71 | ] 72 | }, 73 | "introduction_known_members": { 74 | "de": [ 75 | "Hallo, p[new_members]! 😊", 76 | "Ein herzliches Willkommen in der Gruppe von mir. Ich hab euch ja alle schon mal getroffen! Freut mich, euch hier wiederzusehen 👋", 77 | "Ihr kennt ja das Procedere: Ich sammle Nachrichten und erstelle interessante Statistiken und Grafiken für euch daraus." 78 | ], 79 | "en": [ 80 | "Hello, p[new_members]! 😊", 81 | "Warm welcome to the group from me. I believe I already met all of you before! Glad to meet you again 👋", 82 | "You know the drill: I collect messages and create interesting graphs and statistics for you." 83 | ] 84 | }, 85 | "introduction_known_member": { 86 | "de": [ 87 | "Hallo, p[new_members]! 😊", 88 | "Ein herzliches Willkommen in der Gruppe von mir. Wir haben uns ja schon mal gesehen. Freut mich, dich hier wieder zu treffen 👋", 89 | "Du kennst ja das Procedere: Ich sammle Nachrichten und erstelle interessante Statistiken und Grafiken für euch daraus." 90 | ], 91 | "en": [ 92 | "Hello, p[new_members]! 😊", 93 | "Warm welcome to the group from me. I believe we've met before. Glad to meet you again 👋", 94 | "You know the drill: I collect messages and create interesting graphs and statistics for you." 95 | ] 96 | }, 97 | "ask_for_consent_multiple_members": { 98 | "de": [ 99 | "Ich kann natürlich nicht einfach so ohne Erlaubnis all eure persönlichen Daten speichern - dafür brauche ich zuerst eure Zustimmung 😇.", 100 | "Wenn ihr damit einverstanden seid, dass eure Nachrichten inklusive Inhalt gespeichert werden, sendet bitte kurz `/consent_full` in den Chat, damit ich anfangen kann, zu arbeiten!", 101 | "", 102 | "Wenn ihr damit einverstanden seid, dass die Anzahl und das Datum eurer Nachrichten gespeichert werden, der Inhalt aber nicht, dann sendet einfach `/consent_restricted` in den Chat und ich weiß auch darüber Bescheid.", 103 | "", 104 | "Wenn ihr komplett unsichtbar bleiben wollt, schreibt einfach nichts davon und ich werde eure Nachrichten ungelesen verwerfen. Versprochen! Seid euch jedoch bewusst, dass in diesem Fall die Statistiken für die gesamte Gruppe verfälscht werden könnten (z.B. Gesamtzahl der Nachrichten, oder Anteile der einzelnen Nutzer daran)." 105 | ], 106 | "en": [ 107 | "Of course I won't just collect your personal data without permission - I need your consent first 😇.", 108 | "If you're okay with me saving your messages (including content), just use the `/consent_full` command, so that I can get to work!", 109 | "", 110 | "If you only want me to save the timestamp and total count of your messages, but not the content, then use the `/consent_restricted` instead so I know what to do.", 111 | "", 112 | "If you want to remain completely invisible to me, don't do anything and I will discard all messages from you. Promise! I can't guarantee that the group statistics might not be a bit skewed then (e.g. the total message count or message quotas per user)." 113 | ] 114 | }, 115 | "ask_for_consent_single_member": { 116 | "de": [ 117 | "Ich kann natürlich nicht einfach so ohne Erlaubnis all deine persönlichen Daten speichern - dafür brauche ich zuerst deine Zustimmung 😇.", 118 | "Wenn du damit einverstanden bist, dass deine Nachrichten inklusive Inhalt gespeichert werden, sende bitte kurz `/consent_full` in den Chat, damit ich anfangen kann, zu arbeiten!", 119 | "", 120 | "Wenn du damit einverstanden bist, dass die Anzahl und das Datum deiner Nachrichten gespeichert werden, der Inhalt aber nicht, dann sende einfach `/consent_restricted` in den Chat und ich weiß auch darüber Bescheid.", 121 | "", 122 | "Wenn du komplett unsichtbar bleiben willst, schreib einfach nichts davon und ich werde deine Nachrichten ungelesen verwerfen. Versprochen! Sei dir jedoch bewusst, dass in diesem Fall die Statistiken für die gesamte Gruppe verfälscht werden könnten (z.B. Gesamtzahl der Nachrichten, oder Anteile der einzelnen Nutzer daran)." 123 | ], 124 | "en": [ 125 | "Of course I won't just collect your personal data without permission - I need your consent first 😇.", 126 | "If you're okay with me saving your messages (including content), just use the `/consent_full` command, so that I can get to work!", 127 | "", 128 | "If you only want me to save the timestamp and total count of your messages, but not the content, then use the `/consent_restricted` instead so I know what to do.", 129 | "", 130 | "If you want to remain completely invisible to me, don't do anything and I will discard all messages from you. Promise! I cant't guarantee that the group statistics might not be a bit skewed then (e.g. the total message count or message quotas per user)." 131 | ] 132 | }, 133 | "confirm_consent_update": { 134 | "de": [ 135 | "Alles klar, p[user_name]. Ich habe deine Einstellungen zum Daten sammeln von `p[previous_value]` auf `p[new_value]` geändert." 136 | ], 137 | "en": [ 138 | "Alright, p[user_name]. I changed your consent level from `p[previous_value]` to `p[new_value]`." 139 | ] 140 | }, 141 | "invalid_consent_update": { 142 | "de": [ 143 | "Das ist leider kein gültiger Wer für deine Einstellungen. Zulässig sind", 144 | "· `full`", 145 | "· `restricted`", 146 | "· `deny`" 147 | ], 148 | "en": [ 149 | "I'm afraid that's not a valid option. You can use", 150 | "· `full`", 151 | "· `restricted`", 152 | "· `deny`" 153 | ] 154 | }, 155 | "personal_data_group": { 156 | "de": [ 157 | "Alles klar. Ich habe dir eine private Nachricht geschickt." 158 | ], 159 | "en": [ 160 | "Alright. I sent you a private message." 161 | ] 162 | }, 163 | "personal_data_private": { 164 | "de": [ 165 | "Hey, *p[user_address]*!", 166 | "Du hast in der Gruppe *p[group_name]* deine persönlichen Daten angefragt. Hier ist ein Überblick darüber, was ich über dich gespeichert habe:", 167 | "", 168 | "· `p[message_count]` Nachrichten aus `p[chat_count]` Chats.", 169 | "· Vorname: `p[first_name]`", 170 | "· Nachname: `p[last_name]`", 171 | "· Nutzername: `p[username]`", 172 | "· Telegram ID: `p[telegram_id]`", 173 | "· Datensammelerlaubnis: `p[consent_level]`", 174 | "· Sprache: `p[language_code]`" 175 | ], 176 | "en": [ 177 | "Hey, *p[user_address]*!", 178 | "You asked for an overview of your personal data in *p[group_name]*. Here's what I got about you:", 179 | "", 180 | "· `p[message_count]` messages from `p[chat_count]` chats.", 181 | "· First name: `p[first_name]`", 182 | "· Last name: `p[last_name]`", 183 | "· Username: `p[username]`", 184 | "· Telegram ID: `p[telegram_id]`", 185 | "· Data collection permissions: `p[consent_level]`", 186 | "· Language: `p[language_code]`" 187 | ] 188 | }, 189 | "group_chat_data": { 190 | "de": [ 191 | "Dies ist ein Gruppenchat.", 192 | "· ID: `p[id]`", 193 | "· Name: `p[title]`", 194 | "· Alle Mitglieder sind Admins: `p[all_members_are_administrators]`" 195 | ], 196 | "en": [ 197 | "This is a group chat.", 198 | "· ID: `p[id]`", 199 | "· Name: `p[title]`", 200 | "· All members are admins: `p[all_members_are_administrators]`" 201 | ] 202 | }, 203 | "private_chat_data": { 204 | "de": [ 205 | "Dies ist ein privater Chat", 206 | "· ID: `p[id]`", 207 | "· Vorname: `p[first_name]`", 208 | "· Nachname: `p[last_name]`", 209 | "· Nutzername: `p[username]`" 210 | ], 211 | "en": [ 212 | "This is a private chat", 213 | "· ID: `p[id]`", 214 | "· First name: `p[first_name]`", 215 | "· Last name: `p[last_name]`", 216 | "· Username: `p[username]`" 217 | ] 218 | }, 219 | "word_count": { 220 | "de": [ 221 | "Insgesamt wurde in diesem chat schon `p[total_words]` Worte geschrieben. Das sind durchschnittlich `p[avgPerMessage]` pro Nachricht." 222 | ], 223 | "en": [ 224 | "A total of `p[total_words]` words have been sent in this chat so far. That's an average of `p[avgPerMessage]` per message." 225 | ] 226 | }, 227 | "weekdays": { 228 | "de": [ 229 | "Sonntag", 230 | "Montag", 231 | "Dienstag", 232 | "Mittwoch", 233 | "Donnerstag", 234 | "Freitag", 235 | "Samstag" 236 | ], 237 | "en": [ 238 | "Sunday", 239 | "Monday", 240 | "Tuesday", 241 | "Wednesday", 242 | "Thursday", 243 | "Friday", 244 | "Saturday" 245 | ] 246 | } 247 | }, 248 | "message_types": { 249 | "text": { 250 | "de": { 251 | "single": "Textnachricht", 252 | "many": "Textnachrichten" 253 | }, 254 | "en": { 255 | "single": "Text", 256 | "many": "Texts" 257 | } 258 | }, 259 | "audio": { 260 | "de": { 261 | "single": "Audio", 262 | "many": "Audios" 263 | }, 264 | "en": { 265 | "single": "Audio", 266 | "many": "Audios" 267 | } 268 | }, 269 | "document": { 270 | "de": { 271 | "single": "Dokument", 272 | "many": "Dokumente" 273 | }, 274 | "en": { 275 | "single": "Document", 276 | "many": "Documents" 277 | } 278 | }, 279 | "photo": { 280 | "de": { 281 | "single": "Foto", 282 | "many": "Fotos" 283 | }, 284 | "en": { 285 | "single": "Photo", 286 | "many": "Photos" 287 | } 288 | }, 289 | "sticker": { 290 | "de": { 291 | "single": "Sticker", 292 | "many": "Sticker" 293 | }, 294 | "en": { 295 | "single": "Sticker", 296 | "many": "Stickers" 297 | } 298 | }, 299 | "video": { 300 | "de": { 301 | "single": "Video", 302 | "many": "Videos" 303 | }, 304 | "en": { 305 | "single": "Video", 306 | "many": "Videos" 307 | } 308 | }, 309 | "voice": { 310 | "de": { 311 | "single": "Sprachnachricht", 312 | "many": "Sprachnachrichten" 313 | }, 314 | "en": { 315 | "single": "Voice note", 316 | "many": "Voice notes" 317 | } 318 | }, 319 | "contact": { 320 | "de": { 321 | "single": "Kontakt", 322 | "many": "Kontakte" 323 | }, 324 | "en": { 325 | "single": "Contact", 326 | "many": "Contacts" 327 | } 328 | }, 329 | "location": { 330 | "de": { 331 | "single": "Standort", 332 | "many": "Standorte" 333 | }, 334 | "en": { 335 | "single": "Location", 336 | "many": "Locations" 337 | } 338 | }, 339 | "new_chat_members": { 340 | "de": { 341 | "single": "Neues Chatmitglied", 342 | "many": "Neue Chatmitglieder" 343 | }, 344 | "en": { 345 | "single": "New chat member", 346 | "many": "New chat members" 347 | } 348 | }, 349 | "left_chat_member": { 350 | "de": { 351 | "single": "Entferntes/gegangenes Mitglied", 352 | "many": "Entfernte/gegangene Mitglieder" 353 | }, 354 | "en": { 355 | "single": "Removed/left member", 356 | "many": "Removed/left members" 357 | } 358 | }, 359 | "new_chat_title": { 360 | "de": { 361 | "single": "Neuer Chatname", 362 | "many": "Neue Chatnamen" 363 | }, 364 | "en": { 365 | "single": "New chat title", 366 | "many": "New chat titles" 367 | } 368 | }, 369 | "new_chat_photo": { 370 | "de": { 371 | "single": "Neues Chatbild", 372 | "many": "Neue Chatbilder" 373 | }, 374 | "en": { 375 | "single": "New chat photo", 376 | "many": "New chat photos" 377 | } 378 | }, 379 | "delete_chat_photo": { 380 | "de": { 381 | "single": "Gelöschtes Chatbild", 382 | "many": "Gelöschte Chatbilder" 383 | }, 384 | "en": { 385 | "single": "Deleted chat photo", 386 | "many": "Deleted chat photos" 387 | } 388 | }, 389 | "group_chat_created": { 390 | "de": { 391 | "single": "Gruppenchat erstellt", 392 | "many": "Gruppenchats erstellt" 393 | }, 394 | "en": { 395 | "single": "Group chat created", 396 | "many": "Group chats created" 397 | } 398 | }, 399 | "game": { 400 | "de": { 401 | "single": "Nachricht aus Spielen", 402 | "many": "Nachrichten aus Spielen" 403 | }, 404 | "en": { 405 | "single": "Message from games", 406 | "many": "Messages from games" 407 | } 408 | }, 409 | "pinned_message": { 410 | "de": { 411 | "single": "Angepinnte Nachricht", 412 | "many": "Angepinnte Nachrichten" 413 | }, 414 | "en": { 415 | "single": "Pinned message", 416 | "many": "Pinnes messages" 417 | } 418 | }, 419 | "poll": { 420 | "de": { 421 | "single": "Umfrage", 422 | "many": "Umfragen" 423 | }, 424 | "en": { 425 | "single": "Poll", 426 | "many": "Polls" 427 | } 428 | }, 429 | "migrate_from_chat_id": { 430 | "de": { 431 | "single": "Migrierung aus einem Chat", 432 | "many": "Migrierungen aus einem Chat" 433 | }, 434 | "en": { 435 | "single": "Migration from a chat", 436 | "many": "Migrations from a chat" 437 | } 438 | }, 439 | "migrate_to_chat_id": { 440 | "de": { 441 | "single": "Migrierung in einen Chat", 442 | "many": "Migrierungen in einen Chat" 443 | }, 444 | "en": { 445 | "single": "Migration in a chat", 446 | "many": "Migrations in a chat" 447 | } 448 | }, 449 | "channel_chat_created": { 450 | "de": { 451 | "single": "Kanal erstellt", 452 | "many": "Kanäle erstellt" 453 | }, 454 | "en": { 455 | "single": "Channel created", 456 | "many": "Channels created" 457 | } 458 | }, 459 | "supergroup_chat_created": { 460 | "de": { 461 | "single": "Supergroup erstellt", 462 | "many": "Supergroups erstellt" 463 | }, 464 | "en": { 465 | "single": "Supergroup created", 466 | "many": "Supergroups created" 467 | } 468 | }, 469 | "successfull_payment": { 470 | "de": { 471 | "single": "Erfolgreicher Bezahlvorgang", 472 | "many": "Erfolgreiche Bezahlvorgänge" 473 | }, 474 | "en": { 475 | "single": "Successfull payment", 476 | "many": "Successfull payments" 477 | } 478 | }, 479 | "invoice": { 480 | "de": { 481 | "single": "Rechnung", 482 | "many": "Rechnungen" 483 | }, 484 | "en": { 485 | "single": "Invoice", 486 | "many": "Invoices" 487 | } 488 | }, 489 | "video_note": { 490 | "de": { 491 | "single": "Videonotiz", 492 | "many": "Videonotizen" 493 | }, 494 | "en": { 495 | "single": "Video note", 496 | "many": "Video notes" 497 | } 498 | }, 499 | "gif": { 500 | "de": { 501 | "single": "Gif", 502 | "many": "Gifs" 503 | }, 504 | "en": { 505 | "single": "Gif", 506 | "many": "Gifs" 507 | } 508 | } 509 | } 510 | } --------------------------------------------------------------------------------