Сегодня наш бот получает необходимый функционал для полноценной работы. Этот пост больше и длиннее предыдущих, но разбивать его на отдельные уроки нет смысла: мы добавим автономное голосование за авторов, удобное обновление настроек бота и практически исключим возможность повторного голосования за посты.
Предыдущие уроки
Внимание! Для корректной работы проверяйте, чтобы 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 лицензией. Всю ответственность за использование кода вы принимаете на себя.
@ontofractal, кажется в коде опечатка. В строке
golos.api.getFollowings(operatorAccountName, '', 'blog', 100, (err, result) => {
в конце названия функции лишняя буква s - должно быть:
golos.api.getFollowing(operatorAccountName, '', 'blog', 100, (err, result) => {
====
Пожелание: Просьба добавить проверку на силу голоса для того чтобы бот перестал голосовать если сила голоса падает ниже определенного уровня.
Действительно это опечатка, спасибо за багрепорт! Ты имеешь в виду ограничение "voting power", да?
Да. Думаю многим понравится возможность установить в настройках минимальное значение voting power , скажем, 80%. Иначе при большом списке авторов сила голоса быстро будет падать)
@ontofractal, Поздравляю!,
Ваш пост был упомянут в моем хит-параде в следующей категории:
У скрипта есть минус - он голосует за посты которые вышли очень давно, но сейчас были отредактированны. Как получше разберусь попробую исправить =)
Не знаете, в чём может быть проблема?спустя несколько часов работы вылетает(кликабельно)
Судя по ошибке, нода возвращает
null
вместо данных о блоке. Почему это происходит? Давай посмотрим на этот код:Если нода, к которой обращается бот (публичная нода golos.io) еще не получила блок созданный и подписанный одним из делегатов (блок каждые 3 секунды + задержка в 200 мс например), то бот запросит данные блока о котором не знает нода. Соответственно результатом JSONRPC вызова будет
null
и при попытке доступа к свойствуtransactions
из переменной содержащейnull
будет та ошибка, что на скриншоте.Решением будет или автоматически перезагружать бота с помощью докера или написать код, который при результате
null
пропустит действия с блоком.Спасибо за развёрнутый ответ. буду пробовать исправить.
а можно глупые вопросы по 1 уроку позадавать? DD а то там комменты отключены уже.Это всё на винде покатит? Запускать командную строку через "шифт-Ф10" в папке с nodejs можно? или надо какой то инструмент командной строки. встал где-то тут (кликабельно)
Да, на винде тоже покатит :) Используй эмулятор консоли для этого, например, Cmder: https://github.com/cmderdev/cmder
Ошибка на скриншоте говорит, что у тебя нету git (программы управления контроля версиий), а он нужен, т.к. package.json указывает на мой git репозиторий.
В cmder git доступен по умолчанию.
А всё разобрался.Блоки полетели. =) Увековечу здесь, вдруг пригодится кому. просто скрипт надо в файл main.js пихать, файл ложить в папку с node, а в консоли просто node main.js запускать. Это из за незнания основ, но надо ж с чего-то начинать.
отлично! рад, что получилось :)
Спасибо! заработало.Ну видимо я так хрен разберусь,я дошёл до команд const из первого урока и консоль виндовская их не знает. буду убунту ставить. Цель научиться взаимодействовать с блокчейном самому и научить других, сделать пару рабочих скриптов для себя и людей далёких от программирования.
у тебя скорее всего устаревшая версия node, попробуй обновить до последней 7. https://nodejs.org/en/download/
но убунту для разработки все равно удобней, конечно :)
Напиши пожалуйста как создавать и редактировать посты.
Чисто что и как передавать в golos.broadcast.comment, а то мучаюсь с json_metadata
ок, в одном из следующих постов напишу. Насколько я помню в json_metadata должен быть не JSON объект, а строка JSON.stringify. может в этом проблема?
Вот по-видимому как раз-таки должен быть объект
А у @vik, я смотрел, строкой.
И Вас вопросами подастаю можно? DD
Можно) Если смогу-помогу)) Но сразу оговорюсь-я на линуксовом сервере экспериментирую, по особенностям запуска на винде не подскажу.
уже забабахал, всё работает, скоро запилю инструктаж для чайников. ещё бы разобраться как это дело на steemit запустить
Спасибо!ну видимо я так хрен разберусь,я дошёл до команд const из первого урока и консоль виндовская их не знает. буду убунту ставить
Это не сложно на самом деле)
@ontofractial
Я думаю, не менее актуально создавать ботов на заказ для пользователей, нежели учить=) Многим будет проще оплатить ваши услуги.
ps: Это вам, как идея.
Вообще улет. Буду разбираться на досуге.
Очень хороший пост))