Разработка персональных ботов для Голоса. Урок 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 и систему контроля версий, но пока будет достаточно сделать следующее:

  1. создать и скопировать вручную файлы main.js и package.json из коммита этого урока в моем репозитории
  2. внимание: для корректной работы необходимо использовать мой модуль ontofractal/golosjs (в package.json)
  3. с помощью текстового редактора настроить список аккаунтов, чьи голоса будем копировать
  4. запустить npm install.
  5. запустить бота node main.js

В завершающем скринкасте я покажу, как настроить бот для аккаунта @ontofractal, который будет повторять голоса топ 19 делегатов с минимальным весом.

asciicast

Ссылки на блокчейн эксплорер с голосами из скринкаста, которые аккаунт ontofractal передал в блокчейн следуя списку делегатов (за время создания скринкаста голосовал только @good-karma):

Важно

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

В следующем уроке

  • продолжим добавлять важный функционал
  • научимся восстаналивать работу бота при критических ошибках
  • изучим возможности для деплоймента на удаленных серверах

(∩`-´)⊃━炎炎炎炎炎

голосакадемияпрограммированиеботы
131
4088.773 GOLOS
1
В избранное
ontofractal
На Golos с 2016 M09
131
1

Зарегистрируйтесь, чтобы проголосовать за пост или написать комментарий

Авторы получают вознаграждение, когда пользователи голосуют за их посты. Голосующие читатели также получают вознаграждение за свои голоса.

Зарегистрироваться
Комментарии (48)
Сортировать по:
Сначала старые