huginn_ruby_agent
Advanced tools
| # frozen_string_literal: true | ||
| require 'huginn_ruby_agent' | ||
| require 'huginn_ruby_agent/agent' | ||
| module Agents | ||
| class RubyAgent < Agent | ||
| include FormConfigurable | ||
| can_dry_run! | ||
| default_schedule "never" | ||
| # TODO: remove redundant | ||
| gem_dependency_check { defined?(MiniRacer) } | ||
| description <<-MD | ||
| The Ruby Agent allows you to write code in Ruby that can create and receive events. If other Agents aren't meeting your needs, try this one! | ||
| You should put code in the `code` option. | ||
| You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent: | ||
| * `@api.create_event(payload)` | ||
| * `@api.incoming_vents()` (the returned event objects will each have a `payload` property) # TODO | ||
| * `@api.memory()` # TODO | ||
| * `@api.memory(key)` # TODO | ||
| * `@api.memory(keyToSet, valueToSet)` # TODO | ||
| * `@api.set_memory(object)` (replaces the Agent's memory with the provided object) # TODO | ||
| * `@api.delete_key(key)` (deletes a key from memory and returns the value) # TODO | ||
| * `@api.credential(name)` | ||
| * `@api.set_credential(name, valueToSet)` | ||
| * `@api.options()` # TODO | ||
| * `@api.options(key)` # TODO | ||
| * `@api.log(message)` | ||
| * `@api.error(message)` | ||
| MD | ||
| form_configurable :code, type: :text, ace: true | ||
| form_configurable :expected_receive_period_in_days | ||
| form_configurable :expected_update_period_in_days | ||
| def validate_options | ||
| errors.add(:base, "The 'code' option is required") unless options['code'].present? | ||
| end | ||
| def working? | ||
| return false if recent_error_logs? | ||
| if interpolated['expected_update_period_in_days'].present? | ||
| return false unless event_created_within?(interpolated['expected_update_period_in_days']) | ||
| end | ||
| if interpolated['expected_receive_period_in_days'].present? | ||
| return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago | ||
| end | ||
| true | ||
| end | ||
| def check | ||
| running_agent do |agent| | ||
| agent.check | ||
| end | ||
| end | ||
| def receive(events) | ||
| running_agent do |agent| | ||
| agent.receive events | ||
| end | ||
| end | ||
| def default_options | ||
| code = <<~CODE | ||
| require "bundler/inline" | ||
| gemfile do | ||
| source "https://rubygems.org" | ||
| # gem "mechanize" | ||
| end | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| @api.create_event({ message: 'I made an event!' }) | ||
| end | ||
| def receive(incoming_events) | ||
| incoming_events.each do |event| | ||
| @api.create_event({ message: 'new event', event_was: event[:payload] }) | ||
| end | ||
| end | ||
| end | ||
| CODE | ||
| { | ||
| 'code' => code, | ||
| 'expected_receive_period_in_days' => '2', | ||
| 'expected_update_period_in_days' => '2' | ||
| } | ||
| end | ||
| private | ||
| def running_agent | ||
| agent = HuginnRubyAgent::Agent.new(code:, credentials: credentials_hash) | ||
| yield agent | ||
| agent.events.each do |event| | ||
| create_event(payload: event) | ||
| end | ||
| agent.logs.each do |message| | ||
| log message | ||
| end | ||
| agent.errors.each do |message| | ||
| error message | ||
| end | ||
| agent.changed_credentials.each do |name, value| | ||
| set_credential(name, value) | ||
| end | ||
| end | ||
| def code | ||
| interpolated['code'] | ||
| end | ||
| def credentials_hash | ||
| Hash[user.user_credentials.map { |c| [c.credential_name, c.credential_value] }] | ||
| end | ||
| def set_credential(name, value) | ||
| c = user.user_credentials.find_or_initialize_by(credential_name: name) | ||
| c.credential_value = value | ||
| c.save! | ||
| end | ||
| end | ||
| end |
| require 'open3' | ||
| require 'json' | ||
| require 'huginn_ruby_agent/sdk' | ||
| require 'base64' | ||
| module HuginnRubyAgent | ||
| class Agent | ||
| attr_reader :events, :errors, :logs, :changed_credentials | ||
| def initialize(code:, credentials: {}) | ||
| @code = code | ||
| @events = [] | ||
| @logs = [] | ||
| @errors = [] | ||
| @credentials = credentials | ||
| @changed_credentials = {} | ||
| end | ||
| def check | ||
| execute ".check" | ||
| end | ||
| def receive(events) | ||
| execute ".receive(api.deserialize('#{sdk.serialize(events)}'))" | ||
| end | ||
| def create_event(payload) | ||
| @events << payload | ||
| end | ||
| def log(message) | ||
| @logs << message | ||
| end | ||
| def error(message) | ||
| @errors << message | ||
| end | ||
| def sdk | ||
| @sdk ||= SDK.new | ||
| end | ||
| private | ||
| # https://stackoverflow.com/questions/23884526/is-there-a-safe-way-to-eval-in-ruby-or-a-better-way-to-do-this | ||
| def execute(command=".check") | ||
| Bundler.with_original_env do | ||
| Open3.popen3("ruby", chdir: '/') do |input, output, err, thread| | ||
| input.write sdk.code | ||
| input.write @code | ||
| input.write <<~CODE | ||
| api = Huginn::API.new(serialized_credentials: '#{sdk.serialize(@credentials)}') | ||
| Agent.new(api)#{command} | ||
| CODE | ||
| input.close | ||
| output.readlines.map { |line| sdk.deserialize(line) }.each do |data| | ||
| case data[:action] | ||
| when 'create_event' | ||
| create_event(data[:payload]) | ||
| when 'log' | ||
| log data[:payload] | ||
| when 'error' | ||
| error data[:payload] | ||
| when 'set_credential' | ||
| @changed_credentials[data[:payload][:name].to_sym] = data[:payload][:value] | ||
| end | ||
| end | ||
| log_errors(err) | ||
| end | ||
| end | ||
| rescue StandardError => e | ||
| error "Runtime error: #{e.message}" | ||
| end | ||
| def log_errors(err) | ||
| err.read.lines.each do |line| | ||
| error line.strip | ||
| end | ||
| end | ||
| end | ||
| end |
| require 'json' | ||
| require 'base64' | ||
| module HuginnRubyAgent | ||
| class SDK | ||
| def serialize(payload) | ||
| Base64.urlsafe_encode64(payload.to_json) | ||
| end | ||
| def deserialize(serialized_payload) | ||
| JSON.parse Base64.urlsafe_decode64(serialized_payload.strip), symbolize_names: true | ||
| end | ||
| def code | ||
| <<~CODE | ||
| require 'json' | ||
| require 'base64' | ||
| module Huginn | ||
| class API | ||
| attr_reader :changed_credentials | ||
| def initialize(serialized_credentials: nil) | ||
| @serialized_credentials = serialized_credentials | ||
| @changed_credentials = {} | ||
| end | ||
| def credentials | ||
| @credentials ||= | ||
| begin | ||
| @serialized_credentials.nil? ? {} : deserialize(@serialized_credentials) | ||
| end | ||
| end | ||
| def serialize(payload) | ||
| Base64.urlsafe_encode64(payload.to_json) | ||
| end | ||
| def deserialize(serialized_payload) | ||
| JSON.parse Base64.urlsafe_decode64(serialized_payload.strip), symbolize_names: true | ||
| end | ||
| def credential(name) | ||
| credentials[name.to_sym] | ||
| end | ||
| def set_credential(name, value) | ||
| credentials[name.to_sym] = value | ||
| changed_credentials[name.to_sym] = value | ||
| puts serialize({ action: :set_credential, payload: { name: name, value: value } }) | ||
| end | ||
| def create_event(payload) | ||
| puts serialize({ action: :create_event, payload: payload }) | ||
| end | ||
| def log(message) | ||
| puts serialize({ action: :log, payload: message }) | ||
| end | ||
| def error(message) | ||
| puts serialize({ action: :error, payload: message }) | ||
| end | ||
| end | ||
| end | ||
| CODE | ||
| end | ||
| end | ||
| end |
| require 'huginn_ruby_agent' | ||
| require 'huginn_ruby_agent/agent' | ||
| module HuginnRubyAgent | ||
| describe Agent do | ||
| describe '#check' do | ||
| example 'it produces event' do | ||
| code = <<~CODE | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| @api.create_event({ message: 'hello' }) | ||
| end | ||
| end | ||
| CODE | ||
| agent = described_class.new(code: code) | ||
| agent.check | ||
| expect(agent.events.size).to eq 1 | ||
| expect(agent.events[0]).to eq(message: 'hello') | ||
| end | ||
| example "it captures error" do | ||
| code = <<~CODE | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| some error here | ||
| end | ||
| end | ||
| CODE | ||
| agent = described_class.new(code: code) | ||
| agent.check | ||
| expect(agent.events).to be_empty | ||
| expect(agent.errors).not_to be_empty | ||
| end | ||
| end | ||
| describe '#receive' do | ||
| example 'it produces event' do | ||
| code = <<~CODE | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def receive(events) | ||
| events.each do |event| | ||
| @api.create_event({ number: event[:number] + 1 }) | ||
| end | ||
| end | ||
| end | ||
| CODE | ||
| agent = described_class.new(code: code) | ||
| agent.receive([{ number: 1 }]) | ||
| expect(agent.events.size).to eq 1 | ||
| expect(agent.events[0]).to eq(number: 2) | ||
| end | ||
| end | ||
| describe '#logs' do | ||
| example 'it produces log' do | ||
| code = <<~CODE | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| @api.log "hello" | ||
| end | ||
| end | ||
| CODE | ||
| agent = described_class.new(code: code) | ||
| agent.check | ||
| expect(agent.logs.size).to eq 1 | ||
| expect(agent.logs[0]).to eq "hello" | ||
| end | ||
| end | ||
| describe '#credentials' do | ||
| example 'it gives access to creds' do | ||
| code = <<~CODE | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| @api.create_event token_from_credential: @api.credential(:token) | ||
| end | ||
| end | ||
| CODE | ||
| agent = described_class.new(code: code, credentials: { token: 'abc123' }) | ||
| agent.check | ||
| expect(agent.events[0]).to eq({ token_from_credential: 'abc123' }) | ||
| end | ||
| example 'it updates creds' do | ||
| code = <<~CODE | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| @api.set_credential(:token, 'new_val') | ||
| end | ||
| end | ||
| CODE | ||
| agent = described_class.new(code: code, credentials: { token: 'abc123' }) | ||
| expect(agent.changed_credentials).to be_empty | ||
| agent.check | ||
| expect(agent.changed_credentials).to eq({ token: 'new_val' }) | ||
| end | ||
| end | ||
| end | ||
| end |
| require 'huginn_ruby_agent' | ||
| require 'huginn_ruby_agent/sdk' | ||
| module HuginnRubyAgent | ||
| describe SDK do | ||
| subject(:sdk) { described_class.new } | ||
| def transfered(data) | ||
| sdk.deserialize(sdk.serialize(data)) | ||
| end | ||
| def expect_to_transfer_safe(data) | ||
| expect(transfered(data)).to eq data | ||
| end | ||
| it { expect_to_transfer_safe(1) } | ||
| it { expect_to_transfer_safe({}) } | ||
| it { expect_to_transfer_safe({ payload: 1 }) } | ||
| it { expect_to_transfer_safe({ action: 'create_event', payload: { number: 1 } }) } | ||
| it { expect_to_transfer_safe({ action: 'create_event', payload: { message: "hello" }}) } | ||
| end | ||
| end |
@@ -5,3 +5,5 @@ # frozen_string_literal: true | ||
| # HuginnAgent.load 'huginn_ruby_agent/concerns/my_agent_concern' | ||
| HuginnAgent.register 'huginn_ruby_agent/ruby_agent' | ||
| module HuginnRubyAgent | ||
| end | ||
| HuginnAgent.register 'agents/ruby_agent' |
| # frozen_string_literal: true | ||
| require 'date' | ||
| require 'cgi' | ||
| require 'tempfile' | ||
| require 'base64' | ||
| # https://stackoverflow.com/questions/23884526/is-there-a-safe-way-to-eval-in-ruby-or-a-better-way-to-do-this | ||
| module Agents | ||
| class RubyAgent < Agent | ||
| include FormConfigurable | ||
| can_dry_run! | ||
| default_schedule "never" | ||
| gem_dependency_check { defined?(MiniRacer) } | ||
| description <<-MD | ||
| The Ruby Agent allows you to write code in Ruby that can create and receive events. If other Agents aren't meeting your needs, try this one! | ||
| You should put code in the `code` option. | ||
| You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent: | ||
| * `createEvent(payload)` | ||
| * `incomingEvents()` (the returned event objects will each have a `payload` property) | ||
| * `memory()` | ||
| * `memory(key)` | ||
| * `memory(keyToSet, valueToSet)` | ||
| * `setMemory(object)` (replaces the Agent's memory with the provided object) | ||
| * `deleteKey(key)` (deletes a key from memory and returns the value) | ||
| * `credential(name)` | ||
| * `credential(name, valueToSet)` | ||
| * `options()` | ||
| * `options(key)` | ||
| * `log(message)` | ||
| * `error(message)` | ||
| MD | ||
| form_configurable :code, type: :text, ace: true | ||
| form_configurable :expected_receive_period_in_days | ||
| form_configurable :expected_update_period_in_days | ||
| def validate_options | ||
| errors.add(:base, "The 'code' option is required") unless options['code'].present? | ||
| end | ||
| def working? | ||
| return false if recent_error_logs? | ||
| if interpolated['expected_update_period_in_days'].present? | ||
| return false unless event_created_within?(interpolated['expected_update_period_in_days']) | ||
| end | ||
| if interpolated['expected_receive_period_in_days'].present? | ||
| return false unless last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago | ||
| end | ||
| true | ||
| end | ||
| def check | ||
| log_errors do | ||
| execute_check | ||
| end | ||
| end | ||
| def receive(events) | ||
| log_errors do | ||
| execute_receive(events) | ||
| end | ||
| end | ||
| def default_options | ||
| code = <<~CODE | ||
| require "bundler/inline" | ||
| gemfile do | ||
| source "https://rubygems.org" | ||
| # gem "mechanize" | ||
| end | ||
| class Agent | ||
| def initialize(api) | ||
| @api = api | ||
| end | ||
| def check | ||
| @api.create_event({ message: 'I made an event!' }) | ||
| end | ||
| def receive(incoming_events) | ||
| incoming_events.each do |event| | ||
| @api.create_event({ message: 'new event', event_was: event[:payload] }) | ||
| end | ||
| end | ||
| end | ||
| CODE | ||
| { | ||
| 'code' => code, | ||
| 'expected_receive_period_in_days' => '2', | ||
| 'expected_update_period_in_days' => '2' | ||
| } | ||
| end | ||
| private | ||
| def execute_check | ||
| Bundler.with_original_env do | ||
| Open3.popen3("ruby", chdir: '/') do |input, output, err, thread| | ||
| input.write sdk_code | ||
| input.write code | ||
| input.write <<~CODE | ||
| Agent.new(Huginn::API.new).check | ||
| CODE | ||
| input.close | ||
| output.readlines.map { |line| JSON.parse(line, symbolize_names: true) }.each do |data| | ||
| case data[:action] | ||
| when 'create_event' | ||
| create_event(payload: data[:payload]) | ||
| when 'log' | ||
| log data[:payload] | ||
| when 'error' | ||
| error data[:payload] | ||
| end | ||
| end | ||
| errors = err.read | ||
| error err.read | ||
| log "thread #{thread.value}" | ||
| end | ||
| end | ||
| end | ||
| def execute_receive(events) | ||
| Bundler.with_original_env do | ||
| Open3.popen3("ruby", chdir: '/') do |input, output, err, thread| | ||
| input.write sdk_code | ||
| input.write code | ||
| input.write <<~CODE | ||
| api = Huginn::API.new | ||
| begin | ||
| Agent.new(api).receive( | ||
| JSON.parse( | ||
| Base64.decode64( | ||
| "#{Base64.encode64(events.to_json)}" | ||
| ), | ||
| symbolize_names: true | ||
| ) | ||
| ) | ||
| rescue StandardError => ex | ||
| api.error ex | ||
| end | ||
| CODE | ||
| input.close | ||
| output.readlines.map { |line| JSON.parse(line, symbolize_names: true) }.each do |data| | ||
| case data[:action] | ||
| when 'create_event' | ||
| create_event(payload: data[:payload]) | ||
| when 'log' | ||
| log data[:payload] | ||
| when 'error' | ||
| error data[:payload] | ||
| end | ||
| end | ||
| errors = err.read | ||
| error err.read | ||
| log "thread #{thread.value}" | ||
| end | ||
| end | ||
| end | ||
| def code | ||
| interpolated['code'] | ||
| end | ||
| def sdk_code | ||
| <<~CODE | ||
| require 'json' | ||
| require 'base64' | ||
| module Huginn | ||
| class API | ||
| def create_event(payload) | ||
| puts( | ||
| { | ||
| action: :create_event, | ||
| payload: payload | ||
| }.to_json | ||
| ) | ||
| end | ||
| def log(message) | ||
| puts( | ||
| { | ||
| action: :log, | ||
| payload: message | ||
| }.to_json | ||
| ) | ||
| end | ||
| def error(message) | ||
| puts( | ||
| { | ||
| action: :error, | ||
| payload: message | ||
| }.to_json | ||
| ) | ||
| end | ||
| end | ||
| end | ||
| CODE | ||
| end | ||
| def log_errors | ||
| begin | ||
| yield | ||
| rescue StandardError => e | ||
| error "Runtime error: #{e.message}" | ||
| end | ||
| end | ||
| end | ||
| end |
| # frozen_string_literal: true | ||
| require 'rails_helper' | ||
| require 'huginn_agent/spec_helper' | ||
| describe Agents::RubyAgent do | ||
| before(:each) do | ||
| @valid_options = Agents::RubyAgent.new.default_options | ||
| @checker = Agents::RubyAgent.new(name: 'RubyAgent', options: @valid_options) | ||
| @checker.user = users(:bob) | ||
| @checker.save! | ||
| end | ||
| pending 'add specs here' | ||
| end |