Skip to content

F1NH4WK/DeschampsBot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DeschampBot

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

Made with JavaScript Made with Node.js Made with MongoDB

Exemplo

Processo de Criação

  1. Obtenção das notícias
  2. Criando o Bot Discord

Obtenção de notícias

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:

src/utils/email/get_news.py

  def getEmailStream():
    with IMAP4_SSL('imap.gmail.com', 993) as M:
    M = IMAP4_SSL(host='imap.gmail.com', port=993)
    M.login(email, password)
    M.select(mailbox = 'INBOX')
    typ, msgID = M.search(None, 'FROM', "'newsletter"', (UNSEEN)')

    typ, data = M.fetch(msgID[0], '(BODY.PEEK[TEXT])')

    M.store(msgID[0].replace(b' ', b','), '+FLAGS', '\Seen')

    return data[0][1]

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_SSLpois 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

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.

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.

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.

Formatando as notícias

É 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.

src/utils/email/format_news.py

from lxml import html
import quopri

def formatNews(encodedEmail):
   news = []

   decodedEmail = (quopri.decodestring(encodedEmail)).decode('utf-8')
   source_email = html.fromstring(decodedEmail)

   newsTitles = source_email.xpath('//td/p[position() > 1]/strong[1]')
   newsTitles.insert(0, source_email.xpath('//td/p[1]/strong[2]')[0])
   # For some reason the first <strong> contains nothing, so we need to do this insert.

   newsContent = source_email.xpath('//td/p')
   newsLinks = source_email.xpath('//td/p/a[last()]')

   for index, title in enumerate(newsTitles):
      notice = newsContent[index].text_content()
      notice = notice.replace(title.text_content(), '')
      # Removing the title from notice, so we avoing sending it twice

      news.append({
         'title': title.text_content(),
         'content': notice.strip().capitalize()
      })

   # Adding links to the last news

   for index, link in enumerate(reversed(newsLinks)):
      content = news[-index - 1]['content']
      linkText = content.rsplit(':')[-1]
      new_content = content.replace(linkText, f' [{linkText.strip().capitalize()}]({link.get("href")})')
      news[-index - 1]['content'] = new_content

   return news

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 <p>.

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.

  for index, link in enumerate(reversed(newsLinks)):
      content = news[-index - 1]['content']
      linkText = content.rsplit(':')[-1]
      new_content = content.replace(linkText, f' [{linkText.strip().capitalize()}]({link.get("href")})')
      news[-index - 1]['content'] = new_content

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 <a> dentro da tag <p>.

Enviando as notícias para o terminal

Com isso, retomamos nosso processo no src/utils/email/get_news.py, prosseguindo para o seguinte comando:

src/utils/email/get_news.py

print(json.dumps(formatedNews, ensure_ascii = False))

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.

Criando o Bot Discord

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:

src/index.js

const client = new Client({
	intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds], 
	presence: { 
		status:PresenceUpdateStatus.Online,
		activities: [{
			name: 'Filipe Deschamps', 
			type: ActivityType.Watching
		}]
	}
})

client.commands = new Collection([
	['selecionarcanal', selectChanel]
])

client.on(Events.InteractionCreate, interactionCreate.execute)
client.once(Events.ClientReady, ready.execute)

client.login(process.env.TOKEN)

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.

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.

Construindo o embed

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.

src/utils/generator/embedBuilder.js

export default async function getEmbed(){

    const json = await PythonShell.run("src/utils/email/get_news.py")
    const news = JSON.parse(json[0])

    if (news.length == undefined) return null
    // Avoinding empty embed message
    
    const embed = new EmbedBuilder()

    .setColor(Colors.Yellow)
    .setAuthor({
        name: 'Curso.dev',
        url: 'https://curso.dev/'
    })
    ...

    for (const notice of news){

        let error_news = 0
          embed.addFields({
              name: notice.title,
              value: notice.content,
          })
      }

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.

Enviando as notícias aos servidores

src/events/send_news.js

export default async function sendNews(clientCache){
    const embed = await getEmbed()
    if (embed == null) return null

    const servers = await getAllChannels()

    try{
        for (const server of servers){
            const channel = clientCache.get(server[0])
            if (channel == undefined) {
                console.log(server[1])
                removeFromDB(server[1])
                continue
            } 
            await channel.sendTyping();
            await channel.send({embeds: [embed]})   

            logger.info(`Newsletter enviada no canal: ${server}`)
        }

        return true
    }
    catch(err){
        console.log(err)
    }
}

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!