В этом уроке мы продолжим разрабатывать персонального бота на JavaScript, изучим структуру блоков и транзакций, научимся отличать операции голосования за посты аккаунтами Голоса и обрабатывать их данные.
Предыдущие посты
Предисловие
- Требуется базовый уровень понимания JavaScript, веб технологий и командной строки.
- Задавайте вопросы в комментариях, если я что-то непонятно объясняю.
- У меня минимальный опыт работы с русскоязычной терминологией в программировании, поэтому названия я буду оставлять на английском языке.
- В этом уроке используется неоптимальный код, паттерны и структура, в приоритете находится простота и читаемость кода.
На чем мы остановились в прошлый раз?
У нас получилось построить поток новых блоков Голоса. В этом уроке у нас следующие задачи:
- изучить структуру блоков и транзакций
- отличать типы транзакций
- обрабатывать данные о голосах пользователей
Для удобства я создал репозиторий на Github, в котором каждый коммит будет соответствовать уроку. Код предыдущего урока по ссылке.
Для предварительного ознакомления
- основы функционального программирования
- система модулей nodejs
- fat arrows functions или стрелочные функции: лямбда функции в JS
- шаблонные строки или template strings
- lodash: универсальный набор утилит для JS
Изучаем структуру блоков и транзакций
Для фана и профита посмотрим на блоки и транзакции в режиме реального времени на момент написания этого поста. Для этого нам нужно кое-что поменять. В прошлом скринкасте вместо объектов и списков содержащих данные на глубoких уровнях вложенности, мы видели только что-то вроде operations: [Object]
вместо данных об операциях включенных в транзакцию. Это дефолтное поведение функции console.log
. Воспользуемся включенным в nodejs модулем utils
.
const util = require('util') // добавим в начало файла
const getBlockData = height => {
golos.api.getBlock(height, (err, result) => {
if (err) {
console.log(err)
}
else {
console.log('')
console.log('============ НОВЫЙ БЛОК ============')
// console.log(result) заменим на
console.log(util.inspect(result, {showHidden: false, depth: null}))
}
})
}
Структура блоков
Изучим структуру последнего блока в скинкасте. Транзакции в нем отсутствуют.
{ previous: '002d5b03fac21f67ea13fa5222f0e48a531397fe', // хеш поинтер на предыдущий блок
timestamp: '2017-01-29T19:58:54', // время блока*
witness: 'lehard', // делегат сгенерировавший блок
transaction_merkle_root: '0000000000000000000000000000000000000000', // корень дерева Меркле
extensions: [],
// подпись делегата созданная с помощью приватного ключа @lehard, публичный ключ которого виден на блокчейне
witness_signature: '1f0ec01cba24b62e6c2f7184b00bd7eab8b258b826d655db5fdc3764ba2006b2ed0286ef739a5d164bb4a873ef76ef9e11ddea76bc8c056a01dab89af6b0be1007',
// пустой array транзакций
transactions: [] }
*о времени блока. Времени в блокчейне не существует, timestamp проверяется на соответствие правилам нодами.
Теперь изучим структуру предпоследнего блока в скринкасте, включающего в себя только 1 транзакцию.
{ previous: '002d5b02471c498fc3d199d414408845309bff1f',
timestamp: '2017-01-29T19:58:51',
witness: 'serejandmyself',
transaction_merkle_root: 'bfad56084bf5792d94d938bc1215888b35c72b5c',
extensions: [],
// подпись делегата созданная с помощью приватного ключа @serejandmyself, публичный ключ которого виден на блокчейне
witness_signature: '201831d219f8951b6e752b715c6856e528dab67ae50f21f49b7a5f6b483fade2497ee4ee91ede89f4e24582e4da74b1601b3105740ced935a22b25e604016bcf56',
transactions:
[ { ref_block_num: 22892,
ref_block_prefix: 1807731190,
expiration: '2017-01-29T19:59:00',
operations:
[ [ 'comment',
{ parent_author: 'makgorn',
parent_permlink: 'sdelal-sam',
author: 'strecoza',
permlink: 're-makgorn-sdelal-sam-20170129t195847237z',
title: '',
body: 'классная такая корявая палка сосны, я бы тоже такой стол хотела))',
json_metadata: '{"tags":["handmade"]}' } ] ],
extensions: [],
// подпись транзакции аккаунтом @strecoza, создавшим транзакцию
signatures: [ '1f670e47bcd6d92886ab7f3bde7f32f4e8401d55b91155dcbe8deaa313451640d400ec714f0a9662a273ce5f5e39643fa432ca76f7ee5bb0a767440f966ef813e4' ] } ] }
В этом уроке нас особенно интересует структура данных на пути exampleBlock.transactions[0].operations
, где exampleBlock -- блок над этой строчкой.
[ [
// тип операции
'comment',
// данные операции
{ parent_author: 'makgorn',
parent_permlink: 'sdelal-sam',
author: 'strecoza',
permlink: 're-makgorn-sdelal-sam-20170129t195847237z',
title: '',
body: 'классная такая корявая палка сосны, я бы тоже такой стол хотела))',
json_metadata: '{"tags":["handmade"]}' } ] ]
Каждая операция имеет форму [opType, opData]
, где opType -- строка, а opData -- объект.
Мы теперь знаем, что операции находятся в транзакциях (их в блоке может быть несколько). В большинстве транзакций только одна операция, но может быть и несколько. Насколько я помню тут есть какая-то особенность, если кто-то знает о ней -- напишите в комментариях.
Теперь напишем код для обработки операций.
// создадим функцию, которая достанет все операции из всех транзакций блока и поместит их в array
const unnestOps = (blockData) => {
// метод map создает новый array применяя функцию переданную в первый аргумент к каждому элементу
// используем метод flatten модуля lodash для уплощения(?!) вложенных списков
return _.flatten(blockData.transactions.map(tx => tx.operations))
}
// поменяем имя функции с 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(op => console.log(util.inspect(op, {showHidden: false, depth: null})))
}
})
}
У нас получилось выводить на экран данные каждой операции в блоке. Теперт создадим функцию, которая будет определять тип транзакции и в соответствии с типом операции выберет подходящее действие. В нашем случае тип операции будет vote
и пока мы продолжим выводить данные.
Форма данных операции vote
выглядит так: {"voter": "exampleVoter", "author": "exampleAuthor", "permlink":"examplePermlink", "weight":"10000" }
const selectOpHandler = (op) => {
// используем destructuring, очень удобную фичу EcmaScript2016
// это, конечно, не паттерн метчинг Elixir или Elm, но все равно сильно помогает улучшить читаемость кода
const [opType, opData] = op
if (opType === 'vote') {
// используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
console.log(`@${opData.voter} проголосовал за пост ${opData.permlink} написанный @${opData.author} c весом ${opData.weight}`)
}
}
Весь код
const golos = require('golos') // импортируем модуль голоса
const util = require('util')
const Promise = require("bluebird") // импортируем модуль Bluebird -- самую популярную имплементацию Promise
const _ = require('lodash')
const accountName = '' // аккаунт пользователя, который запускает бота
const postingKey = '' // приватный постинг ключ пользователя, который запускает бота
const accountNameToFollow = 'academy' // аккаунт пользователя за которым следим
// создаем новый 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 selectOpHandler = (op) => {
// используем destructuring, очень удобную фичу EcmaScript2016
// это, конечно, не паттерн метчинг Elixir или Elm, но все равно сильно помогает улучшить читаемость кода
const [opType, opData] = op
if (opType === 'vote') {
// используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
console.log(`@${opData.voter} проголосовал за пост ${opData.permlink} написанный @${opData.author} c весом ${opData.weight}`)
}
}
// поменяем имя функции с 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))
Как выглядит наш работающий бейби бот
Заключение
Мы научились "распаковывать" блоки, доставать операции, определять операцию "vote" и выводить отформатированные голоса аккаунтов.
Финальный код также опубликован на Гитхабе в коммите этого урока.
В следующем уроке мы научимся:
- как передавать операции на ноды Голоса
- как голосовать за посты
- как повторять голоса @academy или другого аккаунта
Если у вас, как и у меня, будет ругаться на квадратную скобку после const в строке 'const [opType, opData] = op' , проверьте версию своего node.js. У меня на Linux Cinnamon была четвертая версия в то время как на сайте https://nodejs.org доступны более современные версии. Воспользовавшись методом №2 с этого отсюда, я обновил свой node.js и код заработал как положено. Респект и уважуха автору за урок.
Спасибо! :)
)))... а я как подопытный кролик...))) ну да, 320 постов за неделю))))))
контора пишет))
Жду продолжения)
Продолжение следует)
На днях зарегистрировался на Голосе, пока разбираюсь, что здесь происходит, но уже смело могу заявить, что более полезного блога, чем ваш, не видел! У вас много времени занимает подготовка таких публикаций?
Спасибо, но на площадке уже много полезных блогов кроме моего :) Сейчас время подготовки одного урока занимает, наверное, часов 6-8. Но если учитывать время инвестированное в изучение материала для этих уроков, то месяцы.
P.S. приветствую магистра Ордена Дырявой Чаши :D
Приятно познакомиться с еще одним жителем Ехо ) 6-8 часов - это впечатляет...
Спасибо, у вас очень полезные уроки, которые помогают с API голоса!
Кроме ботов можно наделать еще массу полезных вещей.
Пользуясь случаям спрошу - случился примитивный ступор в переборе объектов:
result.transactions[0].operations[0][0]
Мой триггер. Получаю вид операции - апвот, коммент и т.д.
result.transactions[0].operations[0][1].voter
Голосующий
result.transactions[0].operations[0][1].author
Тот за кого голосуют или в случае комментария - комментатор
result.transactions[0].operations[0][1].permlink
Ссылка на пост, за который голосовали или комментарий в случае триггер комментария
result.transactions[0].operations[0][1].parent_author
Ссылка на пост, который прокомментировали
Все выше работает корректно возвращая нужные мне данные.
Но никак не могу получить доступ к ИМЯ1 и ИМЯ2 из транзакции фолловинга custom_json:
["custom_json",{"required_auths":[],"required_posting_auths":["ИМЯ1"],"id":"follow","json":"[\"follow\",{\"follower\":\"ИМЯ1\",\"following\":\"ИМЯ2\",\"what\":[\"blog\"]}]"}]
Киньте строчкой кто-то :)
Спасибо за отзыв! :)
Лови скринкаст, увидишь в чем был трабл с транзакцией custom_json