bot_mob 0.2.2 → 0.3.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 +4 -4
- data/.env +1 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +16 -0
- data/.travis.yml +1 -1
- data/Gemfile +0 -2
- data/LICENSE.txt +21 -0
- data/README.md +5 -4
- data/bin/console +8 -4
- data/bin/mob +4 -0
- data/bot_mob.gemspec +8 -14
- data/examples/fizz_buzz_bot.rb +30 -0
- data/examples/hello_bot.rb +14 -0
- data/examples/server.rb +7 -0
- data/lib/bot_mob.rb +46 -32
- data/lib/bot_mob/bot.rb +146 -29
- data/lib/bot_mob/command.rb +38 -0
- data/lib/bot_mob/connection.rb +18 -50
- data/lib/bot_mob/core_ext/hash.rb +148 -0
- data/lib/bot_mob/core_ext/string.rb +8 -0
- data/lib/bot_mob/environment.rb +25 -0
- data/lib/bot_mob/inbound_message.rb +13 -32
- data/lib/bot_mob/networks/roaming.rb +12 -0
- data/lib/bot_mob/networks/roaming/connection.rb +34 -0
- data/lib/bot_mob/networks/roaming/outbound_message.rb +15 -0
- data/lib/bot_mob/networks/slack.rb +11 -0
- data/lib/bot_mob/networks/slack/connection.rb +37 -0
- data/lib/bot_mob/networks/slack/outbound_message.rb +18 -0
- data/lib/bot_mob/outbound_message.rb +23 -10
- data/lib/bot_mob/public/console.js +10 -0
- data/lib/bot_mob/roster.rb +8 -80
- data/lib/bot_mob/server.rb +47 -0
- data/lib/bot_mob/version.rb +1 -1
- data/lib/bot_mob/views/authorize.erb +1 -0
- data/lib/bot_mob/views/console.erb +6 -0
- data/lib/bot_mob/views/index.erb +1 -0
- data/lib/bot_mob/views/layout.erb +11 -0
- metadata +50 -110
- data/lib/bot_mob/ambassador.rb +0 -34
- data/lib/bot_mob/application.rb +0 -43
- data/lib/bot_mob/authority.rb +0 -41
- data/lib/bot_mob/development/ambassador.rb +0 -21
- data/lib/bot_mob/development/connection.rb +0 -34
- data/lib/bot_mob/development/inbound_message.rb +0 -17
- data/lib/bot_mob/errors.rb +0 -12
- data/lib/bot_mob/install.rb +0 -16
- data/lib/bot_mob/nil_connection.rb +0 -29
- data/lib/bot_mob/slack.rb +0 -10
- data/lib/bot_mob/slack/ambassador.rb +0 -58
- data/lib/bot_mob/slack/connection.rb +0 -87
- data/lib/bot_mob/slack/inbound_message.rb +0 -52
- data/lib/bot_mob/wire.rb +0 -69
@@ -0,0 +1,18 @@
|
|
1
|
+
module BotMob
|
2
|
+
module Networks
|
3
|
+
module Slack
|
4
|
+
# # BotMob::Networks::Slack::OutboundMessage
|
5
|
+
#
|
6
|
+
# Formatted message for delivery to slack
|
7
|
+
class OutboundMessage < BotMob::OutboundMessage
|
8
|
+
attr_reader :inbound, :channel
|
9
|
+
|
10
|
+
def initialize(**options)
|
11
|
+
@network = :slack
|
12
|
+
@channel = '#general'
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,19 +1,32 @@
|
|
1
1
|
module BotMob
|
2
|
-
#
|
2
|
+
# # BotMob::OutboundMessage
|
3
3
|
#
|
4
|
-
#
|
5
|
-
# way to consistently allow your bot to respond
|
6
|
-
# to a given network
|
4
|
+
# Strucutred data for all messages leaving your bot
|
7
5
|
class OutboundMessage
|
8
|
-
attr_reader :
|
6
|
+
attr_reader :body, :network, :inbound
|
9
7
|
|
10
|
-
def initialize(
|
11
|
-
@
|
12
|
-
@
|
8
|
+
def initialize(**options)
|
9
|
+
@network ||= options[:network]
|
10
|
+
@inbound = options[:inbound]
|
11
|
+
@body = options[:body]
|
13
12
|
end
|
14
13
|
|
15
|
-
def
|
16
|
-
|
14
|
+
def self.prepare(message, options = {})
|
15
|
+
return message if message.is_a?(BotMob::OutboundMessage)
|
16
|
+
|
17
|
+
target_network = options[:inbound].network
|
18
|
+
message_delegate(target_network).new(options.merge(body: message))
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
private
|
23
|
+
|
24
|
+
def message_delegate(network)
|
25
|
+
delegate = BotMob::Networks.const_get(network.to_s.camelize)
|
26
|
+
delegate.const_get('OutboundMessage')
|
27
|
+
rescue NameError
|
28
|
+
raise BotMob::InvalidNetworkError
|
29
|
+
end
|
17
30
|
end
|
18
31
|
end
|
19
32
|
end
|
data/lib/bot_mob/roster.rb
CHANGED
@@ -1,89 +1,17 @@
|
|
1
1
|
module BotMob
|
2
|
+
# ## BotMob::Roster
|
3
|
+
#
|
4
|
+
# This manages the pool of bots made available
|
5
|
+
# by the application
|
2
6
|
class Roster
|
3
|
-
attr_reader :
|
7
|
+
attr_reader :bots
|
4
8
|
|
5
9
|
def initialize
|
6
|
-
@
|
7
|
-
@bots = {}
|
10
|
+
@bots = []
|
8
11
|
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
# `connect` receives an auth hash that contains a bot_class
|
13
|
-
# along with any accompanying metadata that the bot will need
|
14
|
-
# to connect to the specified network. Provided by BotMob::Authority
|
15
|
-
# delivered over the wire.
|
16
|
-
#
|
17
|
-
# Example `auth` param:
|
18
|
-
#
|
19
|
-
# {
|
20
|
-
# bot_class: 'FooBot',
|
21
|
-
# network: 'slack',
|
22
|
-
# external_id: 'USER1',
|
23
|
-
# data: {
|
24
|
-
# token: 'abc123'
|
25
|
-
# }
|
26
|
-
# }
|
27
|
-
#
|
28
|
-
def connect(auth)
|
29
|
-
# { network: :dev, external_id: 'foo', bot_class: 'FooBot', data: {} }
|
30
|
-
class_name = auth['bot_class'].to_s
|
31
|
-
bot_class = registry[class_name]
|
32
|
-
raise BotMob::UnregisteredBotError, 'You must explicitly register your bot class with the roster before connecting that bot type' if bot_class.nil?
|
33
|
-
BotMob.logger.info "Selecting #{class_name.inspect} from registry: #{bot_class.inspect}"
|
34
|
-
|
35
|
-
unless auth.is_a?(BotMob::Install)
|
36
|
-
begin
|
37
|
-
BotMob.logger.info "Updating #{class_name} database record for #{auth['external_id']}..."
|
38
|
-
BotMob::Install.create_with_auth(auth)
|
39
|
-
rescue ActiveRecord::StatementInvalid => e
|
40
|
-
BotMob.logger.info "Unable to save record: #{e.message}"
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
network = auth['network']
|
45
|
-
allocate(bot_class, network, auth)
|
46
|
-
end
|
47
|
-
|
48
|
-
def load
|
49
|
-
begin
|
50
|
-
BotMob::Install.all.each do |install|
|
51
|
-
connect(install)
|
52
|
-
end
|
53
|
-
|
54
|
-
BotMob.logger.info '=> Roster loaded'
|
55
|
-
@available = true
|
56
|
-
rescue ActiveRecord::ConnectionNotEstablished => e
|
57
|
-
@available = false
|
58
|
-
BotMob.logger.info "=> Database unavailable, skipping roster load"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def [](key)
|
63
|
-
registry[key.to_s]
|
64
|
-
end
|
65
|
-
|
66
|
-
def register(bot_class)
|
67
|
-
@registry[bot_class.to_s] = bot_class
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def available?
|
73
|
-
@available.nil? || @available
|
74
|
-
end
|
75
|
-
|
76
|
-
def allocate(bot_class, network, auth)
|
77
|
-
key = "#{bot_class}##{auth['network']}.#{auth['external_id']}"
|
78
|
-
|
79
|
-
if @bots[key]
|
80
|
-
BotMob.logger.info "Using existing #{key} bot"
|
81
|
-
else
|
82
|
-
BotMob.logger.info "Setting up a new #{bot_class} bot: #{key}"
|
83
|
-
@bots[key] = bot_class.new(auth['network'].to_sym, auth['external_id'], auth['data'] || {})
|
84
|
-
end
|
85
|
-
|
86
|
-
@bots[key].refresh
|
13
|
+
def register(bot, **_options)
|
14
|
+
@bots << (bot.is_a?(Class) ? bot.new : bot)
|
87
15
|
end
|
88
16
|
end
|
89
17
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module BotMob
|
4
|
+
# # BotMob::Server
|
5
|
+
#
|
6
|
+
# Basic server for external access to
|
7
|
+
# connected bots.
|
8
|
+
class Server < Sinatra::Base
|
9
|
+
def self.run!(**options)
|
10
|
+
set :roster, options[:roster] || BotMob::Roster.new
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/' do
|
15
|
+
erb :index
|
16
|
+
end
|
17
|
+
|
18
|
+
get '/console' do
|
19
|
+
erb :console
|
20
|
+
end
|
21
|
+
|
22
|
+
get '/authorize' do
|
23
|
+
erb :authorize
|
24
|
+
end
|
25
|
+
|
26
|
+
get '/message' do
|
27
|
+
results = []
|
28
|
+
settings.roster.bots.map do |bot|
|
29
|
+
result = bot.receive(params[:text], network: params[:network])
|
30
|
+
results << result.body unless result.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
results.join(', ')
|
34
|
+
end
|
35
|
+
|
36
|
+
get '/command/:method' do
|
37
|
+
results = []
|
38
|
+
settings.roster.bots.map do |bot|
|
39
|
+
method_name = params[:method].to_sym
|
40
|
+
result = bot.receive_command(method_name)
|
41
|
+
results << result unless result.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
results.join(', ')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/bot_mob/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
<div>Authorize</div>
|
@@ -0,0 +1 @@
|
|
1
|
+
Homepage
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bot_mob
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Werner
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: sinatra
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '1.4'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '1.4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: slack-ruby-client
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,48 +38,6 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.7'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: celluloid-io
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0.17'
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0.17'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: activerecord
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '4.1'
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '4.1'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: dotenv
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
41
|
- !ruby/object:Gem::Dependency
|
84
42
|
name: bundler
|
85
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,143 +67,125 @@ dependencies:
|
|
109
67
|
- !ruby/object:Gem::Version
|
110
68
|
version: '10.0'
|
111
69
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
70
|
+
name: rspec
|
113
71
|
requirement: !ruby/object:Gem::Requirement
|
114
72
|
requirements:
|
115
73
|
- - "~>"
|
116
74
|
- !ruby/object:Gem::Version
|
117
|
-
version: '0
|
75
|
+
version: '3.0'
|
118
76
|
type: :development
|
119
77
|
prerelease: false
|
120
78
|
version_requirements: !ruby/object:Gem::Requirement
|
121
79
|
requirements:
|
122
80
|
- - "~>"
|
123
81
|
- !ruby/object:Gem::Version
|
124
|
-
version: '0
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: rubocop-rspec
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
82
|
+
version: '3.0'
|
139
83
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
84
|
+
name: dotenv
|
141
85
|
requirement: !ruby/object:Gem::Requirement
|
142
86
|
requirements:
|
143
87
|
- - "~>"
|
144
88
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
89
|
+
version: '2.1'
|
146
90
|
type: :development
|
147
91
|
prerelease: false
|
148
92
|
version_requirements: !ruby/object:Gem::Requirement
|
149
93
|
requirements:
|
150
94
|
- - "~>"
|
151
95
|
- !ruby/object:Gem::Version
|
152
|
-
version: '
|
96
|
+
version: '2.1'
|
153
97
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
98
|
+
name: rubocop
|
155
99
|
requirement: !ruby/object:Gem::Requirement
|
156
100
|
requirements:
|
157
101
|
- - "~>"
|
158
102
|
- !ruby/object:Gem::Version
|
159
|
-
version: '
|
103
|
+
version: '0.45'
|
160
104
|
type: :development
|
161
105
|
prerelease: false
|
162
106
|
version_requirements: !ruby/object:Gem::Requirement
|
163
107
|
requirements:
|
164
108
|
- - "~>"
|
165
109
|
- !ruby/object:Gem::Version
|
166
|
-
version: '
|
110
|
+
version: '0.45'
|
167
111
|
- !ruby/object:Gem::Dependency
|
168
|
-
name:
|
112
|
+
name: simplecov
|
169
113
|
requirement: !ruby/object:Gem::Requirement
|
170
114
|
requirements:
|
171
|
-
- - "
|
115
|
+
- - "~>"
|
172
116
|
- !ruby/object:Gem::Version
|
173
|
-
version: '0'
|
117
|
+
version: '0.12'
|
174
118
|
type: :development
|
175
119
|
prerelease: false
|
176
120
|
version_requirements: !ruby/object:Gem::Requirement
|
177
121
|
requirements:
|
178
|
-
- - "
|
122
|
+
- - "~>"
|
179
123
|
- !ruby/object:Gem::Version
|
180
|
-
version: '0'
|
124
|
+
version: '0.12'
|
181
125
|
- !ruby/object:Gem::Dependency
|
182
126
|
name: webmock
|
183
|
-
requirement: !ruby/object:Gem::Requirement
|
184
|
-
requirements:
|
185
|
-
- - ">="
|
186
|
-
- !ruby/object:Gem::Version
|
187
|
-
version: '0'
|
188
|
-
type: :development
|
189
|
-
prerelease: false
|
190
|
-
version_requirements: !ruby/object:Gem::Requirement
|
191
|
-
requirements:
|
192
|
-
- - ">="
|
193
|
-
- !ruby/object:Gem::Version
|
194
|
-
version: '0'
|
195
|
-
- !ruby/object:Gem::Dependency
|
196
|
-
name: sqlite3
|
197
127
|
requirement: !ruby/object:Gem::Requirement
|
198
128
|
requirements:
|
199
129
|
- - "~>"
|
200
130
|
- !ruby/object:Gem::Version
|
201
|
-
version: '1.
|
131
|
+
version: '1.22'
|
202
132
|
type: :development
|
203
133
|
prerelease: false
|
204
134
|
version_requirements: !ruby/object:Gem::Requirement
|
205
135
|
requirements:
|
206
136
|
- - "~>"
|
207
137
|
- !ruby/object:Gem::Version
|
208
|
-
version: '1.
|
209
|
-
description: Easily create
|
138
|
+
version: '1.22'
|
139
|
+
description: Easily create and manage bots
|
210
140
|
email:
|
211
141
|
- m@mjw.io
|
212
142
|
executables: []
|
213
143
|
extensions: []
|
214
144
|
extra_rdoc_files: []
|
215
145
|
files:
|
146
|
+
- ".env"
|
216
147
|
- ".env.test"
|
217
148
|
- ".gitignore"
|
149
|
+
- ".rubocop.yml"
|
218
150
|
- ".travis.yml"
|
219
151
|
- CODE_OF_CONDUCT.md
|
220
152
|
- Gemfile
|
153
|
+
- LICENSE.txt
|
221
154
|
- README.md
|
222
155
|
- Rakefile
|
223
156
|
- bin/console
|
157
|
+
- bin/mob
|
224
158
|
- bin/setup
|
225
159
|
- bot_mob.gemspec
|
160
|
+
- examples/fizz_buzz_bot.rb
|
161
|
+
- examples/hello_bot.rb
|
162
|
+
- examples/server.rb
|
226
163
|
- lib/bot_mob.rb
|
227
|
-
- lib/bot_mob/ambassador.rb
|
228
|
-
- lib/bot_mob/application.rb
|
229
|
-
- lib/bot_mob/authority.rb
|
230
164
|
- lib/bot_mob/bot.rb
|
165
|
+
- lib/bot_mob/command.rb
|
231
166
|
- lib/bot_mob/connection.rb
|
232
|
-
- lib/bot_mob/
|
233
|
-
- lib/bot_mob/
|
234
|
-
- lib/bot_mob/
|
235
|
-
- lib/bot_mob/errors.rb
|
167
|
+
- lib/bot_mob/core_ext/hash.rb
|
168
|
+
- lib/bot_mob/core_ext/string.rb
|
169
|
+
- lib/bot_mob/environment.rb
|
236
170
|
- lib/bot_mob/inbound_message.rb
|
237
|
-
- lib/bot_mob/
|
238
|
-
- lib/bot_mob/
|
171
|
+
- lib/bot_mob/networks/roaming.rb
|
172
|
+
- lib/bot_mob/networks/roaming/connection.rb
|
173
|
+
- lib/bot_mob/networks/roaming/outbound_message.rb
|
174
|
+
- lib/bot_mob/networks/slack.rb
|
175
|
+
- lib/bot_mob/networks/slack/connection.rb
|
176
|
+
- lib/bot_mob/networks/slack/outbound_message.rb
|
239
177
|
- lib/bot_mob/outbound_message.rb
|
178
|
+
- lib/bot_mob/public/console.js
|
240
179
|
- lib/bot_mob/roster.rb
|
241
|
-
- lib/bot_mob/
|
242
|
-
- lib/bot_mob/slack/ambassador.rb
|
243
|
-
- lib/bot_mob/slack/connection.rb
|
244
|
-
- lib/bot_mob/slack/inbound_message.rb
|
180
|
+
- lib/bot_mob/server.rb
|
245
181
|
- lib/bot_mob/version.rb
|
246
|
-
- lib/bot_mob/
|
182
|
+
- lib/bot_mob/views/authorize.erb
|
183
|
+
- lib/bot_mob/views/console.erb
|
184
|
+
- lib/bot_mob/views/index.erb
|
185
|
+
- lib/bot_mob/views/layout.erb
|
247
186
|
homepage: https://github.com/mwerner/bot_mob
|
248
|
-
licenses:
|
187
|
+
licenses:
|
188
|
+
- MIT
|
249
189
|
metadata: {}
|
250
190
|
post_install_message:
|
251
191
|
rdoc_options: []
|
@@ -255,7 +195,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
255
195
|
requirements:
|
256
196
|
- - ">="
|
257
197
|
- !ruby/object:Gem::Version
|
258
|
-
version: '
|
198
|
+
version: '0'
|
259
199
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
260
200
|
requirements:
|
261
201
|
- - ">="
|
@@ -263,8 +203,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
263
203
|
version: '0'
|
264
204
|
requirements: []
|
265
205
|
rubyforge_project:
|
266
|
-
rubygems_version: 2.4.
|
206
|
+
rubygems_version: 2.4.5.1
|
267
207
|
signing_key:
|
268
208
|
specification_version: 4
|
269
|
-
summary:
|
209
|
+
summary: The bot mob framework
|
270
210
|
test_files: []
|