pampa_workers 1.1.3

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1e39a8aa4b418ef142a3a2283028092c93f30eed
4
+ data.tar.gz: fcfed79846916674bf24a3cabfab7ba02a4d1a4e
5
+ SHA512:
6
+ metadata.gz: a133aa8e8d815c0a843bb07822c0a63119241a52aa28e9c215abd04390ebee46985df75bb9cb97543b2dbf679cd8c104dcb47ee47946489bcf7cffb41886d94e
7
+ data.tar.gz: 0c31ff155be2ef75bdf1c3b4f587adfdd01f8bab60a17efdfbf7c6ebfe19e3390794c90cf2af5e927d0ea6e96fde1a4c4b34cf038cc6500b17a2c88d16338f1d
@@ -0,0 +1,5 @@
1
+ module BlackStack
2
+ module BaseDivision
3
+
4
+ end # BaseDivision
5
+ end # module BlackStack
@@ -0,0 +1,5 @@
1
+ module BlackStack
2
+ module BaseWorker
3
+ KEEP_ACTIVE_MINUTES = 5 # if the worker didnt send a ping during the last X minutes, it is considered as not-active
4
+ end # BaseWorker
5
+ end # module BlackStack
@@ -0,0 +1,189 @@
1
+ require 'simple_host_monitoring'
2
+ require_relative './user'
3
+ require_relative './role'
4
+ require_relative './timezone'
5
+
6
+ module BlackStack
7
+ class Client < Sequel::Model(:client)
8
+ BlackStack::Client.dataset = BlackStack::Client.dataset.disable_insert_output
9
+
10
+ one_to_many :users, :class=>:'BlackStack::User', :key=>:id_client
11
+ many_to_one :timezone, :class=>:'BlackStack::Timezone', :key=>:id_timezone
12
+ many_to_one :billingCountry, :class=>:'BlackStack::LnCountry', :key=>:billing_id_lncountry
13
+ many_to_one :user_to_contect, :class=>'BlackStack:::User', :key=>:id_user_to_contact
14
+
15
+
16
+ # -----------------------------------------------------------------------------------------
17
+ # Arrays
18
+ #
19
+ #
20
+ # -----------------------------------------------------------------------------------------
21
+
22
+ # returns the workers belong this clients, that have not been deleted
23
+ def not_deleted_workers()
24
+ BlackStack::Worker.where(:id_client=>self.id, :delete_time=>nil)
25
+ end
26
+
27
+ # returns the hosts where this client has not-deleted workers, even if the host is not belong this client
28
+ def hosts()
29
+ BlackStack::LocalHost.where(id: self.not_deleted_workers.select(:id_host).all.map(&:id_host))
30
+ end
31
+
32
+ # returns the hosts belong this client
33
+ def own_hosts()
34
+ BlackStack::LocalHost.where(:id_client=>self.id, :delete_time=>nil)
35
+ end
36
+
37
+ # -----------------------------------------------------------------------------------------
38
+ # Configuration
39
+ #
40
+ #
41
+ # -----------------------------------------------------------------------------------------
42
+
43
+ # retorna true si la 5 variables (billing_address, billing_city, billing_state, billing_zipcode, billing_id_lncountry) tienen un valor destinto a nil o a un string vacio.
44
+ def hasBillingAddress?
45
+ if (
46
+ self.billing_address.to_s.size == 0 ||
47
+ self.billing_city.to_s.size == 0 ||
48
+ self.billing_state.to_s.size == 0 ||
49
+ self.billing_zipcode.to_s.size == 0 ||
50
+ self.billing_id_lncountry.to_s.size == 0
51
+ )
52
+ return false
53
+ else
54
+ return true
55
+ end
56
+ end
57
+
58
+ # retorna un array de objectos UserRole, asignados a todos los usuarios de este cliente
59
+ def user_roles
60
+ a = []
61
+ self.users.each { |o|
62
+ a.concat o.user_roles
63
+ # libero recursos
64
+ GC.start
65
+ DB.disconnect
66
+ }
67
+ a
68
+ end
69
+
70
+ # si el cliente no tiene una zona horaria configurada, retorna la zona horaria por defecto
71
+ # excepciones:
72
+ # => "Default timezone not found."
73
+ def getTimezone()
74
+ ret = nil
75
+ if (self.timezone != nil)
76
+ ret = self.timezone
77
+ else
78
+ ret = BlackStack::Timezone.where(:id=>BlackStack::Pampa::id_timezone_default).first
79
+ if (ret == nil)
80
+ raise "Default timezone not found."
81
+ end
82
+ end
83
+ return ret
84
+ end
85
+
86
+ # llama a la api de postmark preguntando el reseller email configurado para este clietne fue ferificado
87
+ def checkDomainForSSMVerified()
88
+ return_message = {}
89
+ domain = self.domain_for_ssm
90
+ email = self.from_email_for_ssm
91
+ id = ''
92
+ client = ''
93
+
94
+ if domain != nil && email != nil
95
+ begin
96
+ client_postmark = Postmark::AccountApiClient.new(POSTMARK_API_TOKEN, secure: true)
97
+ client_list = client_postmark.get_signatures()
98
+
99
+ client_list.each do |sign|
100
+ if sign[:domain] == domain
101
+ if sign[:email_address] == email
102
+ id = sign[:id]
103
+ break
104
+ end
105
+ end
106
+ end
107
+
108
+ if id.to_s.size > 0
109
+ client = client_postmark.get_sender(id)
110
+ if !client[:confirmed]
111
+ self.domain_for_ssm_verified = false
112
+ self.save
113
+
114
+ return_message[:status] = "No Verified"
115
+ return_message[:value] = client[:id]
116
+
117
+ return return_message.to_json
118
+ else
119
+ self.domain_for_ssm_verified = true
120
+ self.save
121
+
122
+ # sincronizo con la central
123
+ return_message = {}
124
+
125
+ return_message[:status] = "success"
126
+ return_message[:value] = client[:id]
127
+
128
+ return return_message.to_json
129
+ end
130
+ end
131
+
132
+ rescue Postmark::ApiInputError => e
133
+ return_message[:status] = e.to_s
134
+ return return_message.to_json
135
+ #return e
136
+ rescue => e
137
+ return_message[:status] = e.to_s
138
+ return return_message.to_json
139
+ #return e
140
+ end
141
+ else
142
+ return_message[:status] = 'error'
143
+ return_message[:value] = ''
144
+ return return_message.to_json
145
+ end # checkDomainForSSMVerified
146
+ end
147
+
148
+ # retorna true si el cliente esta configurado para usar su propia nombre/email en el envio de notificaciones
149
+ def resellerSignature?
150
+ self.from_name_for_ssm.to_s.size>0 && self.from_email_for_ssm.to_s.size>0
151
+ end
152
+
153
+ # retorna true si el cliente esta configurado para usar su propia nombre/email en el envio de notificaciones, y si el email fue verificado en postmark
154
+ def resellerSignatureEnabled?
155
+ =begin # TODO: Mover esto a un proceso asincronico
156
+ # si el cliente esta configurado para usar su propia nombre/email
157
+ if self.resellerSignature?
158
+ # pero el email fue verificado en postmark
159
+ if self.domain_for_ssm_verified==nil || self.domain_for_ssm_verified!=true
160
+ # hago la verificacion contra postmark
161
+ self.checkDomainForSSMVerified
162
+ end
163
+ end
164
+ =end
165
+ # return
166
+ resellerSignature? == true && self.domain_for_ssm_verified==true
167
+ end
168
+
169
+ # retorna el email configurado y confirmado en PostMark para cuenta reseller, o retorna el email por defecto
170
+ def resellerSignatureEmail
171
+ # configuracion de cuenta reseller
172
+ if self.resellerSignatureEnabled?
173
+ return self.from_email_for_ssm.to_s
174
+ else
175
+ return BlackStack::Params.getValue("notifications.user.email_from")
176
+ end
177
+ end
178
+
179
+ # retorna el nombre configurado para cuenta reseller, solo si el email esta confirmado en PostMark; o retorna el email por defecto
180
+ def resellerSignatureName
181
+ # configuracion de cuenta reseller
182
+ if self.resellerSignatureEnabled?
183
+ return self.from_email_for_ssm.to_s
184
+ else
185
+ return BlackStack::Params.getValue("notifications.user.name_from")
186
+ end
187
+ end
188
+ end # class Client
189
+ end # module BlackStack
@@ -0,0 +1,46 @@
1
+ module BlackStack
2
+
3
+ class Division < Sequel::Model(:division)
4
+ include BlackStack::BaseDivision
5
+ Division.dataset = Division.dataset.disable_insert_output
6
+
7
+ def home()
8
+ Division.where(
9
+ :db_url=>self.db_url,
10
+ :db_port=>self.db_port,
11
+ :db_user=>self.db_user,
12
+ :db_password=>self.db_password,
13
+ :db_name=>self.db_name,
14
+ :home=>true,
15
+ :available=>true
16
+ ).first
17
+ end
18
+
19
+ def self.getDefault()
20
+ q =
21
+ "SELECT TOP 1 d.id AS did " +
22
+ "FROM division d " +
23
+ "WHERE d.name='#{SIGNUP_DIVISION}' "
24
+ row = DB[q].first
25
+ if (row==nil)
26
+ return nil
27
+ end
28
+ return Division.where(:id=>row[:did]).first
29
+ end
30
+
31
+ # Actualiza el campo stat_name de todas las divisiones que son "gemelas" as la division pasada por parametro.
32
+ # Ver issue #976.
33
+ def self.updateStat(division, stat_name, date_time)
34
+ Division.where(
35
+ :db_url=>division.db_url,
36
+ :db_port=>division.db_port,
37
+ :db_user=>division.db_user,
38
+ :db_password=>division.db_password,
39
+ ).each { |d|
40
+ q = "UPDATE division SET #{stat_name}='#{date_time.to_s}' WHERE id='#{d.id}'"
41
+ }
42
+ end
43
+
44
+ end # class
45
+
46
+ end # module BlackStack
@@ -0,0 +1,7 @@
1
+ module BlackStack
2
+ class Login < Sequel::Model(:login)
3
+ BlackStack::Login.dataset = BlackStack::Login.dataset.disable_insert_output
4
+
5
+ many_to_one :user, :class=>:'BlackStack::User', :key=>:id_user
6
+ end
7
+ end # module BlackStack
@@ -0,0 +1,348 @@
1
+ module BlackStack
2
+
3
+ # clase de base para todos los bots ejecuten acciones con una cuenta de LinkedIn, Facebook, Twitter, etc.
4
+ class MyBotProcess < BlackStack::MyRemoteProcess
5
+ attr_accessor :username, :login_verifications, :run_once
6
+
7
+ # constructor
8
+ def initialize(
9
+ the_worker_name,
10
+ the_division_name,
11
+ the_minimum_enlapsed_seconds=MyProcess::DEFAULT_MINIMUM_ENLAPSED_SECONDS,
12
+ the_verify_configuration=true,
13
+ the_email=nil,
14
+ the_password=nil
15
+ )
16
+ super(the_worker_name, the_division_name, the_minimum_enlapsed_seconds, the_verify_configuration, the_email, the_password)
17
+ self.assigned_process = File.expand_path($0)
18
+ self.worker_name = "#{the_worker_name}"
19
+ self.division_name = the_division_name
20
+ self.minimum_enlapsed_seconds = the_minimum_enlapsed_seconds
21
+
22
+ # algunas clases como CreateLnUserProcess o RepairLnUserProcess, trabajan unicamente con el username especificado en este atributo, llamando al access point get_lnuser_by_username.
23
+ # si este atributo es nil, entonces la clase pide un lnuser a la division, llamando al access point get_lnuser.
24
+ self.username = nil
25
+
26
+ # al correr un proceso sin supervision, el login require verificaciones automaticas que demoran tiempo (account blocingcaptcha, sms pin, bloqueo)
27
+ # las verificaciones consument tiempo.
28
+ # si este proceso se corre de forma supevisada, las verificaciones se pueden deshabilitar
29
+ self.login_verifications = true
30
+
31
+ # al correr sin supervision, el proceso de terminar un un paquete de procesamiento y comenzar con otro, funcionando en un loop infinito.
32
+ # si este proceso se corre de forma supevisada, se desa correr el procesamiento una unica vez.
33
+ # cuando se activa este flag, generalmente se setea el atributo self.username tambien.
34
+ self.run_once = false
35
+ end
36
+
37
+ # returns a hash with the parameters of a lnuser
38
+ # raises an exception if it could not get a lnuser, or if ocurrs any other problem
39
+ def getLnUserByUsername(username)
40
+ nTries = 0
41
+ parsed = nil
42
+ lnuser = nil # hash
43
+ bSuccess = false
44
+ sError = ""
45
+ while (nTries < 5 && bSuccess == false)
46
+ begin
47
+ nTries = nTries + 1
48
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/login.lnuser/get_lnuser.json"
49
+ res = BlackStack::Netting::call_post(url, {'api_key' => BlackStack::Pampa::api_key, 'username' => username.encode("UTF-8")})
50
+ parsed = JSON.parse(res.body)
51
+ if (parsed['status']=='success')
52
+ lnuser = parsed
53
+ bSuccess = true
54
+ else
55
+ sError = parsed['status']
56
+ end
57
+ rescue Errno::ECONNREFUSED => e
58
+ sError = "Errno::ECONNREFUSED:" + e.to_console
59
+ rescue => e2
60
+ sError = "Exception: " + e2.to_console
61
+ end
62
+ end # while
63
+
64
+ if (bSuccess==false)
65
+ raise BlackStack::Netting::ApiCallException.new(sError)
66
+ end
67
+
68
+ return lnuser
69
+ end # getLnUser()
70
+
71
+ # returns a hash with the parameters of a lnuser
72
+ # raises an exception if it could not get a lnuser, or if ocurrs any other problem
73
+ def getLnUser(workflow_name='incrawl.lnsearchvariation')
74
+ nTries = 0
75
+ parsed = nil
76
+ lnuser = nil # hash
77
+ bSuccess = false
78
+ sError = ""
79
+ while (nTries < 5 && bSuccess == false)
80
+ begin
81
+ nTries = nTries + 1
82
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/#{workflow_name}/get_lnuser.json"
83
+ res = BlackStack::Netting::call_post(url, {'api_key' => BlackStack::Pampa::api_key, 'name' => self.fullWorkerName})
84
+ parsed = JSON.parse(res.body)
85
+ if (parsed['status']=='success')
86
+ lnuser = parsed
87
+ bSuccess = true
88
+ else
89
+ sError = parsed['status']
90
+ end
91
+ rescue Errno::ECONNREFUSED => e
92
+ sError = "Errno::ECONNREFUSED:" + e.to_console
93
+ rescue => e2
94
+ sError = "Exception:" + e2.to_console
95
+ end
96
+ end # while
97
+
98
+ if (bSuccess==false)
99
+ raise BlackStack::Netting::ApiCallException.new(sError)
100
+ end
101
+
102
+ return lnuser
103
+ end # getLnUser()
104
+
105
+ #
106
+ def notifyLnUserUrl(id_lnuser, profile_url)
107
+ nTries = 0
108
+ parsed = nil
109
+ bSuccess = false
110
+ sError = ""
111
+ while (nTries < 5 && bSuccess == false)
112
+ begin
113
+ nTries = nTries + 1
114
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/login.lnuser/notify_url.json"
115
+ res = BlackStack::Netting::call_post(url,
116
+ {:api_key => BlackStack::Pampa::api_key,
117
+ 'id_lnuser' => id_lnuser,
118
+ 'url' => profile_url,}
119
+ )
120
+ parsed = JSON.parse(res.body)
121
+
122
+ if (parsed['status']=='success')
123
+ bSuccess = true
124
+ else
125
+ sError = parsed['status']
126
+ end
127
+ rescue Errno::ECONNREFUSED => e
128
+ sError = "Errno::ECONNREFUSED:" + e.to_console
129
+ rescue => e2
130
+ sError = "Exception:" + e2.to_console
131
+ end
132
+ end # while
133
+
134
+ if (bSuccess==false)
135
+ raise "#{sError}"
136
+ end
137
+ end # notifyLnUserStatus
138
+
139
+ #
140
+ def notifyLnUserStatus(id_lnuser, status, workflow_name='incrawl.lnsearchvariation')
141
+ nTries = 0
142
+ parsed = nil
143
+ bSuccess = false
144
+ sError = ""
145
+ while (nTries < 5 && bSuccess == false)
146
+ begin
147
+ nTries = nTries + 1
148
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/#{workflow_name}/notify_lnuser_status.json"
149
+ res = BlackStack::Netting::call_post(url,
150
+ {'api_key' => BlackStack::Pampa::api_key,
151
+ 'id_lnuser' => id_lnuser,
152
+ 'status' => status,}
153
+ )
154
+ parsed = JSON.parse(res.body)
155
+
156
+ if (parsed['status']=='success')
157
+ bSuccess = true
158
+ else
159
+ sError = parsed['status']
160
+ end
161
+ rescue Errno::ECONNREFUSED => e
162
+ sError = "Errno::ECONNREFUSED:" + e.to_console
163
+ rescue => e2
164
+ sError = "Exception:" + e2.to_console
165
+ end
166
+ end # while
167
+
168
+ if (bSuccess==false)
169
+ raise "#{sError}"
170
+ end
171
+
172
+ end # notifyLnUserStatus
173
+
174
+ #
175
+ def notifyLnUserActivity(id_lnuser, code, workflow_name='incrawl.lnsearchvariation')
176
+ nTries = 0
177
+ parsed = nil
178
+ bSuccess = false
179
+ sError = ""
180
+ while (nTries < 5 && bSuccess == false)
181
+ begin
182
+ nTries = nTries + 1
183
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/#{workflow_name}/notify_lnuser_activity.json"
184
+ res = BlackStack::Netting::call_post(url,
185
+ {'api_key' => BlackStack::Pampa::api_key,
186
+ 'id_lnuser' => id_lnuser,
187
+ 'code' => code,}
188
+ )
189
+ parsed = JSON.parse(res.body)
190
+
191
+ if (parsed['status']=='success')
192
+ bSuccess = true
193
+ else
194
+ sError = parsed['status']
195
+ end
196
+ rescue Errno::ECONNREFUSED => e
197
+ sError = "Errno::ECONNREFUSED:" + e.to_console
198
+ rescue => e2
199
+ sError = "Exception:" + e2.to_console
200
+ end
201
+ end # while
202
+
203
+ if (bSuccess==false)
204
+ raise "#{sError}"
205
+ end
206
+ end # notifyLnUserStatus
207
+
208
+ # Toma una captura del browser.
209
+ # Sube un registro a la tabla boterrorlog, con el id del worker, el proceso asinado, y el screenshot.
210
+ #
211
+ # uid: id de un registro en la tabla lnuser.
212
+ # description: backtrace de la excepcion.
213
+ #
214
+ def notifyError(uid, description, oid=nil)
215
+ # tomo captura de pantalla
216
+ file = nil
217
+ =begin # TODO: habilitar esto cuando se migre a RestClient en vez de CallPost
218
+ begin
219
+ screenshot_filename = "./error.png" # TODO: colocar un nombre unico formado por por el fullname del worker, y la fecha-hora.
220
+ BrowserFactory.screenshot screenshot_filename
221
+ file = File.new(screenshot_filename, "rb")
222
+ rescue => e
223
+ puts "Screenshot Error: #{e.to_s}"
224
+ file = nil
225
+ end
226
+ =end
227
+ #puts ""
228
+ #puts "id_worker:#{PROCESS.worker.id}"
229
+ #puts "worker_name:#{PROCESS.fullWorkerName}"
230
+ #puts "process:#{PROCESS.worker.assigned_process}"
231
+ #puts ""
232
+ # subo el error
233
+ nTries = 0
234
+ bSuccess = false
235
+ parsed = nil
236
+ sError = ""
237
+ while (nTries < 5 && bSuccess == false)
238
+ begin
239
+ nTries = nTries + 1
240
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/boterror.json"
241
+ res = BlackStack::Netting::call_post(url, # TODO: migrar a RestClient para poder hacer file upload
242
+ 'api_key' => BlackStack::Pampa::api_key,
243
+ 'id_lnuser' => uid,
244
+ 'id_object' => oid,
245
+ 'worker_name' => PROCESS.fullWorkerName,
246
+ 'process' => PROCESS.worker.assigned_process,
247
+ 'description' => description,
248
+ 'screenshot' => file,
249
+ )
250
+ parsed = JSON.parse(res.body)
251
+ if (parsed['status']=='success')
252
+ bSuccess = true
253
+ else
254
+ sError = parsed['status']
255
+ end
256
+ rescue Errno::ECONNREFUSED => e
257
+ sError = "Errno::ECONNREFUSED:" + e.to_console
258
+ rescue => e2
259
+ sError = "Exception:" + e2.to_console
260
+ end
261
+ end # while
262
+
263
+ if (bSuccess==false)
264
+ raise "#{sError}"
265
+ end
266
+ end
267
+
268
+ #
269
+ def isLnUserAvailable(id_lnuser, need_sales_navigator=false, workflow_name='incrawl.lnsearchvariation')
270
+ nTries = 0
271
+ parsed = nil
272
+ bSuccess = false
273
+ sError = ""
274
+ ret = false
275
+
276
+ while (nTries < 5 && bSuccess == false)
277
+ begin
278
+ nTries = nTries + 1
279
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/#{workflow_name}/is_lnuser_available.json"
280
+ res = BlackStack::Netting::call_post(url,
281
+ {'api_key' => BlackStack::Pampa::api_key,
282
+ 'id_lnuser' => id_lnuser,
283
+ 'need_sales_navigator' => need_sales_navigator,}
284
+ )
285
+ parsed = JSON.parse(res.body)
286
+
287
+ if (parsed['status']=='success')
288
+ bSuccess = true
289
+ ret = parsed['value']
290
+ else
291
+ sError = parsed['status']
292
+ end
293
+ rescue Errno::ECONNREFUSED => e
294
+ sError = "Errno::ECONNREFUSED:" + e.to_s
295
+ rescue => e2
296
+ sError = "Alghoritm Exception" + e2.to_s + '\r\n' + e2.backtrace.join("\r\n").to_s
297
+ end
298
+ end # while
299
+
300
+ if (bSuccess==false)
301
+ raise "#{sError}"
302
+ end
303
+
304
+ return ret
305
+ end # isLnUserAvailable
306
+
307
+ # TODO: deprecated
308
+ def releaseLnUser(id_lnuser, workflow_name='incrawl.lnsearchvariation')
309
+ =begin
310
+ nTries = 0
311
+ parsed = nil
312
+ bSuccess = false
313
+ sError = ""
314
+ ret = false
315
+
316
+ while (nTries < 5 && bSuccess == false)
317
+ begin
318
+ nTries = nTries + 1
319
+ url = "#{BlackStack::Pampa::api_protocol}://#{self.ws_url}:#{self.ws_port}/api1.3/pampa/#{workflow_name}/release_lnuser.json"
320
+ res = BlackStack::Netting::call_post(url,
321
+ {'api_key' => BlackStack::Pampa::api_key, 'id_lnuser' => id_lnuser,}
322
+ )
323
+ parsed = JSON.parse(res.body)
324
+
325
+ if (parsed['status']=='success')
326
+ bSuccess = true
327
+ ret = parsed['value']
328
+ else
329
+ sError = parsed['status']
330
+ end
331
+ rescue Errno::ECONNREFUSED => e
332
+ sError = "Errno::ECONNREFUSED:" + e.to_console
333
+ rescue => e2
334
+ sError = "Exception:" + e2.to_console
335
+ end
336
+ end # while
337
+
338
+ if (bSuccess==false)
339
+ raise "#{sError}"
340
+ end
341
+
342
+ return ret
343
+ =end
344
+ end # isLnUserAvailable
345
+
346
+ end # class MyBotProcess
347
+
348
+ end # module BlackStack