Python Discord Bot (beem) - Part 3 - [EN/PT-BR]

avatar
(Edited)


[EN]
This is the third part of this tutorial on how to create a curation bot in python for Hive blockchain. The first part can be found here: https://ecency.com/hive-139531/@mengao/python-discord-bot-part-1. The second part can be found here: https://ecency.com/hive-139531/@mengao/python-discord-bot-supabase-part
[PT-BR]
Esta é a terceira parte do tutorial para criar um bot de curadoria para discord em python na Hive blockchain. A primeira parte pode ser encontrada aqui: https://ecency.com/hive-139531/@mengao/python-discord-bot-part-1. A segunda parte pode ser encontrada aqui: https://ecency.com/hive-139531/@mengao/python-discord-bot-supabase-part


[EN]
As I said in the first part, I'm writing this post for those with no experience programming. Even though I'm not teaching any basic programing skills, I will try to explain in details how I progress in each step, all my thought process, to make it easy to understand. If you have no idea about programming, I suggest starting with something like this: https://www.codecademy.com/learn/learn-how-to-code and for basic python skills you can check this out: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers

[PT-BR]
Como eu disse na primeira parte, estou escrevendo este post para quem não tem nenhuma experiencia com programação. Mesmo que eu não esteja ensinando aqui os fundamentos da programação, eu vou tentar explicar em detalhes meu progresso a cada passo, todo o meu processo de pensamento, para que fique fácil de entender. Se voce não tem nenhuma ideia sobre o que é programação, eu sugiro olhar algo assim: https://www.codecademy.com/learn/learn-how-to-code e para o basico de python veja isso: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers


[EN]
In this third part we are going to learn how to use beem library to access Hive blockchain. This will be the last part of this series, and at the end we should have a working bot. It might still be a simple bot, but it will work.
Before we start, let's think of what we need to accomplish for this bot to work.
1 - We need to manage the queue. I will add 3 more commands to the bot: upvote, showqueue, removepost. I think it's easy to understand, upvote will add a post to the queue, showqueue will list the posts waiting to be upvoted, and removepost will remove a post from the queue.
2 - We need a function running every minute (you can choose how long), checking the voting power of the curation account, and voting the first post in the queue when voting power reaches 99%.

Since we will be using beem, let's install the library using the command bellow in your terminal.

[PT-BR]
Nesta terceira parte nós vamos aprender a usar a biblioteca beem para acessar o blockchain Hive. Esta será a última parte desta serie, e ao final nós devemos ter um bot funcionando. Ele pode até ser bem sim simples, mas será funcional.
Antes de começar, vamos pensar no que precisamos fazer para o bot funcionar.
1 - Nós precisamos gerenciar a fila. Eu vou adicionar mais 3 comandos no bot: upvote, showqueue, removepost. Eu acho que ficou facil de entender, upvote vai adicionar um post na fila, showqueue vai listar os posts aguardando para serem votados, e removepost vai apagar um post da fila.
2 - Nós precisamos de uma função rodando a cada minuto (ou o tempo que voce preferir), checando o poder de voto da conta de curadoria, e votando no primeiro post da fila sempre que o poder de voto atingir 99%.

Como vamos usar o beem, vamos instalar a biblioteca usando o comando abaixo no seu terminal.

pip install -U beem

[EN]
Let's start with item 1. First thing is to create a new table on supabase for our queue. I'm not going to put a step by step for the table creation, as this was explained on the last post. Just follow the same steps. But this time we will have different fields. We need to store at least the link for the post, curator id and vote weight. I also want to add a status field as I'm not going to delete posts from the table after they get voted, I will just change the status. I will also store the creation date for the post, as I want to have the option to prioritize older posts on my queue. So the fields I'm creating on my database table are: link, curator, vote_weight, status, post_created.
Before we start working on upvote, and since all 3 new bot commands will only be accessible by curators, instead of writing the same code 3 times inside each condition, I decided to write a small function to check if a user is a curator and use it inside each condition. Let's look at this function, it has one parameter, a discord Message object. It will check the database for the user who wrote the message, and return True or False.
[PT-BR]
Vamos começar pelo item 1. Primeiro precisamos criar uma nova tabela no banco de dados para armazenar a nossa fila. Eu não vou explicar passo a passo a criação da tabela, pois ja expliquei no último post. Siga os mesmos passos. Mas desta vez vamos ter campos diferentes. Nós precisamos guardar pelo menos o link do post, o id do curador, e o peso do voto. Eu também quero criar um campo para um status, pois não quero apagar os posts da tabela depois de serem votados, vou apenas mudar o status dele. Eu também quero armazenar a data de criação do post, pois quero ter a opção de priorizar os posts mais antigos na fila. Então os campos que estou criando são: link, curator, vote_weight, status, post_created.
Antes de começar a trabalhar no upvote, e como todos os 3 novos comandos do bot só serão acessíveis por curadores, em vez de escrever o mesmo código 3 vezes dentro de cada condição, eu decidi escrever uma pequena função para checar se um usuário é curador e usar essa função dentro de cada condição. Vamos ver essa função, ela tem um parametro que é um objeto Message do discord. E vai checar o banco de dados pelo usuário que escreveu a mensagem, e retornar True ou False.

def isCurator(message):
    discordUserId = message.author.id
    checkUser = supabase.table(dbCuratorsTable).select("*", count="exact").eq("discord_id", discordUserId).execute()
    if checkUser.count == 0:
        return False
    else:
        return True

[EN]
Now let's start working on our upvote command. This command needs to check if the message comes from a curator. Check if the link is valid and if it's already in the queue. And finally store the link in the database. This will be the first time we are going to use beem library. I will use it first to parse the link, I need 2 functions from beem.utils, resolve_authorperm and construct_authorperm. They need to be imported at the beginning of the script, you will see later when I post the entire script. Then I will get the post from the blockchain to grab the creation date of the post. I will need to use the module Comment from beem, and it will also need to be imported at the start of the script.
Here is what the upvote command looks like, all commented.
[PT-BR]
Agora vamos começar a trabalhar no nosso comando de upvote. Este comando precisa checar se a mensagem vem de um curador. Checar se o link é válido e se ja está na fila. E finalmente guardar o link no banco de dados. Esta será a primeira vez que vamos utilizar a biblioteca beem. Eu vou usar para quebrar o link, vou precisar de duas funções do beem.utils, resolve_authorperm e construct_authorperm. Elas precisam ser importadas no inicio do script, voce vai ver no fim quando postar o script completo. Depois vou buscar o post no blockchain para pegar a data de criação do post. Vou precisar usar o modulo Comment do beem, que também vai precisar ser importado no inicio do script.
Veja aqui como ficou o comando upvote, todo comentado.

    if message.content.startswith('upvote'):
        # check is message comes from curator
        if not isCurator(message):
            await message.channel.send("Not Authorized!")
            return

        # split the parameters, the command has 2 parameters: link and vote weight
        params = message.content.split()
        # using try/except to check for error loading Comment
        try:
            # look the link to documentation in the description above to understand resolve_authorperm
            author, permlink = resolve_authorperm(params[1])
            authorperm = construct_authorperm(author, permlink)
            # check if link is already in the database
            checkLink = supabase.table(dbQueueTable).select("*", count="exact").eq("link", authorperm).execute()
            # if link is not in database
            if checkLink.count == 0:
                # get the post from the blockchain
                c = Comment(authorperm)
                voteWeight = params[2]
                # insert link in the database with vote weight, creation date, and status "waiting"
                supabase.table(dbQueueTable).insert({"curator": message.author.id,"link": authorperm, "vote_weight": voteWeight, "post_created": str(c["created"]), "status": "waiting"}).execute()
                await message.channel.send(type(c["created"]))
            else:
                await message.channel.send("Post already in queue!")
        except:
            await message.channel.send("Invalid Link!")

[EN]
Now that we stored the link in the database, let's work on the other 2 commands, showqueue and removepost. The command removepost is very similar to upvote, but instead of inserting in the database, we are deleting from the database. Since it's very similar, I will only comment the part that is different. Let's check it out.
[PT-BR]
Agora que ja guardamos o link no banco de dados, vamos trabalhar nos outros 2 comandos, showqueue e removepost. O comando removepost é bem parecido com upvote, mas em vez de inserir no banco de dados, vamos apagar do banco de dados. Como é muito similar a outra, eu vou comentar somente a parte diferente. Olha como ficou.

    if message.content.startswith('removepost'):
        if not isCurator(message):
            await message.channel.send("Not Authorized!")
            return

        params = message.content.split()
        try:
            author, permlink = resolve_authorperm(params[1])
            authorperm = construct_authorperm(author, permlink)
            checkLink = supabase.table(dbQueueTable).select("*", count="exact").eq("link", authorperm).execute()
            if checkLink.count == 0:
               await message.channel.send("This link is not in queue")
            else:
                # this is the only different part to upvote. instead of insert, delete.
                result = supabase.table(dbQueueTable).delete().eq("link", authorperm).execute()
                responseText = "Link removed from queue successfully"
                await message.channel.send(responseText)
            return
        except:
            await message.channel.send("Invalid Link!")

[EN]
Now let's see what the command showqueue looks like. It is very similar to the command listcurators. It will return the list of posts waiting to be voted.
[PT-BR]
Agora vamos ver como ficou o comando showqueue. É bem parecido com o comando listcurators. Vai retornar a lista de posts esperando para serem votados.

    if message.content.startswith('showqueue'):
        # check if message comes from curator
        if not isCurator(message):
            await message.channel.send("Not Authorized!")
            return

        # search the database for posts with "waiting" status ordered by creation date
        result = supabase.table(dbQueueTable).select("*").eq("status", "waiting").order("post_created").execute().data
        responseText = 'Posts List:\n>>> '
        for post in result:
            responseText = responseText + "Post: " + str(post["link"]) + '\n'
        await message.channel.send(responseText)
        return

[EN]
With this we finished the first item, we can manage posts in the database.
Now we need to move to item 2, and have a script run every minute looking at the curation account voting power to cast a vote when the voting power reaches 99%.
I did a quick search on Google looking for a solution, since I have seen bots running scripts in timed loops. I found this discord.py extension called Tasks. This is exactly what I was looking for, as discord.ext.tasks will run a loop in the background at a specific interval. I will also need to import it at the start of the script.
Tasks is a separate object from Client, so this part of the code will not be inside the on_message() function. I will use tasks to create a function that will run every minute, and I will start this function inside the on_ready() function from Client. The on_ready() function runs once when you load the bot, and will trigger the loop.
Inside the loop, I will need to use the module Account to check the voting power in the curation account. If voting power is 99%, I will search the database for the first post in the queue to be voted, and will use the module Comment to cast the vote, and to finish I will update the database to set the status of the post from "waiting" to "upvoted".
To be able to use Comment to upvote, I need to create an instance of the object Hive, a connection to the blockchain, holding the posting key of the curation account, and we will use set_shared_blockchain_instance() to make this Hive instance shared with the other classes. Once we do this, our Comment instance will use this Hive connection.
I will add this code together where I get my environment variables. I will also add 2 more environment variables to my .env file: CURATION_ACCOUNT and CURATION_POSTING_KEY.
Here are the 2 blocks of code, one goes where the environment variables are, and the other at the very end of the script.
[PT-BR]
Com isso terminamos o primeiro item, nós conseguimos gerenciar os posts no banco de dados.
Agora vamos para o item 2, e temos que rodar um script a cada minuto olhando para o poder de voto da conta de curadoria, para fazer um voto quando o poder de voto chegar a 99%.
Eu fiz uma busca rapida no Google procurando uma solução, pois ja vi outros bots rodando scripts em loops de tempo. Eu encontrei essa extenção do discord.py chamada Tasks. Isso é exatamente o que eu estava procurando, discord.ext.tasks vai rodar um loop no fundo em um intervalo específico. Também vou precisar importar essa classe no inicio do script. Tasks é um objeto separado de Client, então essa parte do código não vai ficar dentro da função on_message(). Eu vou usar tasks para criar uma função que vai rodar a cada minuto, eu vou iniciar essa função dentro da função on_ready() da Client. A função on_ready() roda apenas uma vez, quando o bot é iniciado, e ela vai ser o gatilho pra iniciar o loop.
Dentro do loop, eu vou precisar do modulo Account para checar se o poder de voto chegou a 99%, eu vou buscar no banco de dados pelo primeiro post da fila para ser votado, e vou usar o módulo Comment para fazer o voto, e para finalizar eu vou atualizar o banco de dados, trocando o status do post de "waiting" para "upvoted".
Para poder usar o Comment para fazer o upvote, eu preciso criar uma instancia do objeto Hive, que é uma conexão com o blockchain, e vai guardar o posting key da conta de curadoria. Depois vou usar set_shared_blockchain_instance() para fazer essa instancia de Hive disponível em outras classes. Quando fazemos isso, a instancia de Comment passa a usar essa conexão com o posting key.
Eu vou colocar esse código junto com as variaveis de ambiente. Eu também vou adicionar mais duas variaveis de ambiente no meu arquivo .env: CURATION_ACCOUNT and CURATION_POSTING_KEY.
Aqui estão os dois blocos de código, um fica no inicio do código junto com as variaveis de ambiente, e o outro bem no final do código.

curationAccountName = os.environ.get("CURATION_ACCOUNT")
curationAccountPostingKey = os.environ.get("CURATION_POSTING_KEY")
hive = Hive(node=['https://api.hive.blog'], keys={'posting':curationAccountPostingKey})
set_shared_blockchain_instance(hive)
@tasks.loop(seconds = 60)
async def myLoop():
    curationAccount = Account(curationAccountName)
    if curationAccount.get_voting_power() >= 99:
        post = supabase.table(dbQueueTable).select("*").eq("status", "waiting").order("post_created").execute().data[0]
        try:
            return
            c = Comment(post["link"])
            c.upvote(post["vote_weight"], voter=curationAccountName)
            supabase.table(dbQueueTable).update({"status": "upvoted"}).eq("id", post["id"]).execute()
        except:
            return

[EN]
This will complete our bot. I know it's very simple, but it can be the start of a bigger project. At least it works as is. Now let's check out the complete code.
[PT-BR]
Com isso completamos o nosso bot. Eu sei que é bem simples, mas pode ser o inicio de algo maior. Pelo menos funciona como esta. Agora vamos ver o codigo completo.

import os
import discord
from discord.ext import tasks
from dotenv import load_dotenv
from supabase import create_client
from beem import Hive
from beem.account import Account
from beem.comment import Comment
from beem.utils import resolve_authorperm, construct_authorperm
from beem.instance import set_shared_blockchain_instance

load_dotenv()

discordToken = os.environ.get("DISCORD_TOKEN")
discordAdmin = os.environ.get("DISCORD_ADMIN_ID")

supabaseURL = os.environ.get("SUPABASE_URL")
supabaseKEY = os.environ.get("SUPABASE_KEY")
dbQueueTable = "curation_queue"
dbCuratorsTable = "curation_curators"
supabase = create_client(supabaseURL, supabaseKEY)

curationAccountName = os.environ.get("CURATION_ACCOUNT")
curationAccountPostingKey = os.environ.get("CURATION_POSTING_KEY")
hive = Hive(node=['https://api.hive.blog'], keys={'posting':curationAccountPostingKey})
set_shared_blockchain_instance(hive)

intents = discord.Intents.all()
client = discord.Client(command_prefix='!', intents=intents)

def isCurator(message):
    discordUserId = message.author.id
    checkUser = supabase.table(dbCuratorsTable).select("*", count="exact").eq("discord_id", discordUserId).execute()
    if checkUser.count == 0:
        return False
    else:
        return True
 
@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))
    myLoop.start()
 
@client.event
async def on_message(message):
    if message.author == client.user:
        return
 
    if message.content.startswith('hi'):
        await message.channel.send(message.author)

    if message.content.startswith('addcurator'):
        if (str(message.author.id) == discordAdmin):
            params = message.content.split()
            discordUserId = message.mentions[0].id
            hiveUser = params[2]
            checkUser = supabase.table(dbCuratorsTable).select("*", count="exact").eq("discord_id", discordUserId).execute()
            if checkUser.count == 0:
                supabase.table(dbCuratorsTable).insert({"discord_id": discordUserId, "hive_id": hiveUser}).execute()
                responseText = "User: <@" + str(discordUserId) + "> added to curation list"      
                await message.channel.send(responseText)
            else:
                await message.channel.send("Error: Discord user is already a curator")
        else:
            await message.channel.send("Not Authorized")

    if message.content.startswith('listcurators'):
        if (str(message.author.id) == discordAdmin):
            result = supabase.table(dbCuratorsTable).select("*").execute().data
            responseText = 'Curators List:\n>>> '
            for curator in result:
                responseText = responseText + "discord: <@" + str(curator["discord_id"]) + ">" + " => Hive: @" + str(curator["hive_id"]) + '\n'
                
            await message.channel.send(responseText)
            return
        else:
            await message.channel.send("Not Authorized")
            return
    
    if message.content.startswith('removecurator'):
        if (str(message.author.id) == discordAdmin):
            discordUserId = message.mentions[0].id
            checkUser = supabase.table(dbCuratorsTable).select("*", count="exact").eq("discord_id", discordUserId).execute()
            if checkUser.count == 0:
                await message.channel.send("This user is not a curator")
            else:
                result = supabase.table(dbCuratorsTable).delete().eq("discord_id", discordUserId).execute()
                responseText = "User: <@" + str(discordUserId) + "> removed from curation list"
                await message.channel.send(responseText)
            return
        else:
            await message.channel.send("Not Authorized")
            return

    if message.content.startswith('upvote'):
        if not isCurator(message):
            await message.channel.send("Not Authorized!")
            return

        params = message.content.split()
        try:
            author, permlink = resolve_authorperm(params[1])
            authorperm = construct_authorperm(author, permlink)
            checkLink = supabase.table(dbQueueTable).select("*", count="exact").eq("link", authorperm).execute()
            if checkLink.count == 0:
                c = Comment(authorperm)
                voteWeight = params[2]
                supabase.table(dbQueueTable).insert({"curator": message.author.id,"link": authorperm, "vote_weight": voteWeight, "post_created": str(c["created"]), "status": "waiting"}).execute()
                await message.channel.send(type(c["created"]))
            else:
                await message.channel.send("Post already in queue!")
        except:
            await message.channel.send("Invalid Link!")
 
    if message.content.startswith('removepost'):
        if not isCurator(message):
            await message.channel.send("Not Authorized!")
            return

        params = message.content.split()
        try:
            author, permlink = resolve_authorperm(params[1])
            authorperm = construct_authorperm(author, permlink)
            checkLink = supabase.table(dbQueueTable).select("*", count="exact").eq("link", authorperm).execute()
            if checkLink.count == 0:
               await message.channel.send("This link is not in queue")
            else:
                result = supabase.table(dbQueueTable).delete().eq("link", authorperm).execute()
                responseText = "Link removed from queue successfully"
                await message.channel.send(responseText)
            return
        except:
            await message.channel.send("Invalid Link!")
        
    if message.content.startswith('showqueue'):
        if not isCurator(message):
            await message.channel.send("Not Authorized!")
            return

        result = supabase.table(dbQueueTable).select("*").eq("status", "waiting").order("post_created").execute().data
        responseText = 'Posts List:\n>>> '
        for post in result:
            responseText = responseText + "Post: " + str(post["link"]) + '\n'
        await message.channel.send(responseText)
        return

@tasks.loop(seconds = 60)
async def myLoop():
    curationAccount = Account(curationAccountName)
    if curationAccount.get_voting_power() <= 80:
        post = supabase.table(dbQueueTable).select("*").eq("status", "waiting").order("post_created").execute().data[0]
        try:
            c = Comment(post["link"])
            c.upvote(post["vote_weight"], voter=curationAccountName)
            supabase.table(dbQueueTable).update({"status": "upvoted"}).eq("id", post["id"]).execute()
        except:
            return
 
client.run(discordToken)

[EN]
I hope this can help people trying to learn to code. I think that creating a discord bot is a fun way to learn to code. Feel free to ask any questions.

If you have an extra vote for witness, please consider voting for my witness by clicking here

See you next time!!

[PT-BR]
Espero que este post ajude outras pessoas aprendendo a programar. Eu acho que criar um bot para discord é uma forma divertida de aprender a programar. Fique a vontade para fazer qualquer pergunta.

Se voce tem um voto para testemunha sobrando, por favor considere me dar um voto clicando aqui

Até a próxima!!!



0
0
0.000
8 comments
avatar

Que legal! Vou tentar implementar para aprender mais sobre Supabase. Como amante do open source, vale a pena dar uma estudada. Me tira uma dúvida: essa é a parte final? Quando estou estudando algo, gosto de ler tudo de uma vez e já colocar para rodar, kkkk, em vez de ler por partes. Sei lá, acho que aprendo melhor assim. Vou tentar colocar para rodar com fins de estudo. Uma dica para quem quiser hospedar o bot depois: quem tiver pouco conhecimento, recomendo o uso do [discloud(https://discloudbot.com/). Não sei se tem suporte ao Supabase lá, mas se não tiver, é só pedir para o "Pedro Ricardo - PR". Foi com ele que aprendi o básico do Discord.js. Lá no canal dele, ele é super atencioso e está sempre disposto a ajudar. Para quem tem mais conhecimento, outra dica é usar o tier free da Oracle Cloud. Fiquei usando por 1 ano uma VPS de 24 gigas de RAM totalmente grátis. E por um erro meu, eu a excluí. Agora só posso criar com menos RAM, mesmo assim estou usando VPS da Oracle Cloud totalmente grátis 24/7. Recomendo.

0
0
0.000
avatar

Esse é o ultimo post, o bot ja funciona assim, mas com certeza pode ser melhorado. Se seu interesse é só testar o Supabase, voce nem precisa dessa terceira parte, basta parar na segunda.
Pra hospedar o bot gratis, uma boa opção é https://heliohost.org/
Qualquer hospedagem serve para usar o Supabase, nao precisa ter suporte, pq vc acessa por uma API. Vou dar uma olhada no Oracle Cloud, mas se vc ja tem uma VPS gratis, entao voce ja tem hospedagem pro bot.
Se voce tiver qualquer duvida quando for usar, me avisa.

0
0
0.000
avatar

Ahh, entendi Via API, bem interessante, isso Que dar não ler e deixar para ler tudo depois. Peguei o assunto pela metade, kkkkkkk. Assim que tiver tempo, vou brincar com isso. Ah, desculpa aê

0
0
0.000
avatar

kkkk desculpa o que?? tmj
se tiver alguma duvida no código, avisa aqui ou no discord

0
0
0.000
avatar

Nice one @mengao! I definitely learned something new again!
Even if this is for fun, it’s already a working bot that communities can use. Good job!

0
0
0.000
avatar

Thank you! It would be cool to see someone actually using for curation. I’m happy to help setting it up if anyone needs it.

0
0
0.000