[ENG/ITA] Python and Hive: My First Attempt with a Bot

in #hive-13953129 days ago

cover


La versione italiana si trova sotto quella inglese

The italian version is under the english one


Python and Hive: My First Attempt with a Bot

Let's start right away with a small premise: I am not a programmer.

I have studied anything but that in life, and my job requires no computer skills beyond knowing how to use Word, Outlook and not much more.

I have no background of any kind.

However, I always had a passion for computer science and in my spare time I often found myself trying to learn as much as I could: assembling a pc, configuring a modem, using image and video editing programs, installing an operating system, learning some HTML... in short, nothing sci-fi, but I always tried to add a little more information, expanding my knowledge as much as I could.

And it was with this goal always in mind that a few months ago I set myself a new challenge: trying to learn a programming language, such as Python.

Why Python?

Because by reading all around it seemed the most suitable for a neophyte, along with Javascript.

Python in fact is extremely “readable,” meaning that once you have learned some basics, you can literally read it as if it were text, although written in a very tight and concise manner.

For someone who didn't know anything about anything, that seemed to me to be quite a merit, and so for a few weeks I got on it and, in between workink tasks, I tried to read and learn as much as I could.

Then at work came a very tiring and busy period, and for a few months I had to put everything on hold.

Now, that I can finally enjoy some rest and quiet during the summer vacation, I decided to pick up where I left off and, with the goal of starting to put into practice what I have learned, I set out to attempt to write the code for a bot.

And not just any bot, but a bot that works on Hive!

I love Hive, so I thought it made sense to develop my first little project here.


And what does this bot do?

Something very simple and that other bots on Hive already do: as soon as the keyword (for example !CURATE) is posted in a comment, the bot detects it and, depending on the configuration chosen and whether the command comes from an enabled account, upvotes and/or comments the post under which the comment was left.

If you have ever seen the @tipu bot at work, well, this one works exactly the same way, at least in the basic behavior.

In fact, my code is very simple and for now I have not implemented any additional features that would make it even more versatile and useful.

To write it I based it on this bot created by the Hive Pizza Team, which I used as a base, after cleaning it up of features I didn't need, slightly updating some parts of the code and inserting a new function to upvote the target post.

At first I didn't know where to start, but after a few hours of work I got it working and slowly refined it to the result I wanted.


The work is over?

Absolutely not!

In fact:

  1. I haven't tested the bot extensively yet, and there are at least a couple of exceptions that I want to implement in the code so that it is even more robust;
  2. I then need to figure out how to share it on GitHub so that anyone can access it;
  3. finally, if the proper functioning is confirmed, I would like to consider creating my own small server to host it on, so that I can use it as I please.

A small dream would then be to make it accessible to anyone via a dedicated site, creating an interface with which to connect and set up the bot to one's liking... but before I get to that point, if I ever get there, it will take some time 😅


And here is the code!

After all this talk, it is time to share the code that makes up my first project, which, as mentioned above, is based on the code of a bot created by the Hive Pizza Team.


#!/usr/bin/env python3
"""A script to find and react to commands in comments"""
from beem import Hive, exceptions
from beem.blockchain import Blockchain
from beem.comment import Comment
import beem.instance
import os
import jinja2
import configparser
import time
import re

# Global configuration

BLOCK_STATE_FILE_NAME = "last_block.txt"

config = configparser.ConfigParser()
config.read("config")

ENABLE_COMMENTS = config["Global"]["ENABLE_COMMENTS"] == "True"
ENABLE_UPVOTES = config["Global"]["ENABLE_UPVOTES"] == "True"

CALLER_ACCOUNT = config["Global"]["CALLER_ACCOUNT"]
ACCOUNT_NAME = config["Global"]["ACCOUNT_NAME"]
ACCOUNT_POSTING_KEY = config["Global"]["ACCOUNT_POSTING_KEY"]
HIVE_API_NODE = config["Global"]["HIVE_API_NODE"]
HIVE = Hive(node=[HIVE_API_NODE], keys=[config["Global"]["ACCOUNT_POSTING_KEY"]])
HIVE.chain_params["chain_id"] = (
    "beeab0de00000000000000000000000000000000000000000000000000000000"
)
beem.instance.set_shared_blockchain_instance(HIVE)

BOT_COMMAND_STR = config["Global"]["BOT_COMMAND_STR"]

# END Global configuration


print("Configuration loaded:")
for section in config.keys():
    for key in config[section].keys():
        if "_key" in key:
            continue  # don't log posting keys
        print(f"{section}, {key}, {config[section][key]}")


# Markdown template for comment
comment_curation_template = jinja2.Template(
    open(os.path.join("templates", "comment_curation.template"), "r").read()
)


def get_block_number():

    if not os.path.exists(BLOCK_STATE_FILE_NAME):
        return None

    with open(BLOCK_STATE_FILE_NAME, "r") as infile:
        block_num = infile.read()
        return int(block_num)


def set_block_number(block_num):

    with open(BLOCK_STATE_FILE_NAME, "w") as outfile:
        outfile.write(f"{block_num}")


def give_upvote(parent_post, author, vote_weight):
    if ENABLE_UPVOTES:
        print("Upvoting!")
        parent_post.upvote(weight=vote_weight, voter=author)
        # sleep 3s before continuing
        time.sleep(3)
    else:
        print("Upvoting is disabled")


def post_comment(parent_post, author, comment_body):
    if ENABLE_COMMENTS:
        print("Commenting!")
        parent_post.reply(body=comment_body, author=author)
        # sleep 3s before continuing
        time.sleep(3)
    else:
        print("Posting is disabled")


def hive_comments_stream():

    blockchain = Blockchain(node=[HIVE_API_NODE])

    start_block = get_block_number()

    # loop through comments
    for op in blockchain.stream(
        opNames=["comment"], start=start_block, threading=False, thread_num=1
    ):

        set_block_number(op["block_num"])

        # skip comments that don't include the bot's command
        if BOT_COMMAND_STR not in op["body"]:
            continue

        # skip comments that don't come from an authorized caller account
        if CALLER_ACCOUNT not in op["author"]:
            continue

        # skip posts that don't have an author
        if "parent_author" not in op.keys():
            continue

        # vote weight
        if ENABLE_UPVOTES:
            command = re.compile(rf"{BOT_COMMAND_STR} (-?(100|[1-9]?[0-9]))")
            try:
                vote = re.search(command, op["body"])
                vote_weight = int(vote.group(1))
            except AttributeError:
                print("Upvote not specified!")
                continue
        else:
            vote_weight = 0

        # data of the post to be upvoted and/or replied
        author_account = op["author"]
        comment_permlink = op["permlink"]
        parent_author = op["parent_author"]
        reply_identifier = f"{parent_author}/{op['parent_permlink']}"
        terminal_message = (
            f"Found {BOT_COMMAND_STR} command: "
            f"https://peakd.com/{comment_permlink} "
            f"in block {op['block_num']}"
        )
        print(terminal_message)

        try:
            post = Comment(reply_identifier, api="condenser")
        except beem.exceptions.ContentDoesNotExistsException:
            print("Post not found!")
            continue

        # leave an upvote and/or a comment
        comment_body = comment_curation_template.render(
            target_account=parent_author,
            author_account=author_account,
        )
        give_upvote(post, ACCOUNT_NAME, vote_weight)
        post_comment(post, ACCOUNT_NAME, comment_body)


if __name__ == "__main__":

    hive_comments_stream()



This is the configuration file instead:


[Global]
; Command the bot should listen for
BOT_COMMAND_STR = !CURATE
; Disable to stop the bot from posting comments
ENABLE_COMMENTS = True
; Disable to stop the bot from upvoting
ENABLE_UPVOTES = True
; Accounts allowed to call the bot
CALLER_ACCOUNT = arc7icwolf
; Account the bot will post and upvote from
ACCOUNT_NAME = wolf-lord
; Posting key for the account
ACCOUNT_POSTING_KEY = xxxxxx
; Hive API node to use for read/write/upvote ops
HIVE_API_NODE = https://api.deathwing.me


And this is the template with which to compose the comment left by the bot:

Hi @{{target_account}}, your post has been curated by @{{author_account}}.


Of course I am open to suggestions, corrections and improvements!

I've never done anything like this, so I hope I haven't written too much nonsense or violated any rules of some kind... just in case let me know and I'll fix it :)


images property of their respective owners

to support the #OliodiBalena community, @balaenoptera is 3% beneficiary of this post


If you've read this far, thank you! If you want to leave an upvote, a reblog, a follow, a comment... well, any sign of life is really much appreciated!


Versione italiana

Italian version


cover

Python e Hive: il Mio Primo Tentativo con un Bot

Partiamo subito con una piccola premessa: non sono un programmatore.

Nella vita ho studiato tutt'altro ed il mio lavoro non richiede competenze informatiche che vadano al di là del saper utilizzare Word, Outlook e poco più.

Non ho basi di alcun tipo, perciò.

Tuttavia ho sempre avuto una passione per l'informatica e nel mio tempo libero mi sono spesso ritrovato a cercare di imparare quante più cose potevo: assemblare un pc, configurare un modem, utilizzare programmi di editing immagini e video, installare un sistema operativo, imparare un po' di HTML... insomma, niente di fantascientifico, ma nel mio piccolo ho sempre cercato di aggiungere qualche informazione in più, ampliando le mie conoscenze come potevo.

Ed è proprio con questo obiettivo sempre in testa che qualche mese fa mi sono posto una nuova sfida: provare ad imparare un linguaggio di programmazione, come Python.

Perchè Python?

Perchè leggendo a giro sembrava il più indicato per un neofita, insieme a Javascript.

Python infatti è estremamente "leggibile", nel senso che, una volta imparate un minimo di basi, lo si può letteralmente leggere come se fosse un testo, per quanto scritto in maniera molto stringata e concisa.

Per uno che non sapeva niente di niente mi è sembrato un bel pregio, e così per qualche settimana mi sono messo sotto e, tra un impegno e l'altro, ho cercato di leggere ed imparare quante più cose potevo.

Poi sul lavoro è arrivato un periodo molto faticoso e carico di impegni e per qualche mese ho dovuto mettere tutto in pausa.

Ora, che finalmente posso godermi un po' di riposo e tranquillità durante le ferie estive, ho deciso di riprendere da dove mi ero fermato e, con l'obiettivo di iniziare a mettere in pratica quel poco che ho imparato, ho deciso di provare a scrivere il codice per un bot.

E non un bot qualsiasi, ma un bot che funziona su Hive!

Adoro Hive, per cui ho pensato che avesse senso sviluppare qui il mio primo piccolo progetto.


E cosa fa questo bot?

Qualcosa di molto semplice e che già fanno altri bot presenti su Hive: nel momento in cui viene inserita in un commento la parola chiave (ad esempio !CURATE), il bot la rileva e, a seconda della configurazione scelta e se il comando proviene da un account abilitato, upvota e/o commenta il post sotto cui è stato lasciato il commento.

Se avete mai visto il bot @tipu all'opera, ecco, questo funziona esattamente alla stessa maniera, almeno nel comportamento base.

Il mio codice è infatti molto semplice e per ora non ho implementato funzioni ulteriori che possano renderlo ancora più versatile ed utile.

Per scriverlo mi sono basato su questo bot creato dal Hive Pizza Team, che ho usato come base, dopo averlo ripulito delle funzioni che non mi servivano, aggiornando leggermente alcune parti del codice ed inserendo una nuova funzione per upvotare il post bersaglio.

All'inizio non sapevo da che punto partire, ma dopo qualche ora di lavoro sono riuscito a farlo funzionare e ho piano piano perfezionato il funzionamento arrivando al risultato che volevo.


Lavoro finito?

Assolutamente no!

Infatti:

  1. non ho ancora testato in maniera estensiva il bot e ci sono almeno un paio di eccezioni che vogliono implementare nel codice, così da renderlo ancora più robusto;
  2. devo poi capire come condividerlo su GitHub, in modo da renderlo accessibile a chiunque;
  3. infine, se il corretto funzionamento fosse confermato, vorrei considerare la possibilità di crearmi un piccolo server personale su cui hostarlo, in modo da poterlo utilizzare a mio piacimento.

Un piccolo sogno sarebbe poi renderlo accessibile a chiunque tramite un apposito sito, creando un'interfaccia con cui collegarsi e settare il bot a proprio piacimento... ma prima di arrivare a questo punto, semmai ci arriverò, ce ne vorrà di tempo 😅


Ed ecco il codice!

Dopo tutte queste chiacchiere, è il momento di condividere il codice che compone il mio primo progetto, che, come detto sopra, è basato sul codice di un bot creato dal Hive Pizza Team.


#!/usr/bin/env python3
"""A script to find and react to commands in comments"""
from beem import Hive, exceptions
from beem.blockchain import Blockchain
from beem.comment import Comment
import beem.instance
import os
import jinja2
import configparser
import time
import re

# Global configuration

BLOCK_STATE_FILE_NAME = "last_block.txt"

config = configparser.ConfigParser()
config.read("config")

ENABLE_COMMENTS = config["Global"]["ENABLE_COMMENTS"] == "True"
ENABLE_UPVOTES = config["Global"]["ENABLE_UPVOTES"] == "True"

CALLER_ACCOUNT = config["Global"]["CALLER_ACCOUNT"]
ACCOUNT_NAME = config["Global"]["ACCOUNT_NAME"]
ACCOUNT_POSTING_KEY = config["Global"]["ACCOUNT_POSTING_KEY"]
HIVE_API_NODE = config["Global"]["HIVE_API_NODE"]
HIVE = Hive(node=[HIVE_API_NODE], keys=[config["Global"]["ACCOUNT_POSTING_KEY"]])
HIVE.chain_params["chain_id"] = (
    "beeab0de00000000000000000000000000000000000000000000000000000000"
)
beem.instance.set_shared_blockchain_instance(HIVE)

BOT_COMMAND_STR = config["Global"]["BOT_COMMAND_STR"]

# END Global configuration


print("Configuration loaded:")
for section in config.keys():
    for key in config[section].keys():
        if "_key" in key:
            continue  # don't log posting keys
        print(f"{section}, {key}, {config[section][key]}")


# Markdown template for comment
comment_curation_template = jinja2.Template(
    open(os.path.join("templates", "comment_curation.template"), "r").read()
)


def get_block_number():

    if not os.path.exists(BLOCK_STATE_FILE_NAME):
        return None

    with open(BLOCK_STATE_FILE_NAME, "r") as infile:
        block_num = infile.read()
        return int(block_num)


def set_block_number(block_num):

    with open(BLOCK_STATE_FILE_NAME, "w") as outfile:
        outfile.write(f"{block_num}")


def give_upvote(parent_post, author, vote_weight):
    if ENABLE_UPVOTES:
        print("Upvoting!")
        parent_post.upvote(weight=vote_weight, voter=author)
        # sleep 3s before continuing
        time.sleep(3)
    else:
        print("Upvoting is disabled")


def post_comment(parent_post, author, comment_body):
    if ENABLE_COMMENTS:
        print("Commenting!")
        parent_post.reply(body=comment_body, author=author)
        # sleep 3s before continuing
        time.sleep(3)
    else:
        print("Posting is disabled")


def hive_comments_stream():

    blockchain = Blockchain(node=[HIVE_API_NODE])

    start_block = get_block_number()

    # loop through comments
    for op in blockchain.stream(
        opNames=["comment"], start=start_block, threading=False, thread_num=1
    ):

        set_block_number(op["block_num"])

        # skip comments that don't include the bot's command
        if BOT_COMMAND_STR not in op["body"]:
            continue

        # skip comments that don't come from an authorized caller account
        if CALLER_ACCOUNT not in op["author"]:
            continue

        # skip posts that don't have an author
        if "parent_author" not in op.keys():
            continue

        # vote weight
        if ENABLE_UPVOTES:
            command = re.compile(rf"{BOT_COMMAND_STR} (-?(100|[1-9]?[0-9]))")
            try:
                vote = re.search(command, op["body"])
                vote_weight = int(vote.group(1))
            except AttributeError:
                print("Upvote not specified!")
                continue
        else:
            vote_weight = 0

        # data of the post to be upvoted and/or replied
        author_account = op["author"]
        comment_permlink = op["permlink"]
        parent_author = op["parent_author"]
        reply_identifier = f"{parent_author}/{op['parent_permlink']}"
        terminal_message = (
            f"Found {BOT_COMMAND_STR} command: "
            f"https://peakd.com/{comment_permlink} "
            f"in block {op['block_num']}"
        )
        print(terminal_message)

        try:
            post = Comment(reply_identifier, api="condenser")
        except beem.exceptions.ContentDoesNotExistsException:
            print("Post not found!")
            continue

        # leave an upvote and/or a comment
        comment_body = comment_curation_template.render(
            target_account=parent_author,
            author_account=author_account,
        )
        give_upvote(post, ACCOUNT_NAME, vote_weight)
        post_comment(post, ACCOUNT_NAME, comment_body)


if __name__ == "__main__":

    hive_comments_stream()



Questo è invece il file di configurazione:


[Global]
; Command the bot should listen for
BOT_COMMAND_STR = !CURATE
; Disable to stop the bot from posting comments
ENABLE_COMMENTS = True
; Disable to stop the bot from upvoting
ENABLE_UPVOTES = True
; Accounts allowed to call the bot
CALLER_ACCOUNT = arc7icwolf
; Account the bot will post and upvote from
ACCOUNT_NAME = wolf-lord
; Posting key for the account
ACCOUNT_POSTING_KEY = xxxxxx
; Hive API node to use for read/write/upvote ops
HIVE_API_NODE = https://api.deathwing.me


E questo il template con cui comporre il commento lasciato dal bot:

Hi @{{target_account}}, your post has been curated by @{{author_account}}.


Ovviamente sono aperto a suggerimenti, correzioni e miglioramenti!

Non ho mai fatto qualcosa del genere, per cui spero di non aver scritto troppe scemenze o violato qualche regola di qualche tipo... nel caso fatemelo sapere e provvederò a rimediare :)


immagini di proprietà dei rispettivi proprietari

a supporto della community #OliodiBalena, il 3% delle ricompense di questo post va a @balaenoptera

Se sei arrivato a leggere fin qui, grazie! Se hai voglia di lasciare un upvote, un reblog, un follow, un commento... be', un qualsiasi segnale di vita, in realtà, è molto apprezzato!

Posted Using InLeo Alpha

Sort:  

PIZZA!

$PIZZA slices delivered:
@arc7icwolf(2/10) tipped @stewie.wieno

Caspita, non mi intendo di programmazione, ma ammiro il lavoro che hai svolto!

Grazie mille! All'inizio non ci capivo nulla, ma quando ho cominciato a capire cosa dovevo fare è stato molto soddisfacente :)

Pensi che con Phyton si potrebbe creare qualcosa che aiuti la creazione di un curation trail per OdB? (Che permetta di upvotare impostando delle regole?)

Considera che Python è il linguaggio di programmazione d'eccellenza quando si tratta di automatizzare compiti ripetitivi, quindi sicuramente si potrebbe fare qualcosa!

Il problema principale è che poi lo script andrebbe hostato su un server, in modo da renderlo operativo 24/7, ma i server hanno un costo (sia che si noleggi, sia che uno lo acquisti e lo gestisca in prima persona).

Sarebbe quindi un progetto molto interessante ma che richiederebbe un po' di programmazione e valutazione di fattibilità.

!PIZZA

Loading...

First of all:

if "parent_author" not in op.keys():
continue

op.keys() is a list, and here you're checking if this key exists in the list, to determine if op is a comment or a post, but,the "parent_author" key will always be in the list, so you should check:

if op.get("parent_author") == "":
continue

Then, I would change the order of the checks: first, check if it's a comment, then if it's an authorized caller account, and finally if a command exists in the body.

Understood, thanks for your suggestions and corrections :)

That check was in the original code and I left it only because I thought it was meant to catch some bugs, as there was a comment before that check saying "do exist posts without author!?"... but now I'm going to change it as you suggested, so it would work properly.

By the way one more point you should put if you want to it alive on server so better to keep it containerized use Docker for example.
If to talk about GitHub or gitlab you can make your repo visible and maybe even open source project. Keep it up will be glad to see how is going your development work!

Thanks! I'm going to check how Docker works asap :)

You I have experience with docker but for myself it's interesting to try use it with some apps which are connected with blockchain