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

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

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

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

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

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

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

·
74
  ·  2 года назад

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

·
·
69
  ·  2 года назад

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

·
74
  ·  2 года назад

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

·
·
69
  ·  2 года назад

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

·
·
·
74
  ·  2 года назад

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

·
·
·
·
69
  ·  2 года назад

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

·
·
·
·
74
  ·  2 года назад

@mrgreen:

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

·
·
·
·
50
  ·  2 года назад

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

64
  ·  2 года назад

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

·
74
  ·  2 года назад

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

·
·
64
  ·  2 года назад

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

·
·
·
74
  ·  2 года назад

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

·
·
·
·
74
  ·  2 года назад

@stranger27:

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

·
·
·
·
64
  ·  2 года назад

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

66
  ·  2 года назад

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

67
  ·  2 года назад

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

·
74
  ·  2 года назад

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

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

·
·
67
  ·  2 года назад

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

·
67
  ·  2 года назад

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

·
·
74
  ·  2 года назад

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

·
·
·
67
  ·  2 года назад

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

67
  ·  2 года назад

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

·
74
  ·  2 года назад

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

·
·
67
  ·  2 года назад

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

·
·
·
74
  ·  2 года назад

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

·
·
·
·
67
  ·  2 года назад

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

·
·
·
·
74
  ·  2 года назад

@rusldv

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

69
  ·  2 года назад

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

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

·
74
  ·  2 года назад

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

·
·
69
  ·  2 года назад

спасибо.

57
  ·  2 года назад

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

·
74
  ·  2 года назад

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

·
·
57
  ·  2 года назад

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

·
·
·
74
  ·  2 года назад

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

·
·
·
·
57
  ·  2 года назад

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

57
  ·  2 года назад

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

·
74
  ·  2 года назад

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

57
  ·  2 года назад

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

·
74
  ·  2 года назад

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

·
·
57
  ·  2 года назад

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

·
·
·
74
  ·  2 года назад

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

·
·
·
·
57
  ·  2 года назад

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

78
  ·  2 года назад

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

·
74
  ·  2 года назад

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

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

·
74
  ·  2 года назад

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