pug-bot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/Rakefile +29 -0
- data/lib/pug/action/controller.rb +65 -0
- data/lib/pug/action/enumerator.rb +33 -0
- data/lib/pug/action/input.rb +26 -0
- data/lib/pug/action/output.rb +21 -0
- data/lib/pug/bot.rb +53 -0
- data/lib/pug/clients/factory.rb +23 -0
- data/lib/pug/configuration.rb +43 -0
- data/lib/pug/interfaces/action.rb +35 -0
- data/lib/pug/interfaces/client.rb +22 -0
- data/lib/pug/keyword_handler.rb +59 -0
- data/lib/pug/message_handler.rb +76 -0
- data/lib/pug/number_parser.rb +22 -0
- data/lib/pug/results.rb +26 -0
- data/lib/pug/strings.rb +38 -0
- data/lib/pug/telegram_client.rb +72 -0
- data/lib/pug/terminal_client.rb +33 -0
- data/lib/pug/types/result.rb +57 -0
- data/lib/pug/version.rb +5 -0
- data/lib/pug.rb +41 -0
- data/pug-bot.gemspec +23 -0
- data/spec/lib/pug/action/controller_spec.rb +138 -0
- data/spec/lib/pug/action/enumerator_spec.rb +104 -0
- data/spec/lib/pug/bot_spec.rb +78 -0
- data/spec/lib/pug/clients/factory_spec.rb +24 -0
- data/spec/lib/pug/configuration_spec.rb +52 -0
- data/spec/lib/pug/keyword_handler_spec.rb +53 -0
- data/spec/lib/pug/message_handler_spec.rb +78 -0
- data/spec/lib/pug/number_parser_spec.rb +44 -0
- data/spec/lib/pug/types/result_spec.rb +32 -0
- data/spec/lib/spec_helpers/mock_action.rb +39 -0
- data/spec/lib/spec_helpers/mock_client.rb +32 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 130e63accf84439db61edd3da1f0745878a35bf5
|
4
|
+
data.tar.gz: e655cfbcd864bc561b128b0248798a1c8371b62c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 360bc1589c908f872b249c7e0388b87d50dcbdc0df9e791d84b431f7e3136e0589b37f4632212e3fecdfed8b3f1b8b51ae562729028f0b776f844aa191a5d26d
|
7
|
+
data.tar.gz: d6bfc6fef76beabd1e7ee82ac1df9e9368910138e6cd81e4d81ca415fa69be583050d56ad8ce305efa9027b8f2fc319bd2345392142e5d2beca8804a254b7741
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 Alex Figueroa
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# pug-bot
|
2
|
+
|
3
|
+
An automation framework for repetitive dev tasks
|
4
|
+
|
5
|
+
# Installation
|
6
|
+
|
7
|
+
Add the following to your `Gemfile` and then run `bundle install`
|
8
|
+
|
9
|
+
```
|
10
|
+
gem 'pug-bot'
|
11
|
+
```
|
12
|
+
|
13
|
+
# Usage
|
14
|
+
|
15
|
+
Before you can interact with pug-bot, you need to give it something to do via `Action`s
|
16
|
+
|
17
|
+
## Action
|
18
|
+
|
19
|
+
An `Action` is an abstract representation of a task. This could be anything from
|
20
|
+
transforming RGB colors in the form: `66, 134, 244` to `4286f4` or pulling reports from
|
21
|
+
an API to display stock prices.
|
22
|
+
|
23
|
+
To define your own action, you need to create a new class which subclasses `Pug::Interfaces::Action` as follows
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# actions/hello_world_action.rb
|
27
|
+
require 'pug'
|
28
|
+
|
29
|
+
class HelloWorldAction < Pug::Interfaces::Action
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
There are a few methods you **must** override specifically `name`, `requires_input?`, and `execute`.
|
34
|
+
- `name` helps to identify your `Action`
|
35
|
+
- `requires_input?` determines if input is required for your `Action`
|
36
|
+
- `execute` is what happens when the `Action` is ran
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
# actions/hello_world_action.rb
|
40
|
+
require 'pug'
|
41
|
+
|
42
|
+
class HelloWorldAction < Pug::Interfaces::Action
|
43
|
+
def name
|
44
|
+
'Hello, World'
|
45
|
+
end
|
46
|
+
|
47
|
+
def requires_input?
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def execute(input)
|
52
|
+
"Hello, #{input}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Once you've defined your action you just need a way to interact with it.
|
58
|
+
There are two ways to interact with pug-bot: Telegram and Terminal.
|
59
|
+
|
60
|
+
### Telegram Bot Setup
|
61
|
+
|
62
|
+
To use Telegram we need to first setup a Telegram Bot. Thankfully, Telegram makes this a nice experience. Skip this and the next section if you want to just use Terminal.
|
63
|
+
|
64
|
+
Assuming you already have Telegram installed, you'll need to perform the following steps:
|
65
|
+
1. Start a new conversation with `@botfather` on Telegram
|
66
|
+
2. Type `/newbot` and follow its steps to create a new Bot
|
67
|
+
3. Grab the token for the HTTP API, this is your Telegram API token
|
68
|
+
4. Start a new conversation with your newly created Bot
|
69
|
+
5. Visit `https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates` with `YOUR_BOT_TOKEN` replaced with the token from Step 3.
|
70
|
+
6. Look for the `id` field in the `chat` body, this is your Telegram Chat ID
|
71
|
+
|
72
|
+
## Telegram
|
73
|
+
|
74
|
+
To setup Telegram, ensure you have an API token and Chat ID.
|
75
|
+
Replace `YOUR_TELEGRAM_TOKEN` and `YOUR_TELEGRAM_CHAT_ID` with the token and chat_id for your Bot.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
require 'pug'
|
79
|
+
require_relative './actions/hello_world_action'
|
80
|
+
|
81
|
+
Pug.configure do |config|
|
82
|
+
config.type = Pug::Configuration::TELEGRAM
|
83
|
+
config.token = 'YOUR_TELEGRAM_TOKEN'
|
84
|
+
config.chat_id = 'YOUR_TELEGRAM_CHAT_ID'
|
85
|
+
config.actions = [HelloWorldAction.new]
|
86
|
+
end
|
87
|
+
|
88
|
+
Pug::Bot.run
|
89
|
+
```
|
90
|
+
|
91
|
+
## Terminal
|
92
|
+
|
93
|
+
To setup Terminal, you just need to specify the type as Terminal
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
require 'pug'
|
97
|
+
require_relative './actions/hello_world_action'
|
98
|
+
|
99
|
+
Pug.configure do |config|
|
100
|
+
config.type = Pug::Configuration::TERMINAL
|
101
|
+
config.actions = [HelloWorldAction.new]
|
102
|
+
end
|
103
|
+
|
104
|
+
Pug::Bot.run
|
105
|
+
```
|
106
|
+
|
107
|
+
# Interactions
|
108
|
+
|
109
|
+
Every `Action` that pug-bot handles is enumerated. To call your command, you just need to type in the number that it corresponds to.
|
110
|
+
For the `HelloWorldAction`, that would be `0` as shown below:
|
111
|
+
|
112
|
+
## Telegram
|
113
|
+
|
114
|
+
<img src="assets/telegram_example.png" width=60% height=60%>
|
115
|
+
|
116
|
+
## Terminal
|
117
|
+
|
118
|
+
<img src="assets/terminal_example.png" width=60% height=60%>
|
119
|
+
|
120
|
+
# Hints
|
121
|
+
|
122
|
+
Hints can be provided via entering: `help` or `list`.
|
123
|
+
|
124
|
+
<img src="assets/terminal_hints.png" width=60% height=60%>
|
125
|
+
|
126
|
+
|
127
|
+
# Motivation
|
128
|
+
|
129
|
+
There were a lot of tasks that I found myself repeating during my dev work.
|
130
|
+
I wanted to use something that was device/editor agnostic and could handle multiline arguments with no hassle.
|
131
|
+
After playing around with the Telegram API, I hacked together a quick Bot to start this automation.
|
132
|
+
This is the result of a few weeks of tinkering in hopes of making something more extensible.
|
133
|
+
|
134
|
+
If this helps at least one other person work faster then I'll be content.
|
135
|
+
Also, I come from an iOS background so forgive my not-so idomatic Ruby.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true.
|
2
|
+
|
3
|
+
require 'pug/version'
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
desc 'Prepares and installs gem'
|
7
|
+
task :prepare do
|
8
|
+
sh %{ gem build pug-bot.gemspec }
|
9
|
+
sh %{ gem install pug-bot-#{Pug::VERSION}.gem }
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'Run tests for the gem'
|
13
|
+
task :test do
|
14
|
+
sh %{ rspec spec }
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Run Rubocop check for the gem'
|
18
|
+
task :cop do
|
19
|
+
sh %{ rubocop }
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'Verify everything is good before merge'
|
23
|
+
task :flightcheck => [:cop, :test] do
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Generate documentation'
|
27
|
+
task :document do
|
28
|
+
sh %{ yardoc 'lib/**/*.rb' }
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Action
|
5
|
+
# Controls the execution of an Action serially
|
6
|
+
class Controller
|
7
|
+
# @param actions [Array<Pug::Interfaces::Action>] The actions to manage
|
8
|
+
def initialize(actions)
|
9
|
+
@current_action = nil
|
10
|
+
@actions = actions || []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Indicates if there are any actions to manage
|
14
|
+
# @return [Boolean] If there are any actions
|
15
|
+
def actions?
|
16
|
+
!@actions.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Indicates if there is a currently running Action
|
20
|
+
# @return [Boolean] if there is an action currently running
|
21
|
+
def running_action?
|
22
|
+
!@current_action.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Provides information about the Action's input requirements
|
26
|
+
# @return [Input] describing the requirements
|
27
|
+
def action_input
|
28
|
+
raise Strings.no_action_running unless running_action?
|
29
|
+
input_required = @current_action.requires_input?
|
30
|
+
Input.new(@current_action.name, input_required)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determines if an Action can start for a given index
|
34
|
+
# @param index [Integer] the index of the Action in actions
|
35
|
+
# @return [Boolean] indicating if an Action can be started at this index
|
36
|
+
# @note This will return false if there is a currently running action
|
37
|
+
def can_start_action?(index)
|
38
|
+
return false if index.negative? || index >= @actions.length
|
39
|
+
!running_action?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Starts up the Action at the given index if possible
|
43
|
+
# @param index [Integer] the index of the Action in actions
|
44
|
+
def start_action(index)
|
45
|
+
return unless can_start_action?(index)
|
46
|
+
|
47
|
+
action = @actions[index]
|
48
|
+
@current_action = action
|
49
|
+
end
|
50
|
+
|
51
|
+
# Runs the Action prepared by #start_action with provided input
|
52
|
+
# @param input [String] the input to pass to the Action
|
53
|
+
# @return [Pug::Types::Result] indicating the execution result
|
54
|
+
def run_action(input)
|
55
|
+
return Results.no_action_running unless running_action?
|
56
|
+
|
57
|
+
requires_input = @current_action.requires_input?
|
58
|
+
result = @current_action.execute(requires_input ? input : nil)
|
59
|
+
output = Output.new(@current_action.name, result || '')
|
60
|
+
@current_action = nil
|
61
|
+
Types::Result.success(output)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Action
|
5
|
+
# Formats actions to an enumerated text representation
|
6
|
+
class Enumerator
|
7
|
+
# Enumerates Action names with an optional description
|
8
|
+
# @param actions [Array<Interfaces::Action>] Action names to enumerate
|
9
|
+
# @param show_description [Boolean] optional flag that adds descriptions
|
10
|
+
# @return [Array<String>] enumerated names with optional description
|
11
|
+
def names(actions, show_description = false)
|
12
|
+
return [] if actions.nil?
|
13
|
+
actions.each_with_index.map do |action, index|
|
14
|
+
if show_description && !action.description.to_s.empty?
|
15
|
+
"#{index}: #{action.name} # #{action.description}"
|
16
|
+
else
|
17
|
+
"#{index}: #{action.name}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Enumerates and groups Action names into a 2D array
|
23
|
+
# @param actions [Array<Interfaces::Action>] Action names to enumerate
|
24
|
+
# @param group_size [Integer] optional count indicating subarray size
|
25
|
+
# @return [Array<Array<String>>] enumerated and grouped names
|
26
|
+
# @note This does not support descriptions at the moment
|
27
|
+
def grouped_names(actions, group_size = 2)
|
28
|
+
return [] if group_size <= 0
|
29
|
+
names(actions).each_slice(group_size).to_a
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Action
|
5
|
+
# Defines input requirements for an Action
|
6
|
+
# @!attribute action_name
|
7
|
+
# @return [String] the name of the Action
|
8
|
+
# @!attribute required
|
9
|
+
# @return [Boolean] if Action requires input
|
10
|
+
class Input
|
11
|
+
attr_reader :action_name, :required
|
12
|
+
|
13
|
+
# @param action_name [String] The name of the Action
|
14
|
+
# @param required [Boolean] if Action requires input
|
15
|
+
def initialize(action_name, required)
|
16
|
+
@action_name = action_name
|
17
|
+
@required = required
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean] if Action requires input
|
21
|
+
def required?
|
22
|
+
@required
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Action
|
5
|
+
# Defines output of running an Action
|
6
|
+
# @!attribute action_name
|
7
|
+
# @return [String] the name of the Action
|
8
|
+
# @!attribute value
|
9
|
+
# @return [String] the output of the Action
|
10
|
+
class Output
|
11
|
+
attr_reader :action_name, :value
|
12
|
+
|
13
|
+
# @param action_name [String] the name of the Action
|
14
|
+
# @param value [String] the output of the Action
|
15
|
+
def initialize(action_name, value)
|
16
|
+
@action_name = action_name
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/pug/bot.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
# Coordinates the Client with the provided Actions
|
5
|
+
class Bot
|
6
|
+
# Convenience method used to setup the Bot
|
7
|
+
# with the user defined Configuration
|
8
|
+
# @return [Bot] A new instance of Bot.
|
9
|
+
# @see Configuration
|
10
|
+
def self.run
|
11
|
+
config = Pug.configuration
|
12
|
+
config.validate
|
13
|
+
client = Clients::Factory.client_for_config(config)
|
14
|
+
Bot.new(client, config.actions).start
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param client [Interfaces::Client] used to interact with User
|
18
|
+
# @param actions [Array<Interfaces::Action>] available actions
|
19
|
+
def initialize(client, actions)
|
20
|
+
@client = client
|
21
|
+
@handler = MessageHandler.default(actions)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Starts the handling all messages received via the Client
|
25
|
+
# @return [void]
|
26
|
+
def start
|
27
|
+
@client.listen do |message|
|
28
|
+
handle(message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def handle(message)
|
35
|
+
result = @handler.handle(message)
|
36
|
+
output = output_from_result(result)
|
37
|
+
@client.send_message(output) unless output.to_s.empty?
|
38
|
+
return if result.type != Types::Result::SUCCESS
|
39
|
+
action_name = result.value.action_name
|
40
|
+
@client.send_message(Strings.finished_running(action_name))
|
41
|
+
end
|
42
|
+
|
43
|
+
def output_from_result(result)
|
44
|
+
if result.type == Types::Result::SUCCESS
|
45
|
+
result.value.value
|
46
|
+
elsif result.type == Types::Result::INFO
|
47
|
+
result.value
|
48
|
+
else
|
49
|
+
result.error
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Clients
|
5
|
+
# Factory for building Clients based on a configuration
|
6
|
+
class Factory
|
7
|
+
# Builds a Client for the given config
|
8
|
+
# @param config [Configuration] parameters to build Client from
|
9
|
+
# @return [Interfaces::Client] The client from the config parameters
|
10
|
+
def self.client_for_config(config)
|
11
|
+
if !config.nil? && config.type == Configuration::TELEGRAM
|
12
|
+
enumerator = Action::Enumerator.new
|
13
|
+
markup = enumerator.grouped_names(config.actions)
|
14
|
+
client = TelegramClient.new(config.token, config.chat_id)
|
15
|
+
client.configure_keyboard(markup)
|
16
|
+
client
|
17
|
+
else
|
18
|
+
TerminalClient.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
# Defines parameters used to setup Pug
|
5
|
+
# @!attribute type
|
6
|
+
# @return [Integer] the type of client to setup
|
7
|
+
# @see Configuration::TERMINAL
|
8
|
+
# @see Configuration::TELEGRAM
|
9
|
+
# @!attribute token
|
10
|
+
# @return [String] the API token for Telegram
|
11
|
+
# @note This is optional if type is TERMINAL
|
12
|
+
# @!attribute chat_id
|
13
|
+
# @return [String] the chat_id for Telegram
|
14
|
+
# @note This is optional if type is TERMINAL
|
15
|
+
# @!attribute actions
|
16
|
+
# @return [Array<Interfaces::Action>] user defined actions
|
17
|
+
class Configuration
|
18
|
+
attr_accessor :type, :token, :chat_id, :actions
|
19
|
+
|
20
|
+
# @!group Client Types
|
21
|
+
TERMINAL = 0
|
22
|
+
TELEGRAM = 1
|
23
|
+
# @!endgroup
|
24
|
+
|
25
|
+
# @param type [Integer] type of client to setup
|
26
|
+
def initialize(type = TERMINAL)
|
27
|
+
@type = type
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validates if attributes are correctly setup for Pug
|
31
|
+
# @raise RuntimeError if type is not a valid Client type
|
32
|
+
# @raise RuntimeError if Telegram client and no token & chat_id
|
33
|
+
# @raise RuntimeError if provided actions are nil
|
34
|
+
# @return [void]
|
35
|
+
def validate
|
36
|
+
valid_type = [TERMINAL, TELEGRAM].include?(type)
|
37
|
+
raise 'Invalid client type' unless valid_type
|
38
|
+
bad_config = @token.nil? || @chat_id.nil?
|
39
|
+
raise 'Invalid Telegram config' if @type == TELEGRAM && bad_config
|
40
|
+
raise 'No actions provided' if @actions.nil?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Interfaces
|
5
|
+
# Abstract interface representing an Action
|
6
|
+
class Action
|
7
|
+
# The human readable name for the Action
|
8
|
+
# @return [String] Action name
|
9
|
+
def name
|
10
|
+
raise NoMethodError
|
11
|
+
end
|
12
|
+
|
13
|
+
# Optional description for the action
|
14
|
+
# @return [String] Action description
|
15
|
+
def description
|
16
|
+
''
|
17
|
+
end
|
18
|
+
|
19
|
+
# Indicates if the action requires an input
|
20
|
+
# @return [Boolean] if input is required
|
21
|
+
# @note Defaults to false
|
22
|
+
def requires_input?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
# Entry point for Action with provided input if any
|
27
|
+
# @param input [String] The optional input for the Action
|
28
|
+
# @return [String] The output of running the Action
|
29
|
+
# @note This can return nil if there is no output
|
30
|
+
def execute(input) # rubocop:disable UnusedMethodArgument
|
31
|
+
raise NoMethodError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
module Interfaces
|
5
|
+
# Abstract interface for a Client that Pug can talk to
|
6
|
+
# such as Telegram or Terminal
|
7
|
+
class Client
|
8
|
+
# Listens for and passes text via a block
|
9
|
+
# @yieldparam [String] text
|
10
|
+
def listen
|
11
|
+
raise NoMethodError
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sends a message to the User via the Client
|
15
|
+
# @param message [String] the message to send
|
16
|
+
# @return [void]
|
17
|
+
def send_message(message) # rubocop:disable UnusedMethodArgument
|
18
|
+
raise NoMethodError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Pug
|
6
|
+
# Responds to keywords that provide hints to the User
|
7
|
+
class KeywordHandler
|
8
|
+
# @!group Keywords
|
9
|
+
HELP = 'help'
|
10
|
+
LIST = 'list'
|
11
|
+
# @!endgroup
|
12
|
+
|
13
|
+
# @param actions [Array<Interfaces::Action>]
|
14
|
+
# user provided actions
|
15
|
+
def initialize(actions)
|
16
|
+
@actions = actions
|
17
|
+
@keywords = Set[HELP, LIST]
|
18
|
+
@enumerator = Action::Enumerator.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Determines if a given text is a keyword
|
22
|
+
# @param text [String] text to test
|
23
|
+
# @return [Boolean] if text is a keyword
|
24
|
+
def keyword?(text)
|
25
|
+
@keywords.include?(text)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Runs the command corresponding to text
|
29
|
+
# if it is a keyword
|
30
|
+
# @param text [String] text to run command for
|
31
|
+
# @return [String, nil] output of command or nil
|
32
|
+
def run_command_for_keyword(text)
|
33
|
+
return nil unless keyword?(text)
|
34
|
+
map_keyword_to_command(text)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def keywords_excluding_help
|
40
|
+
@keywords.to_a.reject { |keyword| keyword == HELP }
|
41
|
+
end
|
42
|
+
|
43
|
+
def map_keyword_to_command(keyword)
|
44
|
+
return help_response if keyword == HELP
|
45
|
+
return list_response if keyword == LIST
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def help_response
|
50
|
+
commands = keywords_excluding_help.join("\n")
|
51
|
+
Strings.help(commands)
|
52
|
+
end
|
53
|
+
|
54
|
+
def list_response
|
55
|
+
return Strings.no_actions if @actions.empty?
|
56
|
+
@enumerator.names(@actions, true).join("\n")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
# Coordinates between messages provided from Bot
|
5
|
+
# to actions via the Controller
|
6
|
+
class MessageHandler
|
7
|
+
# A convenice initializer for provided actions
|
8
|
+
# @param actions [Array<Interfaces::Action>] actions to coordinate
|
9
|
+
# @return [MessageHandler] A new instance of MessageHandler
|
10
|
+
def self.default(actions)
|
11
|
+
controller = Action::Controller.new(actions)
|
12
|
+
keyword_handler = KeywordHandler.new(actions)
|
13
|
+
MessageHandler.new(controller, NumberParser.new, keyword_handler)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param controller [Action::Controller] to control Action flow
|
17
|
+
# @param parser [NumberParser] to parse numeric text
|
18
|
+
# @param keyword_handler [KeywordHandler] to handle keywords
|
19
|
+
def initialize(controller, parser, keyword_handler)
|
20
|
+
@controller = controller
|
21
|
+
@parser = parser
|
22
|
+
@keyword_handler = keyword_handler
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parses and coordinates the given message with the Controller
|
26
|
+
# @param message [String] message to handle
|
27
|
+
# @return [Types::Result] result of handling message
|
28
|
+
def handle(message)
|
29
|
+
return Results.missing_actions unless @controller.actions?
|
30
|
+
return Results.unknown_input if message.to_s.empty?
|
31
|
+
if @controller.running_action?
|
32
|
+
run_action_with_inputs(message)
|
33
|
+
elsif @keyword_handler.keyword?(message)
|
34
|
+
handle_keyword(message)
|
35
|
+
else
|
36
|
+
parse_message_for_index(message)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def can_start_action?(index)
|
43
|
+
@controller.can_start_action?(index)
|
44
|
+
end
|
45
|
+
|
46
|
+
def run_action_with_inputs(inputs = nil)
|
47
|
+
@controller.run_action(inputs || '')
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_message_for_index(message)
|
51
|
+
index = @parser.number_from_text(message)
|
52
|
+
return Results.unknown_input if index.nil?
|
53
|
+
return Results.invalid_index(index) unless can_start_action?(index)
|
54
|
+
|
55
|
+
@controller.start_action(index)
|
56
|
+
request_input_if_needed(message)
|
57
|
+
end
|
58
|
+
|
59
|
+
def request_input_if_needed(message)
|
60
|
+
input = @controller.action_input
|
61
|
+
if input.required?
|
62
|
+
Results.enter_inputs(input.action_name)
|
63
|
+
else
|
64
|
+
run_action_with_inputs(message)
|
65
|
+
end
|
66
|
+
rescue RuntimeError => ex
|
67
|
+
Types::Result.error(ex)
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_keyword(keyword)
|
71
|
+
response = @keyword_handler.run_command_for_keyword(keyword)
|
72
|
+
return Results.unknown_input if response.nil?
|
73
|
+
Types::Result.info(response)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pug
|
4
|
+
# Parses numeric values from text
|
5
|
+
class NumberParser
|
6
|
+
# Indicates if a text starts with a number
|
7
|
+
# @param text [String] text to test
|
8
|
+
# @return [Boolean] if text starts with numeric text
|
9
|
+
def starts_with_numeric_text?(text)
|
10
|
+
text.to_i.positive? || text.strip.start_with?('0')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Extracts number from text if it starts with
|
14
|
+
# a number
|
15
|
+
# @param text [String] text to extract number from
|
16
|
+
# @return [Integer, nil] number from text or nil
|
17
|
+
def number_from_text(text)
|
18
|
+
return nil unless starts_with_numeric_text?(text)
|
19
|
+
text.to_i
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|