slack-ruby/slack-ruby-bot

View on GitHub
examples/inventory/inventorybot.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require 'slack-ruby-bot'
require 'sqlite3'

# Demonstrate the usage of the Model, View, and Controller
# classes to build an inventory bot.

SlackRubyBot::Client.logger.level = Logger::WARN

# The Controller takes the place of the `command` block. It has access to the
# `client`, `data`, and `match` arguments passed to the `command` block. Each
# instance method generates a route for the bot to match against.
#
# The controller instance accesses the model and view via the `model` and
# `view` accessor methods.
#
# ActiveSupport::Callbacks support is included so every method can have
# before, after, or around hooks.
#
# Helper methods should begin with an underscore (e.g. _row) so that
# `command` routes are not auto-generated for the method.
#
class InventoryController < SlackRubyBot::MVC::Controller::Base
  define_callbacks :react, :notify
  set_callback :react, :around, :around_reaction
  set_callback :notify, :after, :notify_admin
  attr_accessor :_row

  def create
    model.add_item(match[:expression])
  end

  def read
    run_callbacks :react do
      row = model.read_item(match[:expression])
      view.say(channel: data.channel, text: row.inspect)
    end
  end

  def update
    run_callbacks :notify do
      self._row = model.update_item(match[:expression])
    end
  end

  def delete
    result = model.delete_item(match[:expression])
    if result
      view.delete_succeeded
    else
      view.delete_failed
    end
  end

  private

  def notify_admin
    row = _row.first
    return if row[:quantity].to_i.zero?

    view.email_admin("Inventory item #{row[:name]} needs to be refilled.")
    view.say(channel: data.channel, text: "Administrator notified via email to refill #{row[:name]}.")
  end

  def around_reaction
    view.react_wait
    view.say(channel: data.channel, text: 'Please wait while long-running action completes...')
    sleep 2.0
    yield
    view.say(channel: data.channel, text: 'Action has completed!')
    view.unreact_wait
  end
end

# The Model contains all business logic.
#
# ActiveSupport::Callbacks support is included for before, after,
# or around hooks.
#
# The Model has access to the `client`, `data`, and `match` objects.
#
class InventoryModel < SlackRubyBot::MVC::Model::Base
  define_callbacks :fixup
  set_callback :fixup, :before, :normalize_data
  attr_accessor :_line

  def initialize
    @db = SQLite3::Database.new ':memory'
    @db.results_as_hash = true
    @db.execute 'CREATE TABLE IF NOT EXISTS Inventory(Id INTEGER PRIMARY KEY,
            Name TEXT, Quantity INT, Price INT)'

    s = @db.prepare 'SELECT * FROM Inventory'
    results = s.execute
    count = 0
    count += 1 while results.next
    return if count < 4

    add_item "'Audi',3,52642"
    add_item "'Mercedes',1,57127"
    add_item "'Skoda',5,9000"
    add_item "'Volvo',1,29000"
  end

  def add_item(line)
    self._line = line # make line accessible to callback
    run_callbacks :fixup do
      name, quantity, price = parse(_line)
      row = @db.prepare('SELECT MAX(Id) FROM Inventory').execute
      max_id = row.next_hash['MAX(Id)']
      @db.execute "INSERT INTO Inventory VALUES(#{max_id + 1},'#{name}',#{quantity.to_i},#{price.to_i})"
    end
  end

  def read_item(line)
    self._line = line
    run_callbacks :fixup do
      name, _other = parse(_line)
      statement = if name == '*'
                    @db.prepare 'SELECT * FROM Inventory'
                  else
                    @db.prepare("SELECT * FROM Inventory WHERE Name='#{name}'")
                  end

      results = statement.execute
      a = []
      results.each do |row|
        a << { id: row['Id'], name: row['Name'], quantity: row['Quantity'], price: row['Price'] }
      end
      a
    end
  end

  def update_item(line)
    self._line = line
    run_callbacks :fixup do
      name, quantity, price = parse(_line)
      statement = if price
                    @db.prepare "UPDATE Inventory SET Quantity=#{quantity}, Price=#{price} WHERE Name='#{name}'"
                  else
                    @db.prepare "UPDATE Inventory SET Quantity=#{quantity} WHERE Name='#{name}'"
                  end
      statement.execute
      read_item(_line)
    end
  end

  def delete_item(line)
    self._line = line
    run_callbacks :fixup do
      name, _other = parse(_line)
      before_count = row_count
      statement = @db.prepare "DELETE FROM Inventory WHERE Name='#{name}'"
      statement.execute
      before_count != row_count
    end
  end

  private

  def row_count
    statement = @db.prepare 'SELECT COUNT(*) FROM Inventory'
    result = statement.execute
    result.next_hash['COUNT(*)']
  end

  def parse(line)
    line.split(',')
  end

  def normalize_data
    name, quantity, price = parse(_line)
    self._line = [name.capitalize, quantity, price].join(',')
  end
end

# All interactivity logic should live in this class.
#
# ActiveSupport::Callbacks support is included for before, after,
# or around hooks.
#
# The Model has access to the `client`, `data`, and `match` objects.
#
class InventoryView < SlackRubyBot::MVC::View::Base
  def react_wait
    client.web_client.reactions_add(
      name: :hourglass_flowing_sand,
      channel: data.channel,
      timestamp: data.ts,
      as_user: true
    )
  end

  def unreact_wait
    client.web_client.reactions_remove(
      name: :hourglass_flowing_sand,
      channel: data.channel,
      timestamp: data.ts,
      as_user: true
    )
  end

  def email_admin(message)
    # send email to administrator with +message+
    # ...
    puts "Sent email to administrator: #{message}"
  end

  def delete_succeeded
    say(channel: data.channel, text: 'Item was successfully deleted.')
  end

  def delete_failed
    say(channel: data.channel, text: 'Item failed to be deleted.')
  end
end

class InventoryBot < SlackRubyBot::Bot
  help do
    title 'Inventory Bot'
    desc 'This bot lets you create, read, update, and delete items from an inventory.'

    command 'create' do
      desc 'Add an item to the inventory.'
    end

    command 'read' do
      desc 'Get inventory status for an item.'
    end

    command 'update' do
      desc 'Update inventory levels for an item.'
    end

    command 'delete' do
      desc 'Remove an item from inventory.'
    end
  end

  # Instantiate the Model, View, and Controller objects within the +Bot+ subclass
  # or within a SlackRubyBot::Commands::Base subclass.
  #
  model = InventoryModel.new
  view = InventoryView.new
  @controller = InventoryController.new(model, view)
  @controller.class.command_class.routes.each do |route|
    warn route.inspect
  end
end

InventoryBot.run