lita-gsuite 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +134 -0
- data/lib/lita-gsuite.rb +15 -0
- data/lib/lita/commands/deletion_candidates.rb +49 -0
- data/lib/lita/commands/empty_groups.rb +36 -0
- data/lib/lita/commands/list_activities.rb +27 -0
- data/lib/lita/commands/list_admins.rb +39 -0
- data/lib/lita/commands/no_org_unit.rb +38 -0
- data/lib/lita/commands/suspension_candidates.rb +49 -0
- data/lib/lita/commands/two_factor_off.rb +49 -0
- data/lib/lita/commands/two_factor_stats.rb +56 -0
- data/lib/lita/google_activity.rb +50 -0
- data/lib/lita/google_group.rb +29 -0
- data/lib/lita/google_organisation_unit.rb +16 -0
- data/lib/lita/google_user.rb +69 -0
- data/lib/lita/gsuite.rb +373 -0
- data/lib/lita/gsuite_gateway.rb +101 -0
- data/lib/lita/redis_token_store.rb +48 -0
- data/lib/lita/weekly_schedule.rb +54 -0
- data/lib/lita/window_schedule.rb +50 -0
- metadata +166 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Lita
|
4
|
+
module Commands
|
5
|
+
class TwoFactorStats
|
6
|
+
|
7
|
+
def name
|
8
|
+
'two-factor-stats'
|
9
|
+
end
|
10
|
+
|
11
|
+
def run(robot, target, gateway, opts = {})
|
12
|
+
msg = build_msg(gateway)
|
13
|
+
robot.send_message(target, msg) if msg
|
14
|
+
robot.send_message(target, "No stats found") if msg.nil? && opts[:negative_ack]
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_msg(gateway)
|
20
|
+
users = active_users(gateway)
|
21
|
+
if users.any?
|
22
|
+
users_with_tfa = users.select { |user| user.two_factor_enabled? }
|
23
|
+
ou_paths = users.group_by(&:ou_path).keys.sort
|
24
|
+
|
25
|
+
msg = "Active users with Two Factor Authentication enabled:\n\n"
|
26
|
+
ou_paths.each do |ou_path|
|
27
|
+
msg += ou_msg(ou_path, users) + "\n"
|
28
|
+
end
|
29
|
+
msg + "- Overall #{users_with_tfa.size}/#{users.size} (#{percentage(users_with_tfa.size, users.size)}%)"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def ou_msg(ou_path, users)
|
34
|
+
ou_users = users.select { |user| user.ou_path == ou_path }
|
35
|
+
with_tfa = ou_users.select { |user| user.two_factor_enabled? }
|
36
|
+
"- #{ou_path} #{with_tfa.size}/#{ou_users.size} (#{percentage(with_tfa.size, ou_users.size)}%)"
|
37
|
+
end
|
38
|
+
|
39
|
+
def percentage(num, denom)
|
40
|
+
if denom == 0
|
41
|
+
result = BigDecimal.new(0)
|
42
|
+
else
|
43
|
+
result = BigDecimal.new(num) / BigDecimal.new(denom) * 100
|
44
|
+
end
|
45
|
+
result.round(2).to_s("F")
|
46
|
+
end
|
47
|
+
|
48
|
+
def active_users(gateway)
|
49
|
+
gateway.users.reject { |user|
|
50
|
+
user.suspended?
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Lita
|
2
|
+
class GoogleActivity
|
3
|
+
attr_reader :time, :actor, :ip, :name, :params
|
4
|
+
|
5
|
+
def self.from_api(item)
|
6
|
+
item.events.map { |event|
|
7
|
+
GoogleActivity.new(
|
8
|
+
time: item.id.time,
|
9
|
+
actor: item.actor.email,
|
10
|
+
ip: item.ip_address,
|
11
|
+
name: event.name,
|
12
|
+
params: event.parameters.inject({}) { |accum, param|
|
13
|
+
accum[param.name] = param.value
|
14
|
+
accum
|
15
|
+
}
|
16
|
+
)
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(time:, actor:, ip:, name:, params:)
|
21
|
+
@time = time
|
22
|
+
@actor = actor
|
23
|
+
@ip = ip
|
24
|
+
@name = name
|
25
|
+
@params = params
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
@actor
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_msg
|
33
|
+
<<~EOF
|
34
|
+
Date: #{@time.httpdate}
|
35
|
+
Admin User: #{@actor}
|
36
|
+
Action: #{@name.capitalize.gsub('_', ' ')}
|
37
|
+
#{values}
|
38
|
+
EOF
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def values
|
44
|
+
@params.map do |key, value|
|
45
|
+
"#{key.gsub('_', ' ')}: #{value}"
|
46
|
+
end.join("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Lita
|
2
|
+
# A google email group
|
3
|
+
class GoogleGroup
|
4
|
+
attr_reader :id, :email, :name, :member_count, :description
|
5
|
+
|
6
|
+
def self.from_api(group)
|
7
|
+
GoogleGroup.new(
|
8
|
+
id: group.id,
|
9
|
+
email: group.email,
|
10
|
+
name: group.name,
|
11
|
+
description: group.description,
|
12
|
+
member_count: group.direct_members_count,
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(id:, email:, name:, member_count:, description:)
|
17
|
+
@id, @email, @name, @description = id, email, name, description
|
18
|
+
@member_count = member_count.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
@id == other.id && @email == other.email
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
@email
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Lita
|
2
|
+
# A google OrganisationUnit, contains zero or more users
|
3
|
+
class GoogleOrganisationUnit
|
4
|
+
attr_reader :name, :path
|
5
|
+
|
6
|
+
def self.from_api(item)
|
7
|
+
GoogleOrganisationUnit.new(
|
8
|
+
name: item.name, path: item.org_unit_path
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(name:, path:)
|
13
|
+
@name, @path = name, path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Lita
|
2
|
+
# A google apps user
|
3
|
+
class GoogleUser
|
4
|
+
attr_reader :id, :full_name, :email, :created_at, :last_login_at, :ou_path
|
5
|
+
|
6
|
+
def self.from_api_user(user)
|
7
|
+
GoogleUser.new(
|
8
|
+
id: user.id,
|
9
|
+
full_name: user.name.full_name,
|
10
|
+
email: user.primary_email,
|
11
|
+
suspended: user.suspended,
|
12
|
+
created_at: user.creation_time,
|
13
|
+
last_login_at: user.last_login_time,
|
14
|
+
ou_path: user.org_unit_path,
|
15
|
+
admin: user.is_admin,
|
16
|
+
delegated_admin: user.is_delegated_admin,
|
17
|
+
two_factor_enabled: user.is_enrolled_in2_sv,
|
18
|
+
two_factor_enforced: user.is_enforced_in2_sv,
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(id:, full_name:, email:, suspended:, created_at:, last_login_at:, ou_path:, admin:, delegated_admin:, two_factor_enabled:, two_factor_enforced:)
|
23
|
+
@id = id
|
24
|
+
@email = email
|
25
|
+
@full_name = full_name
|
26
|
+
@suspended = suspended
|
27
|
+
@created_at = created_at
|
28
|
+
@last_login_at = last_login_at
|
29
|
+
@ou_path = ou_path
|
30
|
+
@admin = admin
|
31
|
+
@delegated_admin = delegated_admin
|
32
|
+
@two_factor_enabled = two_factor_enabled
|
33
|
+
@two_factor_enforced = two_factor_enforced
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
@email
|
38
|
+
end
|
39
|
+
|
40
|
+
def admin?
|
41
|
+
@admin
|
42
|
+
end
|
43
|
+
|
44
|
+
def delegated_admin?
|
45
|
+
@delegated_admin
|
46
|
+
end
|
47
|
+
|
48
|
+
def path
|
49
|
+
if @ou_path.end_with?("/")
|
50
|
+
"#{@ou_path}#{@email}"
|
51
|
+
else
|
52
|
+
"#{@ou_path}/#{@email}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def suspended?
|
57
|
+
@suspended
|
58
|
+
end
|
59
|
+
|
60
|
+
def two_factor_enabled?
|
61
|
+
@two_factor_enabled
|
62
|
+
end
|
63
|
+
|
64
|
+
def two_factor_enforced?
|
65
|
+
@two_factor_enforced
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/lita/gsuite.rb
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
require "lita"
|
2
|
+
require "lita-timing"
|
3
|
+
require 'googleauth'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Lita
|
7
|
+
class Gsuite < Handler
|
8
|
+
TIMER_INTERVAL = 60
|
9
|
+
OOB_OAUTH_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
10
|
+
|
11
|
+
COMMANDS = [
|
12
|
+
Commands::ListAdmins,
|
13
|
+
Commands::EmptyGroups,
|
14
|
+
Commands::NoOrgUnit,
|
15
|
+
Commands::TwoFactorOff,
|
16
|
+
Commands::TwoFactorStats,
|
17
|
+
Commands::SuspensionCandidates,
|
18
|
+
Commands::DeletionCandidates,
|
19
|
+
].map { |cmd|
|
20
|
+
[cmd.new.name, cmd]
|
21
|
+
}.to_h
|
22
|
+
|
23
|
+
WINDOW_COMMANDS = [
|
24
|
+
Commands::ListActivities
|
25
|
+
].map { |cmd|
|
26
|
+
[cmd.new.name, cmd]
|
27
|
+
}.to_h
|
28
|
+
|
29
|
+
config :oauth_client_id
|
30
|
+
config :oauth_client_secret
|
31
|
+
|
32
|
+
# Authentication commands - each user is required to run these before they can interact with
|
33
|
+
# the Google API
|
34
|
+
route(/^gsuite auth$/, :start_auth, command: true, help: {"gsuite auth" => "Initiate the first of two steps required to authorise the current user wth Google"})
|
35
|
+
route(/^gsuite set-token (.+)$/, :set_token, command: true, help: {"gsuite set-token <token>" => "The second and final step required to authorise the current user with Google. Run 'gsuite auth' first"})
|
36
|
+
|
37
|
+
# Instant queries. Authenticated users can run these commands and the result will be returned
|
38
|
+
# immediately
|
39
|
+
route(/^gsuite list-admins$/, :list_admins, command: true, help: {"gsuite list-admins" => "List active admins"})
|
40
|
+
route(/^gsuite suspension-candidates$/, :suspension_candidates, command: true, help: {"gsuite suspension-candidates" => "List active users that habven't signed in for a while"})
|
41
|
+
route(/^gsuite deletion-candidates$/, :deletion_candidates, command: true, help: {"gsuite deletion-candidates" => "List suspended users that habven't signed in for a while"})
|
42
|
+
route(/^gsuite no-ou$/, :no_org_unit, command: true, help: {"gsuite no-ou" => "List users that aren't assigned to an Organisation Unit"})
|
43
|
+
route(/^gsuite empty-groups$/, :empty_groups, command: true, help: {"gsuite empty-groups" => "List groups with no users"})
|
44
|
+
route(/^gsuite two-factor-stats$/, :two_factor_stats, command: true, help: {"gsuite two-factor-stats" => "Display stats on option of two factor authentication"})
|
45
|
+
route(/^gsuite two-factor-off (.+)$/, :two_factor_off, command: true, help: {"gsuite two-factor-off <OU path>" => "List users from the OU path with two factor authentication off"})
|
46
|
+
|
47
|
+
# Control a schedule of automated commands to run in specific channels
|
48
|
+
route(/^gsuite schedule list$/, :schedule_list, command: true, help: {"gsuite schedule list" => "Print the list of scheduled gsuite commands for the current channel"})
|
49
|
+
route(/^gsuite schedule commands$/, :schedule_commands, command: true, help: {"gsuite schedule commands" => "Print the list of commands available for scheduling"})
|
50
|
+
route(/^gsuite schedule add-weekly (.+) (\d\d:\d\d) (.+)$/, :schedule_add_weekly, command: true, help: {"gsuite schedule add-weekly <day> <HH:MM> <cmd>" => "Add a new weekly scheduled command. Run 'gsuite schedule commands' to see the available commands"})
|
51
|
+
route(/^gsuite schedule add-window (.+)$/, :schedule_add_window, command: true, help: {"gsuite schedule add-window <cmd>" => "Add a new scheduled window command"})
|
52
|
+
route(/^gsuite schedule del (.+)$/, :schedule_delete, command: true, help: {"gsuite schedule del <cmd-id>" => "Delete a scheduled command. Requires a command ID, which is printed in 'gsuite schedule list' output"})
|
53
|
+
|
54
|
+
on :loaded, :start_timers
|
55
|
+
|
56
|
+
def start_timers(payload)
|
57
|
+
weekly_commands_timer
|
58
|
+
window_commands_timer
|
59
|
+
end
|
60
|
+
|
61
|
+
def deletion_candidates(response)
|
62
|
+
return unless confirm_user_authenticated(response)
|
63
|
+
|
64
|
+
Commands::DeletionCandidates.new.run(
|
65
|
+
robot,
|
66
|
+
Source.new(room: response.room),
|
67
|
+
gateway(response.user),
|
68
|
+
negative_ack: true
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def empty_groups(response)
|
73
|
+
return unless confirm_user_authenticated(response)
|
74
|
+
|
75
|
+
Commands::EmptyGroups.new.run(
|
76
|
+
robot,
|
77
|
+
Source.new(room: response.room),
|
78
|
+
gateway(response.user),
|
79
|
+
negative_ack: true
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def list_admins(response)
|
84
|
+
return unless confirm_user_authenticated(response)
|
85
|
+
|
86
|
+
Commands::ListAdmins.new.run(
|
87
|
+
robot,
|
88
|
+
Source.new(room: response.room),
|
89
|
+
gateway(response.user),
|
90
|
+
negative_ack: true
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def no_org_unit(response)
|
95
|
+
return unless confirm_user_authenticated(response)
|
96
|
+
|
97
|
+
Commands::NoOrgUnit.new.run(
|
98
|
+
robot,
|
99
|
+
Source.new(room: response.room),
|
100
|
+
gateway(response.user),
|
101
|
+
negative_ack: true
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def suspension_candidates(response)
|
106
|
+
return unless confirm_user_authenticated(response)
|
107
|
+
|
108
|
+
Commands::SuspensionCandidates.new.run(
|
109
|
+
robot,
|
110
|
+
Source.new(room: response.room),
|
111
|
+
gateway(response.user),
|
112
|
+
negative_ack: true
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def two_factor_off(response)
|
117
|
+
return unless confirm_user_authenticated(response)
|
118
|
+
|
119
|
+
ou_path = response.match_data[1].to_s
|
120
|
+
Commands::TwoFactorOff.new(ou_path).run(
|
121
|
+
robot,
|
122
|
+
Source.new(room: response.room),
|
123
|
+
gateway(response.user),
|
124
|
+
negative_ack: true
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def two_factor_stats(response)
|
129
|
+
return unless confirm_user_authenticated(response)
|
130
|
+
|
131
|
+
Commands::TwoFactorStats.new.run(
|
132
|
+
robot,
|
133
|
+
Source.new(room: response.room),
|
134
|
+
gateway(response.user),
|
135
|
+
negative_ack: true
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def start_auth(response)
|
140
|
+
credentials = google_credentials_for_user(response.user)
|
141
|
+
url = google_authorizer.get_authorization_url(base_url: OOB_OAUTH_URI)
|
142
|
+
if credentials.nil?
|
143
|
+
response.reply "Open the following URL in your browser and enter the resulting code via the 'gsuite set-token <foo>' command:\n\n#{url}"
|
144
|
+
else
|
145
|
+
response.reply "#{response.user.name} is already authorized with Google. To re-authorize, open the following URL in your browser and enter the resulting code via the 'gsuite set-token <foo>' command:\n\n#{url}"
|
146
|
+
end
|
147
|
+
rescue StandardError => e
|
148
|
+
response.reply("Error: #{e.class} #{e.message}")
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_token(response)
|
152
|
+
auth_code = response.match_data[1].to_s
|
153
|
+
|
154
|
+
google_authorizer.get_and_store_credentials_from_code(user_id: response.user.id, code: auth_code, base_url: OOB_OAUTH_URI)
|
155
|
+
response.reply("#{response.user.name} now authorized")
|
156
|
+
rescue StandardError => e
|
157
|
+
response.reply("Error: #{e.class} #{e.message}")
|
158
|
+
end
|
159
|
+
|
160
|
+
def schedule_list(response)
|
161
|
+
room_commands = (weekly_commands_for_room(response.room.name) + window_commands_for_room(response.room.name)).select { |cmd|
|
162
|
+
cmd.room_id == response.room.id
|
163
|
+
}
|
164
|
+
if room_commands.any?
|
165
|
+
room_commands.each do |cmd|
|
166
|
+
response.reply("#{cmd.human}")
|
167
|
+
end
|
168
|
+
else
|
169
|
+
response.reply("no scheduled commands for this room")
|
170
|
+
end
|
171
|
+
rescue StandardError => e
|
172
|
+
response.reply("Error: #{e.class} #{e.message}")
|
173
|
+
end
|
174
|
+
|
175
|
+
def schedule_commands(response)
|
176
|
+
msg = "The following commands are available for scheduling weekly:\n\n"
|
177
|
+
COMMANDS.each do |cmd_name, cmd_klass|
|
178
|
+
msg += "- #{cmd_name}\n"
|
179
|
+
end
|
180
|
+
msg += "The following commands are available for scheduling for sliding windows:\n\n"
|
181
|
+
WINDOW_COMMANDS.each do |cmd_name, cmd_klass|
|
182
|
+
msg += "- #{cmd_name}\n"
|
183
|
+
end
|
184
|
+
response.reply(msg)
|
185
|
+
rescue StandardError => e
|
186
|
+
response.reply("Error: #{e.class} #{e.message}")
|
187
|
+
end
|
188
|
+
|
189
|
+
def schedule_add_weekly(response)
|
190
|
+
return unless confirm_user_authenticated(response)
|
191
|
+
|
192
|
+
_, day, time, cmd_name = *response.match_data
|
193
|
+
|
194
|
+
schedule = WeeklySchedule.new(
|
195
|
+
id: SecureRandom.hex(3),
|
196
|
+
day: day,
|
197
|
+
time: time,
|
198
|
+
cmd: COMMANDS.fetch(cmd_name.downcase, nil),
|
199
|
+
user_id: response.user.id,
|
200
|
+
room_id: response.room.id,
|
201
|
+
)
|
202
|
+
if schedule.valid?
|
203
|
+
redis.hmset("weekly-schedule", schedule.id, schedule.to_json)
|
204
|
+
response.reply("scheduled command")
|
205
|
+
else
|
206
|
+
response.reply("invalid command")
|
207
|
+
end
|
208
|
+
rescue StandardError => e
|
209
|
+
response.reply("Error: #{e.class} #{e.message}")
|
210
|
+
end
|
211
|
+
|
212
|
+
def schedule_add_window(response)
|
213
|
+
return unless confirm_user_authenticated(response)
|
214
|
+
|
215
|
+
cmd_name = response.match_data[1].to_s
|
216
|
+
schedule = WindowSchedule.new(
|
217
|
+
id: SecureRandom.hex(3),
|
218
|
+
cmd: WINDOW_COMMANDS.fetch(cmd_name.downcase, nil),
|
219
|
+
user_id: response.user.id,
|
220
|
+
room_id: response.room.id,
|
221
|
+
)
|
222
|
+
if schedule.valid?
|
223
|
+
redis.hmset("window-schedule", schedule.id, schedule.to_json)
|
224
|
+
response.reply("scheduled command")
|
225
|
+
else
|
226
|
+
response.reply("invalid command")
|
227
|
+
end
|
228
|
+
rescue StandardError => e
|
229
|
+
response.reply("Error: #{e.class} #{e.message}")
|
230
|
+
end
|
231
|
+
|
232
|
+
def schedule_delete(response)
|
233
|
+
cmd_id = response.match_data[1].to_s
|
234
|
+
|
235
|
+
count = redis.hdel("weekly-schedule", cmd_id)
|
236
|
+
count += redis.hdel("window-schedule", cmd_id)
|
237
|
+
if count > 0
|
238
|
+
response.reply("scheduled command #{cmd_id} deleted")
|
239
|
+
else
|
240
|
+
response.reply("no scheduled command with ID #{cmd_id} found")
|
241
|
+
end
|
242
|
+
rescue StandardError => e
|
243
|
+
response.reply("Error: #{e.class} #{e.message}")
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def confirm_user_authenticated(response)
|
249
|
+
credentials = google_credentials_for_user(response.user)
|
250
|
+
if credentials.nil?
|
251
|
+
response.reply("#{response.user.name} not authorized with Google yet. Use the 'gsuite auth' command to initiate authorization")
|
252
|
+
false
|
253
|
+
else
|
254
|
+
true
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def google_credentials_for_user(user)
|
259
|
+
google_authorizer.get_credentials(user.id)
|
260
|
+
end
|
261
|
+
|
262
|
+
def google_authorizer
|
263
|
+
@google_authorizer ||= begin
|
264
|
+
client_id = Google::Auth::ClientId.new(
|
265
|
+
config.oauth_client_id,
|
266
|
+
config.oauth_client_secret
|
267
|
+
)
|
268
|
+
token_store = RedisTokenStore.new(redis)
|
269
|
+
Google::Auth::UserAuthorizer.new(client_id, GsuiteGateway::OAUTH_SCOPES, token_store)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def weekly_commands_timer
|
274
|
+
every_with_logged_errors(TIMER_INTERVAL) do |timer|
|
275
|
+
weekly_commands.each do |cmd|
|
276
|
+
weekly_at(cmd.time, cmd.day, "#{cmd.id}-#{cmd.name}") do
|
277
|
+
target = Source.new(
|
278
|
+
room: Lita::Room.create_or_update(cmd.room_id)
|
279
|
+
)
|
280
|
+
user = Lita::User.find_by_id(cmd.user_id)
|
281
|
+
cmd.run(robot, target, gateway(user))
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def window_commands_timer
|
288
|
+
every_with_logged_errors(TIMER_INTERVAL) do |timer|
|
289
|
+
window_commands.each do |cmd|
|
290
|
+
target = Source.new(
|
291
|
+
room: Lita::Room.create_or_update(cmd.room_id)
|
292
|
+
)
|
293
|
+
user = Lita::User.find_by_id(cmd.user_id)
|
294
|
+
sliding_window ||= Lita::Timing::SlidingWindow.new("#{cmd.id}-#{cmd.name}", redis)
|
295
|
+
sliding_window.advance(duration_minutes: cmd.duration_minutes, buffer_minutes: cmd.buffer_minutes) do |window_start, window_end|
|
296
|
+
cmd.run(robot, target, gateway(user), window_start, window_end)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def weekly_commands_for_room(room_id)
|
303
|
+
weekly_commands.select { |cmd|
|
304
|
+
cmd.room_id == room_id
|
305
|
+
}
|
306
|
+
end
|
307
|
+
|
308
|
+
def window_commands_for_room(room_id)
|
309
|
+
window_commands.select { |cmd|
|
310
|
+
cmd.room_id == room_id
|
311
|
+
}
|
312
|
+
end
|
313
|
+
|
314
|
+
def weekly_commands
|
315
|
+
redis.hgetall("weekly-schedule").map { |_id, data|
|
316
|
+
JSON.parse(data)
|
317
|
+
}.map { |data|
|
318
|
+
WeeklySchedule.new(
|
319
|
+
id: data.fetch("id", "foo"),
|
320
|
+
day: data.fetch("day", nil),
|
321
|
+
time: data.fetch("time", "12:00"),
|
322
|
+
room_id: data.fetch("room_id", nil),
|
323
|
+
user_id: data.fetch("user_id", nil),
|
324
|
+
cmd: COMMANDS.fetch(data.fetch("cmd", nil), nil),
|
325
|
+
)
|
326
|
+
}.select { |schedule|
|
327
|
+
schedule.valid?
|
328
|
+
}
|
329
|
+
end
|
330
|
+
|
331
|
+
def window_commands
|
332
|
+
redis.hgetall("window-schedule").map { |_id, data|
|
333
|
+
JSON.parse(data)
|
334
|
+
}.map { |data|
|
335
|
+
WindowSchedule.new(
|
336
|
+
id: data.fetch("id", nil),
|
337
|
+
room_id: data.fetch("room_id", nil),
|
338
|
+
user_id: data.fetch("user_id", nil),
|
339
|
+
cmd: WINDOW_COMMANDS.fetch(data.fetch("cmd", nil), nil),
|
340
|
+
)
|
341
|
+
}.select { |schedule|
|
342
|
+
schedule.valid?
|
343
|
+
}
|
344
|
+
end
|
345
|
+
|
346
|
+
def every_with_logged_errors(interval, &block)
|
347
|
+
every(interval) do
|
348
|
+
logged_errors do
|
349
|
+
yield
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def logged_errors(&block)
|
355
|
+
yield
|
356
|
+
rescue StandardError => e
|
357
|
+
$stderr.puts "Error in timer loop: #{e.class} #{e.message} #{e.backtrace.first}"
|
358
|
+
end
|
359
|
+
|
360
|
+
def weekly_at(time, day, name, &block)
|
361
|
+
Lita::Timing::Scheduled.new(name, redis).weekly_at(time, day, &block)
|
362
|
+
end
|
363
|
+
|
364
|
+
def gateway(user)
|
365
|
+
Lita::GsuiteGateway.new(
|
366
|
+
user_authorization: google_authorizer.get_credentials(user.id)
|
367
|
+
)
|
368
|
+
end
|
369
|
+
|
370
|
+
Lita.register_handler(self)
|
371
|
+
|
372
|
+
end
|
373
|
+
end
|