Уважаемые пользователи Голос!
Сайт доступен в режиме «чтение» до сентября 2020 года. Операции с токенами Golos, Cyber можно проводить, используя альтернативные клиенты или через эксплорер Cyberway. Подробности здесь: https://golos.io/@goloscore/operacii-s-tokenami-golos-cyber-1594822432061
С уважением, команда “Голос”
GOLOS
RU
EN
UA
ontofractal
7 лет назад

Разработка персональных ботов для Голоса. Урок 5: автономное голосование и более удобное управление


Сегодня наш бот получает необходимый функционал для полноценной работы. Этот пост больше и длиннее предыдущих, но разбивать его на отдельные уроки нет смысла: мы добавим автономное голосование за авторов, удобное обновление настроек бота и практически исключим возможность повторного голосования за посты.

Предыдущие уроки

Внимание! Для корректной работы проверяйте, чтобы package.json зависимость golos выглядела так: "golos":"ontofractal/golosjs"

Предисловие

  • Обязательно задавайте вопросы в комментариях или пишите в http://chat.golos.io, если я что-то непонятно объясняю!
  • Требуется базовый уровень понимания JavaScript, веб технологий и командной строки.
  • У меня минимальный опыт работы с русскоязычной терминологией в программировании, поэтому некоторые названия я буду оставлять на английском языке.
  • В этом уроке используется неоптимальный код, паттерны и структура, в приоритете находится простота и читаемость кода.

Бот куратор

В прошлых уроках у нас было только одно правило -- копирование голосов. В этом мы добавим второе правило, активирующее возможность автономной поддержки авторов.
Автономное голосование отлично подходит тем, кто "вознаграждает авторов, а не лайкает посты".
Совместить работу бота с обеспечением качества кураторства несложно: достаточно просматривать посты за которые проголосовал бот и снимать голос, если пост оказался неудовлетворительного качества. Удалять из списка автономного голосования в случае повторных низкокачественных постов.

Обновление архитектуры бота

В предыдущих уроках настройки бота можно было поменять только в самом коде или при запуске докер контейнера. Это не совсем практично.
Нам нужен удобный "интерфейс" для изменений настроек бота. Для этого мы используем простой, но познавательный метод: список аккаунтов будет размещен на github в виде gist и будем обновляться с помощью HTTP запроса.

Для внедрения этого функционала нам нужно принять важное архитектурное решение. До этого момента наш бот был "stateless" системой, не имея изменяющегося внутреннего состояния.
Исключая баги в имплементации, реакция бота на одинаковые события блокчейна была бы идентичной.
Теперь бот становится "stateful" системой, регулярно обновляя данные о списке аккаунтов из внешнего источника. Реакция бота на события блокчейна будет отличаться в зависимости от внутрненнего состояния программы, в данном случае списка аккаунтов для автономного голосования.

Обновление структуры кода

Код бота усложняется, поэтому для удобства и улучшения читаемости кода, мы разделим код бота на три файла:

  • config.js для всех настроек оператора бота
  • state_manager.js для управления и обновления состояния (списков аккаунтов)
  • main.js код бота для взаимодействия с блокчейном

Конфигурация

const operatorAccountName = process.env.GOLOS_OPERATOR_ACCOUNT // аккаунт оператора бота
// const operatorPostingKey = '5K...' //  альтернативный вариант: вводим приватный ключ прямо в код
const operatorPostingKey = process.env.GOLOS_POSTING_KEY // предпочтительный вариант: используем environment variable для доступа к приватному постинг ключу
// нам нужна  ссылка на raw gist, запрос к которой возвращает только текст внутри gist-а (без HTML страницы github)
const accountsToUpvoteGistUrl = process.env.GOLOS_ACCOUNTS_TO_UPVOTE_GIST_URL
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Object_initializer
// используем кратку форму записи объектов, где property равняется переменной, а value ES2015
module.exports = {operatorAccountName, operatorPostingKey, accountsToUpvoteGistUrl}

Управление состоянием

Используем популярную библиотеку request для HTTP запросов.

const config = require('./config.js')
const golos = require('golos') // импортируем модуль голоса
const request = require('request')
// используем destructuring
const {operatorAccountName, accountsToUpvoteGistUrl} = config
let accountPostsToUpvote = []


// управление и обновление списка аккаунтов, чьи голоса бот должен копировать
// может быть настроено аналогично
const accountVotesToCopy = process.env.GOLOS_ACCOUNT_VOTES_TO_FOLLOW.split(',')
console.log(`Бот будет повторять голоса следующих аккаунтов: ${accountVotesToCopy}`)

const botState = {accountPostsToUpvote, accountVotesToCopy}

const updateAccountPostsToUpvoteFromGist = function () {
    request(accountsToUpvoteGistUrl, (error, response, body) => {
        // если http запрос не будет успешным, то список не обновится до следующего вызова функции
        if (!error && response.statusCode === 200) {
            console.log(`Успешно обновлен список аккаунтов для автономного голосования: ${accountPostsToUpvote}`)
            botState.accountPostsToUpvote = body.split(',') // используем closure для мутации botState
        }
    })
}


// при импортировании файла во время require('./state_manager') в main.js следующие функции будут автоматически вызываны
// для использования фолловингов в качестве списка для автономного голосования следует
// заменить updateAccountPostsToUpvote на функцию updateAccountPostsToUpvoteFromFollowings
updateAccountPostsToUpvoteFromGist()
setInterval(updateAccountPostsToUpvoteFromGist, 30 * 60 * 1000)

module.exports = botState

В main.js мы присваиваем переменной референс на объект состояния бота, в котором присутствуют properties accountPostsToUpvote и accountVotesToFollow. Функции updateAccountPostsToUpvote... мутируют значения данных properties с заданным интервалом.

Проверка текущих голосов за пост

Теперь бот использует 2 правила для определения реакции на события блокчейна. Из-за взаимодействия двух правил может проявиться неожиданное поведение: повтороное голосование за один пост. Ноды принимают операцию повторного голосования за пост только при изменении веса голоса за данный пост, в процессе обнуляя кураторское вознаграждение.
Более того, алгоритм позволяет проводить операцию голосования за один пост не больше чем 6 раз (включая обнуление голоса или флаги).
Как мы можем предотвратить повторное голосование? Используем метод API get_active_votes для получения всех текущих голосов за данный пост.

Пример ответа ноды после выполнения JSONRPC вызова get_active_votes выглядит приблизительно так:

[
    {
        "percent": 10000, "reputation": "28759071217014",
        "rshares": "18897453242648", "time": "2017-01-14T09:20:21",
        "voter": "example-account", "weight": "51460692508758354"
    },
    {
        "percent": 5000, "reputation": "55869071217014",
        "rshares": "4853242648", "time": "2017-01-13T18:50:41",
        "voter": "example-account-2", "weight": "31354692508758354"
    },
{..},
...
]

Зная структуру ответа, мы можем напсать следующую функцию для проверки и голосования за пост в случае прохождения проверки.

const checkStateAndUpvotePost = (author, permlink, weight, delay) => {
    // откладываем голосование за пост на delay
    setTimeout(
        () => golos.api.getActiveVotes(
            author,
            permlink,
            (err, result) => {
                // проверяем есть ли в списке активных голосов аккаунт оператора
                const operatorHasVoted = result.map(x => x.voter).includes(operatorAccountName)
                // если нет JSONRPC запроса и аккаунт оператора не голосовал --> проголосовать
                if (!err && !operatorHasVoted) {
                    // передаем данные для голосования на ноду
                    golos.broadcast.vote(operatorPostingKey, operatorAccountName, author, permlink, weight, (err, result) => {
                        if (err) {
                            console.log('произошла ошибка с передачей голоса на ноду:')
                            console.log(err)
                        } else {
                            // используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
                            console.log(`@${operatorAccountName} проголосовал за пост ${permlink} написанный @${author} c весом ${weight}`)
                        }
                    })
                } else if (operatorHasVoted) {
                    // пишем лог, если оператор уже голосовал за этот пост/комментарий
                    console.log(`Изебгая повтора, бот не проголосовал за пост ${permlink} написанный ${author}`)
                }
            }
        ),
        delay
    )
}

Реагируем на посты

У нас уже есть функция проверки/голосования за пост. Нам нужна функция реагирования на новые операции типа comment.

const reactToIncomingComments = (commentData) => {
    // console.log(commentData)
    const {author, permlink, parent_author} = commentData
    // проверяем входит ли проголосовавший аккаунт в список аккаунтов, за которые бот должен голосовать автономно
    const isApprovedAuthor = botState.accountPostsToUpvote.includes(author)
    // в блокчейне операция "comment" обозначает как посты, так и комментарии
    // у постов parent_author равняется пустой строке
    const isPost = parent_author === ''
    // задаем вес голоса по умолчанию
    const defaultWeight = 10000
    // задаем время для голосования по умолчанию через 15 минут после публикации
    const defaultDelay = 15 * 60 * 1000
    if (isApprovedAuthor && isPost) {
        console.log(`Обнаружено соответствие правилу автономного голосования: ${author} опубликовал ${permlink}`)
        checkStateAndUpvotePost(author, permlink, defaultWeight, defaultDelay)
    }
}

const selectOpHandler = (op) => {
    // используем destructuring, очень удобную фичу EcmaScript2016
    // это, конечно, не паттерн метчинг Elixir или Elm, но все равно сильно помогает улучшить читаемость кода
    const [opType, opData] = op
    if (opType === 'vote') {
        reactToIncomingVotes(opData)
    }
    else if (opType === 'comment') {
        reactToIncomingComments(opData)
    }
}

Синхронизируем список аккаунтов с подписками оператора

По запросу нескольких читателей добавляю секцию с кодом, который синхронизирует список аккаунтов для автономного голосования с текущими подписками аккаунта оператора.
Для этого используем метод API get_following

Пример результата выполнения get_following выглядит примерно так:
Список фолловингом отсортирован по алфавиту.

[ { id: '8.0.8718',
    follower: 'ontofractal',
    following: 'aleksandraz',
    what: [ 'blog' ] },
  { id: '8.0.8681',
    follower: 'ontofractal',
    following: 'alexna',
    what: [ 'blog' ] },
  {...}, ...
  ]

Используя информацию о форме ответа пример, напишем функцию для синхронизации списка аккаунтов для автономного голосования и списка фолловингов.

const updateAccountPostsToUpvoteFromFollowings = function () {
    // первый параметр: имя аккаунта,
    // второй параметр является курсором хоть так и не называется
    // в данном случае указывает с какого фолловинга начинать отсчет (по алфавитному порядку)
    // третий параметр: тип фолловинга, в этом случае 'blog'
    // четвертый  параметр: запрашиваемое количество элементов в списке, не больше 100
    golos.api.getFollowing(operatorAccountName, '', 'blog',  100, (err, result) => {
        if (err) {
            console.log("Во время JSONRPC вызова getFollowing произошла ошибка. ")
            console.log(err)
        } else {
            const followings = result.map(x => x.following)
            console.log(`Успешно обновлен список аккаунтов для автономного голосования: ${followings}`)
            botState.accountPostsToUpvote = followings // используем closure для мутации botState
        }
    })
}

Все вместе

Код уже стал слишком большим, чтобы публиковать его в посте. Вместо этого даю ссылку на commit в репозитории бейби бота.

коммит этого урока

В этом уроке мы научились синхронизировать состояние бота с внешними источниками данных для удобства управления, подключили возможность автономного голосования и обеспечили отсутствие повторного голосования.

О коллбеках

В этом уроке используется мой порт библиотеки steemjs, основным интерфейсом которого являются функции принимающие коллбеки.
Глубокие уровни вложенности коллбэков считаются анти-паттерном и ведут к "аду коллбеков", в прошлом значительной проблемой в javascript программировании. Одним из решений были Promises, которые стали частью стандарта в ES2016. Недостатки Promises для читаемости были похожи: длинные цепочки методов .then() ведут к "пирамиде ужаса"

Элегантным решением этой проблемы является использование async/await, новых кивордов, прошедших стандартизацию в ES2017. Keyword async меняет поведение функции: внутри нее можно использовать keyword await, return в async функции будет всегда возвращать Promise. Киворд await позволяет поставить на паузу выполнение async функции до окончания Promise находящегося справа от await.

C использованием async/await функция запуска нашего бота выглядела бы так:

async function startBot() {
  try {
   const props = await dynamicGlobalProperties()
   const height = pluckBlockHeight(props)
   startFetchingBlocks(height)
  } catch (e) {
   console.log(e)
  }
}

startBot()

Справка о async/await: MDN

В следующих уроках мы сделаем рефакторинг кода и заменим коллбеки async/await функциями.

Важно

Код выпущен под MIT лицензией. Всю ответственность за использование кода вы принимаете на себя.

炎炎炎炎☆┣o(・ω・ )

3
1735.869 GOLOS
Комментарии (25)
Сортировать по:
Сначала старые