Разработка персональных ботов для Голоса. Урок 3.
В этом уроке у нас появляется работающий бейби бот, который умеет повторять голоса @academy и/или других аккаунтов.
Предыдущие посты
Предисловие
- Обязательно задавайте вопросы в комментариях, если я что-то непонятно объясняю!
- Требуется базовый уровень понимания JavaScript, веб технологий и командной строки.
- У меня минимальный опыт работы с русскоязычной терминологией в программировании, поэтому названия я буду оставлять на английском языке.
- В этом уроке используется неоптимальный код, паттерны и структура, в приоритете находится простота и читаемость кода.
На чем мы остановились
В прошлый раз мы закончили разработку нашего бейби бота на том, что научились определять операции голосования за посты и обрабатывать их. Прежде, чем мы приступим к следующей части нам необходимо рассмотреть вопрос с ключами аккаунтов.
Некоторые важные факты о ключах
- любой аккаунт Голоса управляется криптографическими ключами
- есть 4 типа ключей: постинг, активный, собственника и мемо
- публичные ключи начинаются на
GLS
и видны всем в блокчейне - приватные ключи начинаются на
5
- свой приватный ключ можно найти по ссылке https://golos.io/@ТВОЙ_АККАУНТ/permissions
- постинг ключ авторизирует только следующие операции: голос, пост, комментарий, реблог
- активный ключ авторизирует финансовые операции с токенами, голосование за делегатов и смену аватарки :D
- как для бота, так и для сайта golos.io используйте только постинг ключ
- сохраните в нескольких места ваш ключ собственника или owner key и не используйте его вообще, он может понадобится только для сброса постинг или активного ключа
Настраиваем ключи и аккаунт
Сейчас мы будем учиться программировать бота делать действия за нас. Код бейби бота в предыдущих уроках не зависил от аккаунта пользователя бота. Поэтому мы детально разберем кода с настройкой пользовательсих данных.
const accountName = 'ontofractal' // аккаунт пользователя, который запускает бота
const postingKey = process.env.GOLOS_POSTING_KEY // предпочтительный вариант: используем environment variable для доступа к приватному постинг ключу
// const postingKey = '5K...' // альтернативный вариант: вводим приватный ключ прямо в код
const accountVotesToFollow = ['academy'] // array аккаунтов, голоса которых мы будем повторять
Environment variable -- переменная среды или переменная операционной системы, хранящая какую-либо информацию — например, данные о настройках системы в виде строки (текста).
У программ есть доступ к переменным среды, в node.js это происходит с помощью глобальной переменной process
: process.env.GOLOS_POSTING_KEY
В Linux и MacOS стандатный шелл в терминалах называется bash. Задание env(сокращенно от environment) variable выглядит так: export GOLOS_POSTING_KEY=5...
Для пользователей Windows больше информации можно найти тут.
Стандартной практикой обеспеченя безопасности секретов (всевозможных ключей и приватных данных) является отсутствие секретов в коде. Секреты останутся секретами, если сервер взломают, доступ к коду получит подрядчик или программист случайно откроет публичный доступ к репозиторию. На гитхабе присутствуют боты, которые сканируют все новые коммиты, находят забытые там ключи доступа, например, к Amazon AWS, программно создают новые сервера и майнят криптовалюты. Владельцы взломанных аккаунтов получают счета на тысячи долларов.
Что на самом деле значит "проголосовать"
Мы подготовили необходимую информацию: аккаунт, ключ, список аккаунтов, чьи голоса будет повторять бот. У нас уже есть код, который обрабатывает и выводит на экран всю необходимую нам информацию о новых голосах на блокчейне. Теперь мы изучим, как можно программным методом проголосовать за пост или комментарий.
С точки зрения блокчейна, "проголосовать" означает отправить на ноду (сервер) Голоса транзакцию с операцией vote
, подписанную одним из приватных ключей аккаунта, от имени которого совершается операция vote.
Примеры транзакции и операции
Транзакция, включающая в себя операцию голосования выглядит так:
[{"expiration": "2017-01-29T03:14:15", "extensions": [], "operations": [["vote", {"voter": "academy", "author": "qqc", "weight": 10000, "permlink": "javascript-urok-1"}]], "signatures": ["2071b95a146da86a7e33d976a3e1c7e428aef663a1f0d5b6d62f5bb564da4f7555733c688d87f118627b149a88bd8e0f03008385b262885105b3aa195320b6c2a0"], "ref_block_num": 2250, "ref_block_prefix": 2576683073}]
Сама операция имеет следующую форму/вид: ["vote", {"voter": "academy", "author": "qqc", "weight": 10000, "permlink": "javascript-urok-1"}]
Как видим, в самой операции есть только несколько параметров: голосующий аккаунт, автор, пермлинк и вес (не сила) голоса. А вот уже в транзакции есть криптографические подписи и метаданные необходимые для проверки и включения транзакции в блокчейн. Для автоматического создания подписей и отправки транзакции на ноду мы продолжим использовать библиотеку golos.js (мой порт steemjs )
Передаем данные операции голосования на ноду
Так выглядит функция передачи данных голоса на ноду:
golos.broadcast.vote(wif, voter, author, permlink, weight, function(err, result) {
console.log(err, result);
});
Разберем параметры, которая принимает метод vote
:
wif
: ключ голосующего пользователяvoter
: аккаунт голосующего пользователяauthor
: автор постаpermlink
: постоянная ссылкаweight
(вес данного голоса от 1 до 10000)
Пара author
и permlink
являются уникальными идентификаторами контента в блокчейне, как для постов, так и для комментариев. Если запросить данные о посте или комментарии с помощью метода golos.api.getContent(author, permlink)
то результатом будет JS объект, где в том числе будет содержаться property (свойство) id
. Так вот, id
(несмотря на свое название) не является уникальным, неизменным идентификатором постов. В одном из предыдущих хард форков Стима формат и значение id
изменилось для всех постов и комментариев.
Функция реагирования на новые голоса
const reactToIncomingVotes = (voteData) => {
const {voter, author, permlink, weight} = voteData
// проверяем входит ли проголосовавший аккаунт в список
const isMatchingVoter = accountVotesToFollow.includes(voter)
// проверяем не является ли это флагом, т.е. имеет вес ниже 0
// если сделать строго больше 0, то голоса не будут сниматься, даже если аккаунт убрал свой голос за пост
const isMatchingWeight = weight >= 0
if (isMatchingVoter && isMatchingWeight) {
golos.broadcast.vote(postingKey, accountName, author, permlink, weight, (err, result) => {
if (err) {
console.log('===========ПРОИЗОШЛА ОШИБКА, БОТ НЕ ГОЛОСОВАЛ===========')
console.log(err)
} else {
// используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
console.log(`@${accountName} проголосовал за пост ${permlink} написанный @${author} c весом ${weight} копируя голос ${voter}`)
}
})
}
}
Разберем главную функцию нашего урока. Она принимает данные о происходящих на блокчейне Голоса операциях vote
, проверяет их на соответствие правилам, повторяет голос и выводит результат на экран.
const {voter, author, permlink, weight}
использует удобную для повышения читаемости кода фичу ES2016 -- деструктурирование
function(err,result){...}
-- коллбек, обычная функция которая будет вызвана библиотекой golos
, а именно внутренней имплементацией метода golos.broadcast.vote
после получения ответа от ноды.
В зависимости от ответа ноды, результатом будет или выполненная операция голосования за пост или ошибка. Так или иначе результат будет выведен в консоль.
Важно: в операции vote
не содержится данных о том, пост это или комментарий. соответственно и бот будет голосовать как за посты, так и за комментарии. Аккаунт @academy голосует только за посты.
Весь код
const golos = require('golos') // импортируем модуль голоса
const util = require('util') // это встроенный в node.js модуль
const Promise = require("bluebird") // импортируем модуль Bluebird -- самую популярную имплементацию Promise
const _ = require('lodash') // как уже понятно, импортируем lodash ^_^
const accountName = 'ontofractal' // аккаунт пользователя, который запускает бота
const postingKey = process.env.GOLOS_POSTING_KEY // предпочтительный вариант: используем environment variable для доступа к приватному постинг ключу
// const postingKey = '5K...' // альтернативный вариант: вводим приватный ключ прямо в код
const accountVotesToFollow = ['academy'] // array аккаунтов, голоса которых мы будем повторять
// создаем новый Promise обворачивая golos.api.getDynamicGlobalProperties
const dynamicGlobalProperties = new Promise((resolve, reject) => {
golos.api.getDynamicGlobalProperties((err, result) => {
if (err) {
reject(err)
}
else {
resolve(result)
}
})
})
const pluckBlockHeight = x => x.head_block_number
// создадим функцию, которая достанет все операции из всех транзакций блока и поместит их в array
const unnestOps = (blockData) => {
// метод map создает новый array применяя функцию переданную в первый аргумент к каждому элементу
// используем метод flatten модуля lodash для извлечения элементов из вложенных списков и помещения в одноуровней список
return _.flatten(blockData.transactions.map(tx => tx.operations))
}
const reactToIncomingVotes = (voteData) => {
const {voter, author, permlink, weight} = voteData
// проверяем входит ли проголосовавший аккаунт в список
const isMatchingVoter = accountVotesToFollow.includes(voter)
// проверяем не является ли это флагом, т.е. имеет вес ниже 0
// если сделать строго больше 0, то голоса не будут сниматься, даже если аккаунт убрал свой голос за пост
const isMatchingWeight = weight >= 0
if (isMatchingVoter && isMatchingWeight) {
golos.broadcast.vote(postingKey, accountName, author, permlink, weight, (err, result) => {
if (err) {
console.log('===========ПРОИЗОШЛА ОШИБКА, БОТ НЕ ГОЛОСОВАЛ===========')
console.log(err)
} else {
// используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
console.log(`@${accountName} проголосовал за пост ${permlink} написанный @${author} c весом ${weight} копируя голос ${voter}`)
}
})
}
}
const selectOpHandler = (op) => {
// используем destructuring, очень удобную фичу EcmaScript2016
// это, конечно, не паттерн метчинг Elixir или Elm, но все равно сильно помогает улучшить читаемость кода
const [opType, opData] = op
if (opType === 'vote') {
reactToIncomingVotes(opData)
}
}
// поменяем имя функции с getBlockData на processBlockData т.к. ее назначение изменилось
const processBlockData = height => {
golos.api.getBlock(height, (err, result) => {
if (err) {
console.log(err)
}
else {
console.log('')
console.log('============ НОВЫЙ БЛОК ============')
// console.log(result) заменим на
unnestOps(result)
// в отличие от map, метод forEach не возвращает список с результатом применения функции
// также как и map, метод forEach применяет переданную в него функцию к каждому элементу array
// forEach используется для указания того, что результат применения функции является side effect
.forEach(selectOpHandler) // передаем функцию, которая определит, что делать
}
})
}
const startFetchingBlocks = startingHeight => {
let height = startingHeight
setInterval(() => {
processBlockData(height)
height = height + 1 // брррр, мутация
// у нас есть доступ к переменной height благодаря closure
}, 3000)
// Задаем интервал в 3000 мс т.к. блок Голоса генерируется каждые три секунды
}
// резолвим Promise
dynamicGlobalProperties
.then(pluckBlockHeight)
.then(startFetchingBlocks)
.catch(e => console.log(e))
Запускаем бота для своего аккаунта
Код бота этого урока готов к запуску. Бота-куратора можно запускать как на сервере, так и на пользовательском компьютере.
Правильным способом было бы использовать git и систему контроля версий, но пока будет достаточно сделать следующее:
- создать и скопировать вручную файлы
main.js
иpackage.json
из коммита этого урока в моем репозитории - внимание: для корректной работы необходимо использовать мой модуль
ontofractal/golosjs
(в package.json) - с помощью текстового редактора настроить список аккаунтов, чьи голоса будем копировать
- запустить
npm install
. - запустить бота
node main.js
В завершающем скринкасте я покажу, как настроить бот для аккаунта @ontofractal, который будет повторять голоса топ 19 делегатов с минимальным весом.
Ссылки на блокчейн эксплорер с голосами из скринкаста, которые аккаунт ontofractal передал в блокчейн следуя списку делегатов (за время создания скринкаста голосовал только @good-karma):
- http://golosd.com/tx/d04f63a62ce84b9b6de3f569c0175d75cadad719
- http://golosd.com/tx/0da94f558b352192208429629abf283c2d12e850
- http://golosd.com/tx/ce1c085321d7db5972cbd8e48755c96587a2329e
- http://golosd.com/tx/92f4686e9062718c00be52b0a046e4c8b2d179f7
Важно
Код выпущен под MIT лицензией. Всю ответственность за использование кода вы принимаете на себя.
В следующем уроке
- продолжим добавлять важный функционал
- научимся восстаналивать работу бота при критических ошибках
- изучим возможности для деплоймента на удаленных серверах