Разработка персональных ботов для Голоса. Урок 3.

в прошлом году
74 в голос


В этом уроке у нас появляется работающий бейби бот, который умеет повторять голоса @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 лицензией. Всю ответственность за использование кода вы принимаете на себя.

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

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

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

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

когда-нибудь все это пойму

·
74
  ·  в прошлом году

нужно время и усилия -- и поймешь! :)

·
·

о да)

жаль что я не технарь)..

·
74
  ·  в прошлом году

это можно исправить, было бы желание)

·
·

интересно сколько на это "Требуется базовый уровень понимания JavaScript, веб технологий и командной строки" времени уйдет..

·
·
·
74
  ·  в прошлом году

Если активно прикладывать усилия, то для базового уровня необходимо от 1 до 3 месяцев.

·
·
·
·

https://steemit.com/steem/@xeroc/upvote-bot-in-less-than-10-lines-of-code
а вот такие штуки не прокатят? хотя не прокатят наверное. там без учета 30 мин.
надо бы стимит еще покопать может что и найду)

·
·
·
·
74
  ·  в прошлом году

@mrgreen:

это смотря в каком смысле "прокатят" :) вряд ли так чему-то научишься..

·
·
·
·

боооольше, если ты всю жизнь гуманитарий

64
  ·  в прошлом году

Спасибо за ценную инфу!

·
74
  ·  в прошлом году

Пиши, если есть интерес к какой-то конкретной теме :)

·
·
64
  ·  в прошлом году

Если несложно, прошу подсказать как выбрать посты по тегу.

·
·
·
74
  ·  в прошлом году

Насколько я помню, у клиента Голоса (и Стима) нету JSONRPC метода API для выборки постов по тэгу. Т.е. напрямую из нод посты по тегу не вытянешь. Не знаю почему так, казалось бы, что это базовый функционал.

·
·
·
·
74
  ·  в прошлом году

@stranger27:

по такой выборке можно, глянь доки методов библиотеки golosjs у меня в репо

·
·
·
·
64
  ·  в прошлом году

Хорошо, а можно ли тогда смотреть посты по такой выборке: новые, актуальные, трендовые ?

Спасибо! Очень познавательно и полезно

Спасибо за статью. А не подскажите где можно найти описание функций API. Вот этих https://github.com/ontofractal/golosjs/tree/master/doc Я просто не могу въехать в некоторые передаваемые аргументы им))

·
74
  ·  в прошлом году

С документацией у Стима, а соответственно и у Голоса туговато. Вот тут частично задокументированы методы API https://steemit.github.io/steemit-docs/#introduction, но я часто иду смотреть прямо в исходники https://github.com/steemit/steem/blob/master/libraries/app/database_api.cpp

кстати, давай на ты :)

·
·

Ага спасибо. Да давай. Я тоже не люблю, когда так официально общаются :)

·

И у меня тут такая ошибка вылизает. Когда добавил функционал описанный в этом уроке. Вы не сталкивались?

·
·
74
  ·  в прошлом году

Выглядит как опечатка в accoun!VotesToFollow, в коде урока accountVotesToFollow

·
·
·

Спасибо сейчас попробую исправить)

А теперь он ругается когда голосует. (npm - это ник бота. Просто первое что в голову пришло :))
Ключ для постинга правильный я проверял. Тот который на 5..

·
74
  ·  в прошлом году

проверь package.json, в dependencies "golos": "ontofractal/golosjs"? и еще переустановил ли ты npm модули?

·
·

Я туда как на видео зафигарил "golos": "git://github.com/ontofractal/golosjs.git", :))
Ща поменяю

·
·
·
74
  ·  в прошлом году

"ontofractal/golosjs" -- это простая форма записи "git://github.com/ontofractal/golosjs.git" :)
маякни в chat.golos.io, если не получится после переустановки npm install

·
·
·
·

хорошо тогда завтра если сам не справлюсь)

·
·
·
·
74
  ·  в прошлом году

@rusldv

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

ладно. покопаю еще стимит) мало ли)...

Кстати когда уже ограничение по кол-ву комментариев переделают не в курсе случайно?))

·
74
  ·  в прошлом году

Неограниченную глубину комментариев должны внедрить в следующем форке в Стиме или через один (вроде бы) в Голосе .

·
·

спасибо.

Привет. Ошибка аналогичная rusldv, пока не могу разобраться...

·
74
  ·  в прошлом году

Попробуй удалить папку node_modules/, проверь, чтобы package.json в dependencies значение пары было "golos": "ontofractal/golosjs" и сделай заново npm install, rusldv это помогло.

·
·

А про папку можно немного подробнее, я такую даже не создавал... вроде)
И что должно отображаться, если корректно переустановился npm install?

·
·
·
74
  ·  в прошлом году

если ты проходил предыдущие уроки и делал npm install, то она автоматически появилась, теперь ее нужно удалить и заново инсталлировать. ничего особого не отображается, просто сообщение об успехе инсталляции модулей. напиши мне в chat.golos.io, если не сработает.

·
·
·
·

Спасибо, оказалось не тот package.json правил. Работает, как часы. Осталось в облаке разместить

Было бы здорово, если поэтапно опишешь процедуру размещения Бота на виртуальном сервере. Спасибо

·
74
  ·  в прошлом году

Конечно, в следующем или через один урок сделаю про деплоймент, буду использовать docker, есть смысл немного с ним познакомиться до урока.

Зачем столько мучений? :) Превратить Голос к казино и играйте на здоровье?
Хотя... по сути вы и развиваете инструментарий казино :):):)
Сухой остаток (попытка) здесь: https://golos.io/ru--yevolyucziya/@jumper/kriptovalyuta-golos-proekt-solyaris

·
74
  ·  в прошлом году

У тебя какое-то альтернативное определение слова "казино" ¯\(ツ)

·
·

Чистое казино - имея ограниченное кол-во денег, планировать ставки, используя боты-шулеры. А отцы/матери-основатели обещали качественный контент за счет тщательных чтения и отбора его кураторами :) Оно и понятно - лохам надо обещать золотые горы :)
Или ваши боты умеют читать? :):):)

·
·
·
74
  ·  в прошлом году

мне скучно что-то рассказывать человеку, который не потрудился изучить принципы работы Голоса

·
·
·
·

Ну да, зелен виноград :) Не напрягайтесь :)

Очень интересно, но пока не все получается :(

·
74
  ·  в прошлом году

это нормально, у меня тоже сначала почти ничего не получалось :)

Очень помогло, спасибо.

·
74
  ·  в прошлом году

рад слышать/читать :)