├── .vercelignore ├── .prettierignore ├── .env.example ├── src ├── functions │ ├── index.ts │ ├── refuse-message.ts │ ├── forward-message.ts │ └── forward-photo.ts ├── bot │ └── index.ts ├── commands │ ├── composer.ts │ └── text-only │ │ └── index.ts ├── types │ └── shared-interfaces.ts ├── utils │ ├── start.ts │ └── helper.ts ├── main.ts └── responses │ └── messages.ts ├── vercel.json ├── .prettierrc ├── postbuild-set-webhook.sh ├── tsconfig.json ├── api └── index.ts ├── LICENSE ├── package.json ├── README.md └── .gitignore /.vercelignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | public/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/ 2 | node_modules/ 3 | .env* -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 3 | CHAT_ID=-1001234567891 -------------------------------------------------------------------------------- /src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './forward-message'; 2 | export * from './refuse-message'; 3 | export * from './forward-photo'; 4 | -------------------------------------------------------------------------------- /src/bot/index.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'grammy'; 2 | 3 | const bot = new Bot(String(process.env.BOT_TOKEN)); 4 | 5 | export default bot; 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "routes": [ 4 | { 5 | "src": "/.*", 6 | "dest": "api/index.ts" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "organizeImportsSkipDestructiveCodeActions": true, 4 | "printWidth": 100, 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "all" 9 | } 10 | -------------------------------------------------------------------------------- /postbuild-set-webhook.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | # Chama a API do Telegram para configurar o webhook 4 | echo "Setting Webhook..." 5 | curl -X POST \ 6 | -H "Content-Type: application/json" \ 7 | -d "{\"url\": \"$VERCEL_URL/api/index\"}" \ 8 | https://api.telegram.org/bot$BOT_TOKEN/setWebhook -------------------------------------------------------------------------------- /src/commands/composer.ts: -------------------------------------------------------------------------------- 1 | import { Composer } from 'grammy'; 2 | import { help, start } from './text-only'; 3 | 4 | const composer = new Composer(); 5 | 6 | composer.command(['start'], start); 7 | 8 | composer.command(['help', 'ajuda'], help); 9 | 10 | export default composer; 11 | -------------------------------------------------------------------------------- /src/types/shared-interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface PutHashtagsResponse { 2 | jobOpportunity: string; 3 | jobLevel: string; 4 | jobLocal: string; 5 | jobTitle?: string; 6 | jobUrl?: string; 7 | limitDate: string; 8 | footer: string; 9 | encerrada: boolean; 10 | } 11 | 12 | export interface RetrieveContentResponse { 13 | jobTitle: string; 14 | body: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/text-only/index.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'grammy'; 2 | import { helpMessage, welcomeMessage } from '../../responses/messages'; 3 | 4 | export const start = async (ctx: Context) => { 5 | await ctx.reply(welcomeMessage, { parse_mode: 'HTML' }); 6 | }; 7 | export const help = async (ctx: Context): Promise => { 8 | await ctx.reply(helpMessage, { parse_mode: 'HTML' }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/functions/refuse-message.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'grammy'; 2 | import { wrongFormat } from '../responses/messages'; 3 | 4 | export const refuseMessage = async (ctx: Context) => { 5 | if (!ctx.msg) { 6 | return; 7 | } 8 | const messageId: number = ctx.msg.message_id; 9 | 10 | await ctx.react('👎') 11 | await ctx.reply(wrongFormat, { reply_to_message_id: messageId }); 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "lib": ["es2017", "es7", "es6"], 6 | "allowJs": true, 7 | "outDir": "./public", 8 | "strict": true, 9 | "baseUrl": ".", 10 | "esModuleInterop": true, 11 | "noImplicitAny": false, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/start.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { Bot, Context } from 'grammy'; 3 | 4 | const development = async (bot: Bot): Promise => { 5 | console.log(`Bot starting polling: ${process.env.BOT_TOKEN?.split(':')[0]}`); 6 | return bot.api 7 | .deleteWebhook() 8 | .then(async () => { 9 | console.log('Bot starting polling: Success'); 10 | await bot.start(); 11 | }) 12 | .catch(console.error); 13 | }; 14 | 15 | export { development }; 16 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | require('../src/main'); 2 | 3 | import { webhookCallback } from 'grammy'; 4 | import bot from '../src/bot'; 5 | 6 | function runMiddleware(req, res, fn) { 7 | return new Promise((resolve, reject) => { 8 | fn(req, res, result => { 9 | if (result instanceof Error) { 10 | return reject(result); 11 | } 12 | 13 | return resolve(result); 14 | }); 15 | }); 16 | } 17 | 18 | async function handler(req, res) { 19 | await runMiddleware(req, res, webhookCallback(bot, 'http')); 20 | } 21 | 22 | export default handler; 23 | -------------------------------------------------------------------------------- /src/functions/forward-message.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'grammy'; 2 | import { modEntities } from '../utils/helper'; 3 | import bot from '../bot'; 4 | import { basicAnswer } from '../responses/messages'; 5 | 6 | export const forwardMessage = async (ctx: Context) => { 7 | if (!ctx.msg) { 8 | return; 9 | } 10 | const text = ctx.msg.text || ''; 11 | const target = process.env.CHAT_ID!; 12 | const messageId: number = ctx.msg.message_id; 13 | const { header, newEntities } = modEntities(ctx.msg); 14 | const newText = [header, text].join('\n'); 15 | 16 | await ctx.react('🙏'); 17 | await ctx.reply(basicAnswer, { reply_to_message_id: messageId }); 18 | await bot.api.sendMessage(target, newText, { entities: newEntities }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/functions/forward-photo.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'grammy'; 2 | import { modEntities } from '../utils/helper'; 3 | import bot from '../bot'; 4 | import { basicAnswer } from '../responses/messages'; 5 | 6 | export const forwardPhoto = async (ctx: Context) => { 7 | if (!ctx.msg || !ctx.msg.photo) { 8 | return; 9 | } 10 | const photo = ctx.msg.photo[0].file_id; 11 | const caption = ctx.msg.caption || ''; 12 | const target = process.env.CHAT_ID!; 13 | const messageId: number = ctx.msg.message_id; 14 | const { header, newEntities } = modEntities(ctx.msg); 15 | const newCaption = [header, caption].join('\n'); 16 | 17 | await ctx.react('🙏'); 18 | await ctx.reply(basicAnswer, { reply_to_message_id: messageId }); 19 | await bot.api.sendPhoto(target, photo, { 20 | caption: newCaption, 21 | caption_entities: newEntities, 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { limit } from '@grammyjs/ratelimiter'; 2 | import 'dotenv/config'; 3 | import bot from './bot'; 4 | import composer from './commands/composer'; 5 | import { forwardMessage, forwardPhoto, refuseMessage } from './functions'; 6 | import { floodMessage } from './responses/messages'; 7 | import { development } from './utils/start'; 8 | 9 | bot.use( 10 | limit({ 11 | timeFrame: 3000, 12 | limit: 5, 13 | onLimitExceeded: async ctx => { 14 | await ctx.reply(floodMessage(3)); 15 | await ctx.react('👎'); 16 | }, 17 | }), 18 | ); 19 | 20 | bot.use(composer); 21 | 22 | bot.chatType('private').on(['msg:photo'], forwardPhoto); 23 | 24 | bot.chatType('private').on(['msg::url', 'msg:text', 'msg::text_link'], forwardMessage); 25 | 26 | bot.chatType('private').on(['msg'], refuseMessage); 27 | 28 | process.env.NODE_ENV === 'development' && development(bot); 29 | 30 | export {}; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Compiladoras de Cafeína 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": "cafeina-vagas-bot", 3 | "version": "2.0.0", 4 | "description": "Bot de vagas do Cafeina Vagas no Telegram", 5 | "main": "api/index.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "postbuild": "bash postbuild-set-webhook.sh", 9 | "dev": "nodemon src/main.ts", 10 | "format": "prettier --write 'src/**/*.ts'" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/cafeinabots/cafeina-vagas-bot.git" 15 | }, 16 | "author": "Compiladoras", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/cafeinabots/cafeina-vagas-bot/issues" 20 | }, 21 | "homepage": "https://github.com/cafeinabots/cafeina-vagas-bot#readme", 22 | "dependencies": { 23 | "@grammyjs/ratelimiter": "^1.2.0", 24 | "@grammyjs/transformer-throttler": "^1.2.1", 25 | "dotenv": "^16.3.1", 26 | "grammy": "^1.20.3" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^20.10.7", 30 | "nodemon": "^3.0.2", 31 | "prettier": "^3.1.1", 32 | "prettier-plugin-organize-imports": "^3.2.4", 33 | "ts-node": "^10.9.2", 34 | "typescript": "^5.3.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageEntity } from 'grammy/types'; 2 | 3 | export const modEntities = (msg: Message): { header: string; newEntities: MessageEntity[] } => { 4 | const entities = msg.entities || msg.caption_entities || []; 5 | const from = msg.from?.id; 6 | const first_name = msg.from?.first_name || ''; 7 | const last_name = msg.from?.last_name; 8 | const fullname = [first_name, last_name].filter(Boolean).join(' '); 9 | const username = msg.from?.username; 10 | const prefix = '👤 Enviado por: '; 11 | const header = `${prefix}${fullname} #id${from}`; 12 | const headerEntities: MessageEntity[] = [ 13 | { 14 | offset: 0, 15 | length: 15, 16 | type: 'bold', 17 | }, 18 | { 19 | offset: 0, 20 | length: 2, 21 | type: 'text_mention', 22 | user: { 23 | id: from || 0, 24 | is_bot: false, 25 | first_name, 26 | last_name, 27 | username, 28 | }, 29 | }, 30 | { 31 | offset: prefix.length + (fullname.length + 2), 32 | length: from?.toString().length || 0, 33 | type: 'hashtag', 34 | }, 35 | ]; 36 | entities.forEach(entity => (entity.offset += header.length + 1)); 37 | const newEntities = entities.concat(headerEntities); 38 | return { header, newEntities }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/responses/messages.ts: -------------------------------------------------------------------------------- 1 | export const welcomeMessage = `Oi, eu sou a bot que recebe vagas e outros links para o @CafeinaVagas. 2 | 3 | ❤ Fico no aguardo de novas vagas, cursos ou outros materiais legais para compartilhar. 4 | 5 | 💬 Conheça o Cafeína Help, nosso chat para tirar dúvidas: @CafeinaVagasChat 6 | 7 | 🎧 Se você deseja conversar com um ADM, entre em contato pela nossa Central de Atendimento: @SAC_CafeinaVagas`; 8 | 9 | export const basicAnswer = `Oi, recebi sua mensagem! Obrigada por enviar uma vaga ou um conteúdo para o nosso canal! 10 | 11 | Nossos ADMs humanos irão avaliar o link e, caso seja bacana para o grupo, o link será postado. 12 | 13 | PS: Lembrando que só postamos vagas para #iniciantes e sem experiência. 😉`; 14 | 15 | export const helpMessage = `Nosso bot recebe vagas, cursos e outros tipos de materiais. Basta enviar aqui em formato de texto ou link. 16 | 17 | ❤ Fico no aguardo de novas vagas, cursos ou outros materiais legais para compartilhar. 18 | 19 | 💬 Conheça o Cafeína Help, nosso chat para tirar dúvidas: @CafeinaVagasChat 20 | 21 | 🎧 Se você deseja conversar com um ADM, entre em contato pela nossa Central de Atendimento: @SAC_CafeinaVagas`; 22 | 23 | export const errorMessage = `Ops, parece que ocorreu um erro. 🥺 24 | 25 | Para nos ajudar a melhorar cada vez mais o nosso bot, por favor envie o erro a seguir no nosso e-mail (contato@compiladoras.com.br): `; 26 | 27 | export const wrongFormat = `No momento nosso bot não aceita vagas nesse formato. 😔 28 | 29 | Se essa vaga tiver alguma outra descrição ou site, nos envie porque assim poderemos analisar ela!`; 30 | 31 | export const floodMessage = seconds => 32 | `Opa, você enviou muitas mensagens em um curto período! aguarde ${seconds} segundos para enviar outra mensagem!`; 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cafeína Vagas Bot 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | Bot da comunidade **@CafeinaVagas** do Telegram. 16 | 17 | Esse bot é responsável por receber recomendações de vagas para iniciantes e outros materiais bacanas que podemos postar em nosso canal. 18 | 19 | ## Sobre o bot 20 | 21 | - Usamos o framework [grammY](https://github.com/grammyjs/grammY) para criar o bot 22 | - Linguagem usada: **TypeScript** 23 | - Projeto hospedado na Vercel. 24 | 25 | ## Deploy 26 | 27 | - Com um bot criado no telegram e o Token em mãos, clique no botão abaixo e siga o passo a passo 28 | 29 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcafeinabots%2Fcafeina-vagas-bot&env=BOT_TOKEN,CHAT_ID&project-name=cafeina-vagas-bot&repository-name=cafeina-vagas-bot) 30 | 31 | 32 | ## Desenvolvimento 33 | 34 | ```bash 35 | # Siga o processo de deploy acima, e faça um git clone de seu repo 36 | # Copie o .env.example e coloque o BOT_TOKEN do seu bot 37 | $ cp .env.example .env 38 | # Instale as dependências 39 | $ npm install 40 | # Execute o bot em modo de desenvolvimento 41 | $ npm run dev 42 | ``` 43 | 44 | ## Como contribuir? 45 | 46 | Nosso bot é um software de código livre. Aceitamos contribuições de qualquer pessoa para construir um bot ainda melhor. 47 | 48 | Para contribuir, faça um `Fork` desse repositório, realize suas modificações e envie um `Pull Request` para avaliarmos. 49 | 50 | ## Contato 51 | 52 | Para entrar em contato com a equipe, você pode enviar uma mensagem para o nosso bot de atendimento no Telegram ([@SAC_CafeinaBot](https://t.me/SAC_CafeinaBot)) ou enviar e-mail para contato@compiladoras.com.br 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .vercel 107 | public/ 108 | .vscode --------------------------------------------------------------------------------