├── .gitignore ├── src ├── events │ ├── interactionCreate.js │ ├── ready.js │ └── sendNews.js ├── log │ └── logger.js ├── api │ └── register_commands.js ├── index.js ├── utils │ ├── email │ │ ├── get_news.py │ │ └── format_news.py │ └── generator │ │ └── embedBuilder.js ├── database │ └── database.js └── commands │ └── selectChannel.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | package-lock.json 4 | .log -------------------------------------------------------------------------------- /src/events/interactionCreate.js: -------------------------------------------------------------------------------- 1 | import { Events } from "discord.js" 2 | 3 | const interactionCreate = { 4 | name: Events.InteractionCreate, 5 | execute: async (interaction) => { 6 | if (!interaction.isChatInputCommand()) return 7 | // avoiding recursive response 8 | const commandName = interaction.commandName 9 | const executeCommand = interaction.client.commands.get(commandName).execute 10 | await executeCommand(interaction) 11 | } 12 | } 13 | 14 | export default interactionCreate -------------------------------------------------------------------------------- /src/log/logger.js: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | import path from 'node:path' 3 | 4 | const logFilePath = path.join(process.cwd(), 'src/log/.log') 5 | const { combine, timestamp, align, printf, cli } = winston.format 6 | 7 | const loggerFormat = combine( 8 | cli(), 9 | timestamp({ 10 | format: 'YYYY-MM-DD hh:mm:ss' 11 | }), 12 | align(), 13 | printf((info) => `[${info.timestamp}] - ${info.level}: ${info.message}`) 14 | ) 15 | 16 | const logger = winston.createLogger({ 17 | level: 'info', 18 | format: loggerFormat, 19 | transports: [new winston.transports.File({ 20 | filename: logFilePath 21 | })] 22 | }) 23 | 24 | export default logger -------------------------------------------------------------------------------- /src/events/ready.js: -------------------------------------------------------------------------------- 1 | import { Events } from "discord.js"; 2 | import logger from "../log/logger.js"; 3 | import sendNews from "./sendNews.js"; 4 | 5 | const ready = { 6 | name: Events.ClientReady, 7 | execute: (client) => { 8 | logger.info(`${client.user.tag} is ready!`) 9 | 10 | const checkNews = async () => { 11 | 12 | const response = await sendNews(client.channels.cache) 13 | if (response == null) logger.warn('Sem novas noticiais') 14 | else logger.info('Noticias novas!') 15 | 16 | setTimeout(checkNews, 1000 * 60 * 30) 17 | } 18 | 19 | checkNews() 20 | // Checking for news... 21 | } 22 | } 23 | 24 | export default ready -------------------------------------------------------------------------------- /src/api/register_commands.js: -------------------------------------------------------------------------------- 1 | import { REST, Routes } from "discord.js"; 2 | import { config } from 'dotenv' 3 | import selectChannel from '../commands/selectChannel.js' 4 | import logger from "../log/logger.js"; 5 | 6 | config() 7 | 8 | const rest = new REST().setToken(process.env.TOKEN) 9 | 10 | const commands = new Array( 11 | selectChannel.data.toJSON() 12 | ) 13 | 14 | async function registerCommands (){ 15 | try { 16 | logger.info('Started refreshing commands!') 17 | 18 | await rest.put( 19 | Routes.applicationCommands(process.env.CLIENT_ID), 20 | {body: commands} 21 | ) 22 | 23 | for (const command of commands){ 24 | logger.info(`✅ - [${command.name}] slash command added!`) 25 | } 26 | 27 | } 28 | catch(ex){ 29 | logger.error(ex) 30 | } 31 | }; 32 | 33 | registerCommands(); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ActivityType, Client, Collection, Events, 2 | GatewayIntentBits, PresenceUpdateStatus } from "discord.js"; 3 | import { config } from 'dotenv'; 4 | import selectChanel from './commands/selectChannel.js' 5 | import ready from "./events/ready.js"; 6 | import interactionCreate from "./events/interactionCreate.js"; 7 | 8 | config() 9 | 10 | const client = new Client({ 11 | intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds], 12 | presence: { 13 | status:PresenceUpdateStatus.Online, 14 | activities: [{ 15 | name: 'Filipe Deschamps', 16 | type: ActivityType.Watching 17 | }] 18 | } 19 | }) 20 | 21 | client.commands = new Collection([ 22 | ['selecionarcanal', selectChanel] 23 | ]) 24 | 25 | client.on(Events.InteractionCreate, interactionCreate.execute) 26 | client.once(Events.ClientReady, ready.execute) 27 | 28 | client.login(process.env.TOKEN) -------------------------------------------------------------------------------- /src/events/sendNews.js: -------------------------------------------------------------------------------- 1 | import { getAllChannels, removeFromDB } from "../database/database.js"; 2 | import getEmbed from "../utils/generator/embedBuilder.js"; 3 | import logger from "../log/logger.js"; 4 | 5 | export default async function sendNews(clientCache){ 6 | const embed = await getEmbed() 7 | if (embed == null) return null 8 | 9 | const servers = await getAllChannels() 10 | 11 | try{ 12 | for (const server of servers){ 13 | const channel = clientCache.get(server[0]) 14 | if (channel == undefined) { 15 | console.log(server[1]) 16 | removeFromDB(server[1]) 17 | continue 18 | } 19 | await channel.sendTyping(); 20 | await channel.send({embeds: [embed]}) 21 | 22 | logger.info(`Newsletter enviada no canal: ${server}`) 23 | } 24 | 25 | return true 26 | } 27 | catch(err){ 28 | console.log(err) 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deschampsbot", 3 | "version": "1.0.0", 4 | "description": "Um simples bot open-source desenvolvido para compartilhar a newsletter do Filipe Deschamps no discord!", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "bot": "node src/index.js", 9 | "register-commands": "node src/api/register_commands.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/F1NH4WK/DeschampsBot.git" 14 | }, 15 | "keywords": [ 16 | "discord-bot", 17 | "filipe-deschamps", 18 | "newsletter" 19 | ], 20 | "author": "Finhawk", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/F1NH4WK/DeschampsBot/issues" 24 | }, 25 | "homepage": "https://github.com/F1NH4WK/DeschampsBot#readme", 26 | "dependencies": { 27 | "discord.js": "^14.14.1", 28 | "dotenv": "^16.4.1", 29 | "mongodb": "^6.3.0", 30 | "python-shell": "^5.0.0", 31 | "winston": "^3.11.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Finhawk 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 | -------------------------------------------------------------------------------- /src/utils/email/get_news.py: -------------------------------------------------------------------------------- 1 | from imaplib import IMAP4_SSL 2 | import json 3 | from dotenv import load_dotenv 4 | import os 5 | from format_news import formatNews 6 | 7 | load_dotenv() 8 | email = os.getenv('EMAIL') 9 | password = os.getenv('PASSWORD') 10 | 11 | def getEmailStream(): 12 | try: 13 | with IMAP4_SSL('imap.gmail.com', 993) as M: 14 | M = IMAP4_SSL(host='imap.gmail.com', port=993) 15 | M.login(email, password) 16 | M.select(mailbox = 'INBOX') 17 | typ, msgID = M.search(None, 'FROM', "'newsletter"', (UNSEEN)') 18 | 19 | typ, data = M.fetch(msgID[0], '(BODY.PEEK[TEXT])') 20 | 21 | M.store(msgID[0].replace(b' ', b','), '+FLAGS', '\Seen') 22 | # Since we're not using RFC822 protocol anymore, we need to set email as seen manually. 23 | 24 | 25 | return data[0][1] 26 | 27 | except: 28 | raise IndexError 29 | 30 | try: 31 | encodedEmail = getEmailStream() 32 | formatedNews = formatNews(encodedEmail) 33 | 34 | print(json.dumps(formatedNews, ensure_ascii = False)) 35 | # Sending json to javascript! 36 | 37 | except IndexError: 38 | print(json.dumps({})) 39 | # Sending an empty json to recognize there's no news -------------------------------------------------------------------------------- /src/utils/email/format_news.py: -------------------------------------------------------------------------------- 1 | from lxml import html 2 | import quopri 3 | 4 | def formatNews(encodedEmail): 5 | news = [] 6 | 7 | decodedEmail = (quopri.decodestring(encodedEmail)).decode('utf-8') 8 | source_email = html.fromstring(decodedEmail) 9 | 10 | newsTitles = source_email.xpath('//td/p[position() > 1]/strong[1]') 11 | newsTitles.insert(0, source_email.xpath('//td/p[1]/strong[2]')[0]) 12 | # For some reason the first contains nothing, so we need to do this insert. 13 | 14 | newsContent = source_email.xpath('//td/p') 15 | newsLinks = source_email.xpath('//td/p/a[last()]') 16 | 17 | for index, title in enumerate(newsTitles): 18 | notice = newsContent[index].text_content() 19 | notice = notice.replace(title.text_content(), '') 20 | # Removing the title from notice, so we avoing sending it twice 21 | 22 | news.append({ 23 | 'title': title.text_content(), 24 | 'content': notice.strip().capitalize() 25 | }) 26 | 27 | # Adding links to the last news 28 | 29 | for index, link in enumerate(reversed(newsLinks)): 30 | content = news[-index - 1]['content'] 31 | linkText = content.rsplit(':')[-1] 32 | new_content = content.replace(linkText, f' [{linkText.strip().capitalize()}]({link.get("href")})') 33 | news[-index - 1]['content'] = new_content 34 | 35 | return news -------------------------------------------------------------------------------- /src/utils/generator/embedBuilder.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Colors } from "discord.js"; 2 | import logger from "../../log/logger.js"; 3 | import { PythonShell } from 'python-shell' 4 | 5 | export default async function getEmbed(){ 6 | 7 | const json = await PythonShell.run("src/utils/email/get_news.py") 8 | const news = JSON.parse(json[0]) 9 | 10 | if (news.length == undefined) return null 11 | // Avoinding empty embed message 12 | 13 | const embed = new EmbedBuilder() 14 | 15 | .setColor(Colors.Yellow) 16 | .setAuthor({ 17 | name: 'Curso.dev', 18 | url: 'https://curso.dev/' 19 | }) 20 | .setFooter({ 21 | iconURL: 'https://yt3.googleusercontent.com/ytc/AIf8zZQqWLIMgm0a79oUAVIz3mRxAkdH-0F_1oMqhDEI=s900-c-k-c0x00ffffff-no-rj', 22 | text: 'Filipe Deschamps Newsletter' 23 | }) 24 | .setTimestamp(new Date().getTime()) 25 | .setURL('https://filipedeschamps.com.br/newsletter') 26 | .setTitle('Inscreva-se na Newsletter!') 27 | 28 | for (const notice of news){ 29 | 30 | let error_news = 0 31 | 32 | try{ 33 | embed.addFields({ 34 | name: notice.title, 35 | value: notice.content, 36 | }) 37 | } 38 | 39 | catch(err){ 40 | error_news++ 41 | logger.info(`There's ${error_news} notice(s) exceding discord's characters limits`) 42 | 43 | embed 44 | .setFooter({text: `${error_news} notícia${error_news > 1? 's' : ''} não ${error_news > 1? 'puderam': 'pode'} ser ${error_news > 1? 'enviadas': 'enviada'}...`}) 45 | } 46 | } 47 | logger.info('Noticias armazenadas no embed!') 48 | 49 | return embed 50 | } -------------------------------------------------------------------------------- /src/database/database.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb' 2 | import { config } from 'dotenv' 3 | import logger from '../log/logger.js' 4 | 5 | config() // Starting dotenv 6 | 7 | const uri = process.env.MONGO_DB_SECRET 8 | const client = new MongoClient(uri) // starting mongodb 9 | 10 | export async function insertIntoDB(GUILD_ID, CHANNEL_ID){ 11 | 12 | try{ 13 | await client.connect() 14 | const db = client.db('Servers') 15 | const GUILD_DB = db.collection(GUILD_ID) 16 | 17 | const doc = { 18 | CHANNEL_ID: CHANNEL_ID 19 | } 20 | 21 | await GUILD_DB.deleteMany({}) // Deleting eventuals channels, if so 22 | await GUILD_DB.insertOne(doc) 23 | 24 | logger.info(`A guilda ${GUILD_ID} teve o canal ${CHANNEL_ID} adicionado!`) 25 | await client.close() 26 | 27 | return true 28 | } 29 | catch (err){ 30 | return err 31 | } 32 | 33 | } 34 | 35 | export async function removeFromDB(GUILD_ID){ 36 | 37 | await client.connect() 38 | 39 | const db = client.db('Servers') 40 | await db.collection(GUILD_ID).drop() 41 | return console.log(`${GUILD_ID} retira com sucesso!`) 42 | } 43 | 44 | export async function getAllChannels(){ 45 | await client.connect() 46 | 47 | const db = client.db('Servers') 48 | const collections = await db.listCollections().toArray() 49 | const sendInfo = [] 50 | 51 | for (const collection of collections){ 52 | const guild_id = collection.name 53 | let channel_id = await db.collection(guild_id).findOne({}) 54 | channel_id = channel_id.CHANNEL_ID 55 | 56 | sendInfo.push([ channel_id, guild_id ]) 57 | } 58 | 59 | await client.close() 60 | 61 | return sendInfo 62 | } -------------------------------------------------------------------------------- /src/commands/selectChannel.js: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder, ActionRowBuilder, ChannelSelectMenuBuilder, ChannelType, PermissionFlagsBits } from "discord.js" 2 | import { insertIntoDB } from "../database/database.js" 3 | import logger from "../log/logger.js" 4 | 5 | const selectChanel = { 6 | data: new SlashCommandBuilder() 7 | .setName('selecionarcanal') 8 | .setDescription('Selecione um canal para a newsletter ser enviada!') 9 | .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), 10 | 11 | execute: async (interaction) => { 12 | const select = new ChannelSelectMenuBuilder() 13 | .setCustomId(interaction.id) 14 | .setChannelTypes([ChannelType.GuildText]) 15 | .setPlaceholder('Selecione um canal...') 16 | .setMaxValues(1) 17 | .setMinValues(0) 18 | 19 | const row = new ActionRowBuilder() 20 | .setComponents(select) 21 | 22 | const response = await interaction.reply({ 23 | content: 'Aqui estão os canais disponíveis:', 24 | components: [row], 25 | }) 26 | 27 | const collectorFilter = i => i.user.id === interaction.user.id; 28 | 29 | try { 30 | const confirmation = await response.awaitMessageComponent({ 31 | filter: collectorFilter, 32 | time: 30_000 33 | }) 34 | 35 | const channel_id = confirmation.values.join('') 36 | const guild_id = interaction.guild.id 37 | 38 | const insertdb = await insertIntoDB(guild_id, channel_id) 39 | 40 | if (insertdb != true){ 41 | await confirmation.reply({ content: `Estamos com problemas em nosso servidor, tente novamente mais tarde...`}) 42 | logger.error(new Error(insertdb)) 43 | await interaction.deleteReply() 44 | } 45 | 46 | else { 47 | await interaction.deleteReply() 48 | await confirmation.reply({ content: `Beleza! As noticias serão enviadas no canal <#${channel_id}>!`}) 49 | } 50 | } 51 | catch(ex){ 52 | await interaction.followUp({content: 'Parece que você não escolheu um canal :('}) 53 | await interaction.deleteReply() 54 | logger.error(ex) 55 | } 56 | } 57 | } 58 | 59 | export default selectChanel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | DeschampBot 4 |
5 |

6 | 7 |

Um bot open-source desenvolvido para compartilhar a newsletter do Filipe Deschamps no Discord

8 | 9 |

10 | Made with JavaScript 11 | Made with Node.js 12 | Made with MongoDB 13 |

14 | 15 | ![Exemplo](https://i.imgur.com/I19iY4E.gif) 16 | 17 |

Processo de Criação

18 | 19 | 1. Obtenção das notícias 20 | - Formatando as notícias 21 | - Enviando as notícias para o terminal 22 | 2. Criando o Bot Discord 23 | - Construindo o Embed 24 | - Enviando as notícias 25 | 26 | 27 |

Obtenção de notícias

28 |

As notícias são obtidas dentro do diretório src/utils/email. Nele há as funções que permitem acessar a conta GMAIL, realizar uma conexão IMAP e em seguida dar fetch para a conexão client-server ser realizada e os emails serem obtidos:

29 | 30 | > src/utils/email/get_news.py 31 | ```py 32 | def getEmailStream(): 33 | with IMAP4_SSL('imap.gmail.com', 993) as M: 34 | M = IMAP4_SSL(host='imap.gmail.com', port=993) 35 | M.login(email, password) 36 | M.select(mailbox = 'INBOX') 37 | typ, msgID = M.search(None, 'FROM', "'newsletter"', (UNSEEN)') 38 | 39 | typ, data = M.fetch(msgID[0], '(BODY.PEEK[TEXT])') 40 | 41 | M.store(msgID[0].replace(b' ', b','), '+FLAGS', '\Seen') 42 | 43 | return data[0][1] 44 | ``` 45 | 46 | A função conecta com o uso do `with` para permitir que após o algoritmo ser finalizado a conexão se encerra, desta forma não precisamos usar o `IMAP4.close && IMAP4.logout()`. Seguindo, utilizamos o `IMAP4_SSL`pois a conexão com o gmail é por meio do SSL/TLS, assim como a porta, *993*, que também é pré-estabelecida pelo gmail. [Saiba Mais](https://www.getmailspring.com/setup/access-gmail-com-via-imap-smtp) 47 | 48 | Em seguida, realizamos uma busca pelos **IDS** dos emails dentra da **INBOX**, cujo critério é o remetente ser *newsletter* e possuir a flag *não visto*. Após encontrado(s), basta usarmos `M.fetch(IdDoEmail, EstruturaDesejada)`, desta formas temos a byte-string com todos os dados que precisamos, agora, processar. 49 | 50 | > Curiosidade: Por não estar utilizando o protoclo **RFC822**, tenho que manualmente adicionar a flag de visto. A propósito, *flags* nada mais são que propriedades que cada email carrega consigo, semelhante as tags html. 51 | 52 | Por fim, é retornada uma tupla que possui a quantidade de bytes e a raw do email, onde estão todas as informações, no entanto em *byte* e codificadas *quoted-printable*. 53 | 54 |

Formatando as notícias

55 | 56 | É necessário tornar todo aquele html codificado em algo visual e legível, certo? Por isso, no arquivo a seguir nós processamos a *byte-string* obtida anteriormente por meio do parser *lxml*, uma ótima biblioteca, assim como o *BeautifulSoup e html5lib*. No entanto, acabei preferindo o lxml por conta do *xpath*, que facilita muito o *web scraping*. 57 | 58 | > src/utils/email/format_news.py 59 | ```py 60 | from lxml import html 61 | import quopri 62 | 63 | def formatNews(encodedEmail): 64 | news = [] 65 | 66 | decodedEmail = (quopri.decodestring(encodedEmail)).decode('utf-8') 67 | source_email = html.fromstring(decodedEmail) 68 | 69 | newsTitles = source_email.xpath('//td/p[position() > 1]/strong[1]') 70 | newsTitles.insert(0, source_email.xpath('//td/p[1]/strong[2]')[0]) 71 | # For some reason the first contains nothing, so we need to do this insert. 72 | 73 | newsContent = source_email.xpath('//td/p') 74 | newsLinks = source_email.xpath('//td/p/a[last()]') 75 | 76 | for index, title in enumerate(newsTitles): 77 | notice = newsContent[index].text_content() 78 | notice = notice.replace(title.text_content(), '') 79 | # Removing the title from notice, so we avoing sending it twice 80 | 81 | news.append({ 82 | 'title': title.text_content(), 83 | 'content': notice.strip().capitalize() 84 | }) 85 | 86 | # Adding links to the last news 87 | 88 | for index, link in enumerate(reversed(newsLinks)): 89 | content = news[-index - 1]['content'] 90 | linkText = content.rsplit(':')[-1] 91 | new_content = content.replace(linkText, f' [{linkText.strip().capitalize()}]({link.get("href")})') 92 | news[-index - 1]['content'] = new_content 93 | 94 | return news 95 | ``` 96 | 97 | Primeiramente, decodificamos a byte-string recebida com o uso do quopri. O quopri é um decodificador nativo do python utilizado para decodificar formatos *quoted-printable*. O porquê dessas codificações está relacionado com os protoclos RFC e IMAP, além de muitos sites utilizarem codificações *ASCII*. Após decodificado, apenas passamos este, agora html, para o **lxml**, que cria uma *etree* onde podemos fazer diversas buscas. O padrão dos emails da newsletter é bem simples, todas as notícias estão inseridas em uma tabela, e toda notícia corresponde a uma tag `

`. 98 | 99 | 100 | 101 | Portanto, utilizando-se um pouco de lógica podemos obter todas essas notícias. No entanto, ainda temos de obter os links, que podem ser facilmente obtidos utilizando o método do lxml `lxml.html.innerlinks()`, mas que preferi realizar manualmente para evitar possiveis erros. 102 | 103 | ```py 104 | for index, link in enumerate(reversed(newsLinks)): 105 | content = news[-index - 1]['content'] 106 | linkText = content.rsplit(':')[-1] 107 | new_content = content.replace(linkText, f' [{linkText.strip().capitalize()}]({link.get("href")})') 108 | news[-index - 1]['content'] = new_content 109 | ``` 110 | 111 | Talvez tenham se perguntado, por que utilzou reversed? Bom, o lxml providencia os links debaixo pra cima, louco, não? Por isso eu precisei fazer um **loop reverso**, adicionando os **hyperlinks** para cada notícia que possuise uma tag `` dentro da tag `

`. 112 | 113 |

Enviando as notícias para o terminal

114 | 115 | Com isso, retomamos nosso processo no `src/utils/email/get_news.py`, prosseguindo para o seguinte comando: 116 | 117 | > src/utils/email/get_news.py 118 | ```py 119 | print(json.dumps(formatedNews, ensure_ascii = False)) 120 | ``` 121 | Utilizamos a biblioteca json para transformar a string em json, por meio do comando **dump**. Além disso, garantimos que o *ASCII* não será aplicado ao json, *ninguém merece utf-8 em ascii, seŕio*. Por que o **print()**? Anteriormente, havia optado por criar um arquivo json, o qual seria utilizado pelo javascript para ler as notícias e manda-las, no entanto houve diversos problemas com essa funcionalidade, ainda não compreendidas por mim, então utilzei este método mais prático, que usa a biblioteca **PythonShell** pra executar este aquivo Python e receber todos os seus prompts. 122 | 123 |

Criando o Bot Discord

124 | 125 | A criação de um bot no discord é bem complexa no começo, mas conforme você se familiariza, tudo se torna mais fácil. Nosso bot começa em `src/index.js`: 126 | 127 | > src/index.js 128 | 129 | ```js 130 | const client = new Client({ 131 | intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds], 132 | presence: { 133 | status:PresenceUpdateStatus.Online, 134 | activities: [{ 135 | name: 'Filipe Deschamps', 136 | type: ActivityType.Watching 137 | }] 138 | } 139 | }) 140 | 141 | client.commands = new Collection([ 142 | ['selecionarcanal', selectChanel] 143 | ]) 144 | 145 | client.on(Events.InteractionCreate, interactionCreate.execute) 146 | client.once(Events.ClientReady, ready.execute) 147 | 148 | client.login(process.env.TOKEN) 149 | ``` 150 | 151 | Importamos algumas funcionalidades do discord para personalizar as permissões do bot e seu status na rede. Em seguida, criamos um **coleção** de comandos, os quais passamos o nome e a função a ser executada quando este comando for chamado. Esta é uma ótima prática, visto que podemos acessar nosso cliente nas interações por meio do `interaction.client`. Por fim, apenas aplicamos **listeners** no nosso client: um para quando um comando for executado e outro para quando ligar, respectivamente. 152 | 153 | > Não irei explicar como funciona o processo de criação de comandos neste README, mas vocês podem conferir tudo o que estou dizendo na [documentação do discordJs.](https://discordjs.guide/) 154 | 155 |

Construindo o embed

156 | 157 | Com as configurações necessárias para o bot responder aos comandos, só nos resta construir o embed e mandar as notícias. Para a construção do embed, utilizamos o constructor **EmbedBuilder()**, adicionando todas as informações. Referenciado no supracitado, o **PythonShell** executa o código python e manda no prompt o json, no presente arquivo apenas recebemos ele. Apenas realizamos um verificação inicial com o âmbito de evitar um embed sem notícias. 158 | 159 | > src/utils/generator/embedBuilder.js 160 | 161 | ```js 162 | export default async function getEmbed(){ 163 | 164 | const json = await PythonShell.run("src/utils/email/get_news.py") 165 | const news = JSON.parse(json[0]) 166 | 167 | if (news.length == undefined) return null 168 | // Avoinding empty embed message 169 | 170 | const embed = new EmbedBuilder() 171 | 172 | .setColor(Colors.Yellow) 173 | .setAuthor({ 174 | name: 'Curso.dev', 175 | url: 'https://curso.dev/' 176 | }) 177 | ... 178 | 179 | for (const notice of news){ 180 | 181 | let error_news = 0 182 | embed.addFields({ 183 | name: notice.title, 184 | value: notice.content, 185 | }) 186 | } 187 | ``` 188 | 189 | Realizamos um loop para ir pegando cada título e contéudo de cada notícia e adicionando ele aos campos do embed. Note que há uma variável chamada `error_news`, pois existem algumas notícias da newsletter que ultrapassam o limite de caracteres do discord, **1024**. 190 | 191 |

Enviando as notícias aos servidores

192 | 193 | > src/events/send_news.js 194 | ```js 195 | export default async function sendNews(clientCache){ 196 | const embed = await getEmbed() 197 | if (embed == null) return null 198 | 199 | const servers = await getAllChannels() 200 | 201 | try{ 202 | for (const server of servers){ 203 | const channel = clientCache.get(server[0]) 204 | if (channel == undefined) { 205 | console.log(server[1]) 206 | removeFromDB(server[1]) 207 | continue 208 | } 209 | await channel.sendTyping(); 210 | await channel.send({embeds: [embed]}) 211 | 212 | logger.info(`Newsletter enviada no canal: ${server}`) 213 | } 214 | 215 | return true 216 | } 217 | catch(err){ 218 | console.log(err) 219 | } 220 | } 221 | ``` 222 | 223 | Esta é a parte que ainda busco otimizar, pois existe a possibilidade do bot estar em diversos servidores, o que acarretaria em respostas mais demoradas e possiveis erros devido ao fluxo de informação. No entanto, este arquivo conecta-se com o **mongodb**, que possui o **id de cada canal** onde a notícia será enviada. Com o embed criado, canais preparados, basta apenas mandar as notícias! --------------------------------------------------------------------------------