waylon-core 0.1.1 → 0.1.4
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/.roxanne.yml +5 -1
- data/.rubocop.yml +8 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +42 -44
- data/README.md +1 -1
- data/lib/waylon/condition.rb +29 -1
- data/lib/waylon/conditions/black_hole.rb +34 -0
- data/lib/waylon/conditions/regex.rb +8 -0
- data/lib/waylon/config.rb +4 -3
- data/lib/waylon/core.rb +6 -0
- data/lib/waylon/group.rb +1 -1
- data/lib/waylon/logger.rb +14 -7
- data/lib/waylon/message.rb +20 -0
- data/lib/waylon/route.rb +1 -1
- data/lib/waylon/routes/black_hole.rb +17 -0
- data/lib/waylon/rspec/matchers/route_matcher.rb +7 -0
- data/lib/waylon/rspec/skill.rb +1 -1
- data/lib/waylon/rspec/test_message.rb +2 -0
- data/lib/waylon/rspec/test_sense.rb +21 -15
- data/lib/waylon/rspec/test_server.rb +4 -1
- data/lib/waylon/rspec/test_user.rb +2 -2
- data/lib/waylon/rspec/test_worker.rb +2 -2
- data/lib/waylon/sense.rb +5 -6
- data/lib/waylon/sense_registry.rb +2 -0
- data/lib/waylon/services/ping.rb +30 -0
- data/lib/waylon/skill.rb +54 -21
- data/lib/waylon/skill_registry.rb +47 -10
- data/lib/waylon/skills/default.rb +5 -0
- data/lib/waylon/skills/diagnostics.rb +60 -0
- data/lib/waylon/skills/fun.rb +5 -2
- data/lib/waylon/skills/groups.rb +173 -0
- data/lib/waylon/skills/help.rb +174 -0
- data/lib/waylon/user.rb +5 -0
- data/lib/waylon/version.rb +1 -1
- data/lib/waylon/webhook.rb +24 -25
- data/lib/waylon/webhook_registry.rb +32 -0
- data/lib/waylon.rb +3 -0
- data/scripts/release.sh +7 -0
- data/scripts/test.sh +1 -1
- data/waylon-core.gemspec +1 -1
- metadata +16 -8
data/lib/waylon/sense.rb
CHANGED
@@ -20,17 +20,16 @@ module Waylon
|
|
20
20
|
|
21
21
|
# The connection between Senses and Skills happens here, via a Route and a Hash of details
|
22
22
|
# @param route [Route] route The matching Route from the SkillRegistry
|
23
|
-
# @param
|
24
|
-
# @param body [String] Message content for the Skill
|
23
|
+
# @param request [String] The request message (or its ID) from the messaging platform
|
25
24
|
# @api private
|
26
|
-
def self.enqueue(route,
|
25
|
+
def self.enqueue(route, request)
|
27
26
|
details = {
|
28
27
|
"sense" => self,
|
29
|
-
"
|
30
|
-
"
|
28
|
+
"request" => request,
|
29
|
+
"route" => route.name
|
31
30
|
}
|
32
31
|
|
33
|
-
Resque.enqueue route.destination,
|
32
|
+
Resque.enqueue route.destination, details
|
34
33
|
end
|
35
34
|
|
36
35
|
# Provides a simple mechanism for referencing the Group subclass provided by this Sense
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module Services
|
5
|
+
# A Ping Service for monitoring
|
6
|
+
class Ping < Sinatra::Base
|
7
|
+
configure do
|
8
|
+
set :protection, except: :http_origin
|
9
|
+
set :logging, ::Waylon::Logger
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
content_type "application/json"
|
14
|
+
halt 403 unless request.get? || request.options?
|
15
|
+
end
|
16
|
+
|
17
|
+
after do
|
18
|
+
headers "Access-Control-Allow-Methods" => %w[OPTIONS GET] if request.options?
|
19
|
+
end
|
20
|
+
|
21
|
+
get "/" do
|
22
|
+
{ status: :ok }.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
options "/" do
|
26
|
+
halt 200
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/waylon/skill.rb
CHANGED
@@ -5,7 +5,7 @@ module Waylon
|
|
5
5
|
class Skill
|
6
6
|
include BaseComponent
|
7
7
|
|
8
|
-
attr_reader :sense, :tokens, :request
|
8
|
+
attr_reader :sense, :tokens, :request, :route
|
9
9
|
|
10
10
|
# Config namespace for config keys
|
11
11
|
# @return [String] The namespace for config keys
|
@@ -14,32 +14,31 @@ module Waylon
|
|
14
14
|
end
|
15
15
|
|
16
16
|
# Resque uses this to execute the Skill. Just defers to the `action` subclass method
|
17
|
-
# @param action [Symbol,String] The method on the Skill subclass to call
|
18
17
|
# @param details [Hash] Input details about the message for running a Skill action
|
19
|
-
def self.perform(
|
20
|
-
new(details["sense"], details["
|
21
|
-
|
18
|
+
def self.perform(details)
|
19
|
+
skill = new(details["sense"], details["route"], details["request"], details["meta"])
|
20
|
+
skill.send(skill.route.action.to_sym)
|
22
21
|
end
|
23
22
|
|
24
23
|
# Redis/Resque queue name
|
25
24
|
# @api private
|
26
25
|
# @return [Symbol]
|
27
26
|
def self.queue
|
28
|
-
:
|
27
|
+
:skills
|
29
28
|
end
|
30
29
|
|
31
30
|
# Adds skills to the SkillRegistry
|
32
31
|
# @param condition [Condition,Regexp] The condition that determines if this route applies
|
33
32
|
# @param action [Symbol,String] The method on the Skill subclass to call
|
34
33
|
# @param allowed_groups [Symbol,Array<Symbol>] The group or list of groups allowed to use this
|
35
|
-
# @param help [String] A description of how to use the skill
|
36
|
-
def self.route(condition, action, allowed_groups: :everyone, help: nil, name: nil)
|
34
|
+
# @param help [String,Hash] A description of how to use the skill
|
35
|
+
def self.route(condition, action, allowed_groups: :everyone, help: nil, name: nil, mention_only: true)
|
37
36
|
name ||= "#{to_s.split("::").last.downcase}##{action}"
|
38
37
|
real_cond = case condition
|
39
38
|
when Condition
|
40
39
|
condition
|
41
40
|
when Regexp
|
42
|
-
Conditions::Regex.new(condition, action, allowed_groups, help)
|
41
|
+
Conditions::Regex.new(condition, action, allowed_groups, help, mention_only)
|
43
42
|
else
|
44
43
|
log("Unknown condition for route for #{name}##{action}", :warn)
|
45
44
|
nil
|
@@ -48,13 +47,13 @@ module Waylon
|
|
48
47
|
end
|
49
48
|
|
50
49
|
# @param sense [Class,String] Class (or Class name) of the source Sense
|
51
|
-
# @param
|
52
|
-
# @param request [String,Integer] Reference to the request from the Sense provider (usually an ID)
|
50
|
+
# @param request [String,Integer] Reference to the request from the Sense provider (usually ID or message itself)
|
53
51
|
# @param meta [Hash] Optional meta data that can be passed along from a Sense for use in Skills
|
54
|
-
def initialize(sense,
|
52
|
+
def initialize(sense, route, request, meta)
|
55
53
|
@sense = sense.is_a?(Class) ? sense : Module.const_get(sense)
|
56
|
-
@
|
54
|
+
@route = SkillRegistry.find_by_name(route)
|
57
55
|
@request = request
|
56
|
+
@tokens = @route.tokens(message.body) || []
|
58
57
|
@meta = meta
|
59
58
|
end
|
60
59
|
|
@@ -62,7 +61,6 @@ module Waylon
|
|
62
61
|
# @return [String]
|
63
62
|
def acknowledgement
|
64
63
|
responses = [
|
65
|
-
"I'll get back to you in just a sec.",
|
66
64
|
"You got it!",
|
67
65
|
"As you wish.",
|
68
66
|
"Certainly!",
|
@@ -73,11 +71,10 @@ module Waylon
|
|
73
71
|
"Of course!",
|
74
72
|
"I'd be delighted!",
|
75
73
|
"Right away!",
|
76
|
-
"Gladly",
|
74
|
+
"Gladly!",
|
77
75
|
"All right.",
|
78
76
|
"I'm all over it.",
|
79
77
|
"I'm on it!",
|
80
|
-
"Let me see what I can do.",
|
81
78
|
"Will do!"
|
82
79
|
]
|
83
80
|
responses.sample
|
@@ -97,7 +94,8 @@ module Waylon
|
|
97
94
|
sense: @sense,
|
98
95
|
message: @request,
|
99
96
|
tokens: @tokens,
|
100
|
-
meta: @meta
|
97
|
+
meta: @meta,
|
98
|
+
route: route.name
|
101
99
|
}
|
102
100
|
end
|
103
101
|
|
@@ -111,7 +109,11 @@ module Waylon
|
|
111
109
|
# Provides a wrapped message for responding to the received Sense
|
112
110
|
# @return [Waylon::Message]
|
113
111
|
def message
|
114
|
-
sense.
|
112
|
+
sense.message_from_request(request)
|
113
|
+
end
|
114
|
+
|
115
|
+
def named_tokens
|
116
|
+
@named_tokens ||= @route.named_tokens(message.body) || {}
|
115
117
|
end
|
116
118
|
|
117
119
|
# Defers to the Sense to react to a message
|
@@ -124,9 +126,40 @@ module Waylon
|
|
124
126
|
end
|
125
127
|
|
126
128
|
# Defers to the Sense to determine how to reply to a message
|
127
|
-
# @param [String]
|
128
|
-
def reply(text)
|
129
|
-
sense.
|
129
|
+
# @param text [String] The reply text
|
130
|
+
def reply(text, private: false)
|
131
|
+
if private && sense.supports?(:private_messages)
|
132
|
+
sense.private_reply(request, text)
|
133
|
+
else
|
134
|
+
log("Unable to send private message for Sense #{sense.name}, replying instead", :debug) if private
|
135
|
+
sense.reply(request, text)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Defers to the Sense to determine how to reply to a message with rich content
|
140
|
+
# @param blocks [String] The reply blocks
|
141
|
+
def reply_with_blocks(blocks, private: false)
|
142
|
+
unless sense.supports?(:blocks)
|
143
|
+
log("Unable to use blocks with Sense #{sense.name}")
|
144
|
+
return false
|
145
|
+
end
|
146
|
+
if private
|
147
|
+
sense.private_reply_with_blocks(request, blocks)
|
148
|
+
else
|
149
|
+
log("Unable to send private message for Sense #{sense.name}, replying instead", :debug) if private
|
150
|
+
sense.reply_with_blocks(request, blocks)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Defers to the Sense to do threaded replies if it can, otherwise it falls back to normal replies
|
155
|
+
# @param text [String] The reply text
|
156
|
+
def threaded_reply(text)
|
157
|
+
if sense.supports?(:threads)
|
158
|
+
sense.threaded_reply(request, text)
|
159
|
+
else
|
160
|
+
log("Unable to reply in theads for Sense #{sense.name}, replying instead", :debug)
|
161
|
+
sense.reply(request, text)
|
162
|
+
end
|
130
163
|
end
|
131
164
|
end
|
132
165
|
end
|
@@ -5,6 +5,8 @@ module Waylon
|
|
5
5
|
class SkillRegistry
|
6
6
|
include Singleton
|
7
7
|
|
8
|
+
attr_reader :routes
|
9
|
+
|
8
10
|
# A wrapper around the singleton #register method
|
9
11
|
# @param name [String] The name of the skill in the registry
|
10
12
|
# @param class_name [Class] The class to associate with the name
|
@@ -14,8 +16,24 @@ module Waylon
|
|
14
16
|
instance.register(name, class_name, condition)
|
15
17
|
end
|
16
18
|
|
17
|
-
def
|
18
|
-
|
19
|
+
def self.find_by_name(name)
|
20
|
+
[
|
21
|
+
*instance.routes,
|
22
|
+
Routes::PermissionDenied.new,
|
23
|
+
Routes::BlackHole.new,
|
24
|
+
Routes::Default.new
|
25
|
+
].find { |r| r.name == name.to_s }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.route(message)
|
29
|
+
instance.route(message)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Provides the default route based on the received message.
|
33
|
+
# @param message [Waylon::Message] The received message
|
34
|
+
# @return [Waylon::Route]
|
35
|
+
def default_route(message)
|
36
|
+
message.to_bot? ? Routes::Default.new : Routes::BlackHole.new
|
19
37
|
end
|
20
38
|
|
21
39
|
# Gathers a Hash of help data for all routes a user is permitted to access
|
@@ -23,9 +41,13 @@ module Waylon
|
|
23
41
|
# @return [Hash]
|
24
42
|
def help(user)
|
25
43
|
data = {}
|
26
|
-
@routes.select { |r| r.permits?(user) }.each do |permitted|
|
27
|
-
data[permitted.destination.
|
28
|
-
data[permitted.destination.
|
44
|
+
@routes.select { |r| r.permits?(user) && r.mention_only? }.each do |permitted|
|
45
|
+
data[permitted.destination.component_namespace] ||= []
|
46
|
+
data[permitted.destination.component_namespace] << if permitted.help
|
47
|
+
{ name: permitted.name, help: permitted.help }
|
48
|
+
else
|
49
|
+
{ name: permitted.name }
|
50
|
+
end
|
29
51
|
end
|
30
52
|
|
31
53
|
data.reject { |_k, v| v.empty? }
|
@@ -53,14 +75,25 @@ module Waylon
|
|
53
75
|
# Given a message, find a suitable skill Route for it (sorted by priority, highest first)
|
54
76
|
# @param message [Waylon::Message] A Message instance
|
55
77
|
# @return [Hash]
|
78
|
+
# rubocop:disable Metrics/AbcSize
|
79
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
80
|
+
# rubocop:disable Metrics/MethodLength
|
81
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
56
82
|
def route(message)
|
57
83
|
route = nil
|
58
|
-
message_text = message.
|
84
|
+
message_text = message.body.strip
|
59
85
|
@routes ||= []
|
60
|
-
@routes.sort_by(&:priority).reverse.each do |
|
61
|
-
if
|
62
|
-
|
63
|
-
|
86
|
+
@routes.sort_by(&:priority).reverse.each do |this_route|
|
87
|
+
if this_route.permits?(message.author) &&
|
88
|
+
this_route.matches?(message_text) &&
|
89
|
+
(this_route.properly_mentions?(message) || message.private?)
|
90
|
+
route = this_route
|
91
|
+
elsif this_route.permits?(message.author) &&
|
92
|
+
this_route.matches?(message_text) &&
|
93
|
+
!this_route.properly_mentions?(message)
|
94
|
+
# Black hole these because they're not direct mentions
|
95
|
+
route = Routes::BlackHole.new
|
96
|
+
elsif this_route.matches?(message_text)
|
64
97
|
route = Routes::PermissionDenied.new
|
65
98
|
end
|
66
99
|
if route
|
@@ -70,5 +103,9 @@ module Waylon
|
|
70
103
|
end
|
71
104
|
route
|
72
105
|
end
|
106
|
+
# rubocop:enable Metrics/AbcSize
|
107
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
108
|
+
# rubocop:enable Metrics/MethodLength
|
109
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
73
110
|
end
|
74
111
|
end
|
@@ -20,6 +20,11 @@ module Waylon
|
|
20
20
|
reply("#{prefix} #{responses.sample} #{help_postfix}")
|
21
21
|
end
|
22
22
|
|
23
|
+
# This action ignores messages
|
24
|
+
def ignore
|
25
|
+
log("Ignoring black-holed message from #{message.author.email}")
|
26
|
+
end
|
27
|
+
|
23
28
|
# A useful addition to message to tell the User how to get help
|
24
29
|
# @return [String]
|
25
30
|
def help_postfix
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module Skills
|
5
|
+
# Built-in info routes
|
6
|
+
class Diagnostics < Skill
|
7
|
+
# Say hello to Waylon
|
8
|
+
route(
|
9
|
+
/^diagnostics|status$/i,
|
10
|
+
:status,
|
11
|
+
help: {
|
12
|
+
usage: "diagnostics|status",
|
13
|
+
description: "Retrieve this bot's current status"
|
14
|
+
}
|
15
|
+
)
|
16
|
+
|
17
|
+
# Provides info about Waylon's status
|
18
|
+
def status # rubocop:disable Metrics/AbcSize
|
19
|
+
response = []
|
20
|
+
response << "*Framework Version:* Waylon v#{Waylon::Core::VERSION}"
|
21
|
+
response << "*Sense plugins:*"
|
22
|
+
loaded_senses.each { |c| response << " - #{c}" }
|
23
|
+
response << "*Skill plugins:*"
|
24
|
+
loaded_routes.each { |d| response << " - #{d}" }
|
25
|
+
response << "*Redis:*"
|
26
|
+
state, read_time, write_time = test_redis
|
27
|
+
response << " - *Test Result:* #{state ? "Success" : "Error"}"
|
28
|
+
response << " - *Read time:* #{read_time}s"
|
29
|
+
response << " - *Write time:* #{write_time}s"
|
30
|
+
if Resque.redis.connected?
|
31
|
+
response << "*Queue Monitoring:*"
|
32
|
+
response << " - Failed jobs: #{Resque::Failure.count}"
|
33
|
+
end
|
34
|
+
|
35
|
+
reply response.join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
def loaded_routes
|
39
|
+
SkillRegistry.instance.routes.map { |r| r.destination.name }.sort.uniq
|
40
|
+
end
|
41
|
+
|
42
|
+
def loaded_senses
|
43
|
+
SenseRegistry.instance.senses.map { |_s, c| c.name }.sort.uniq
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_redis
|
47
|
+
test_key = ("a".."z").to_a.sample(10).join
|
48
|
+
test_value = (0..1000).to_a.sample(20).map(&:to_s).join
|
49
|
+
test_result = nil
|
50
|
+
|
51
|
+
write_time = Benchmark.realtime { db.store(test_key, test_value) }
|
52
|
+
read_time = Benchmark.realtime { test_result = db.load(test_key) }
|
53
|
+
|
54
|
+
db.delete(test_key)
|
55
|
+
|
56
|
+
[(test_value == test_result), read_time.round(6), write_time.round(6)]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/waylon/skills/fun.rb
CHANGED
@@ -6,8 +6,9 @@ module Waylon
|
|
6
6
|
class Fun < Skill
|
7
7
|
# Say hello to Waylon
|
8
8
|
route(
|
9
|
-
/^(hello|hi)
|
10
|
-
:hello
|
9
|
+
/^(hello|hi)([.!]+)?$/i,
|
10
|
+
:hello,
|
11
|
+
help: "hi|hello"
|
11
12
|
)
|
12
13
|
|
13
14
|
# Responds to "hello" in less boring ways
|
@@ -19,6 +20,8 @@ module Waylon
|
|
19
20
|
"How can I be of service?"
|
20
21
|
]
|
21
22
|
|
23
|
+
react :wave
|
24
|
+
|
22
25
|
reply responses.sample
|
23
26
|
end
|
24
27
|
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module Skills
|
5
|
+
# Built-in skills for managing groups
|
6
|
+
class Groups < Skill
|
7
|
+
route(
|
8
|
+
/^add (.+) to (.+)$/,
|
9
|
+
:add_to_group,
|
10
|
+
help: {
|
11
|
+
usage: "add USER[,USER] to GROUP",
|
12
|
+
description: "Add USER(s) to a GROUP"
|
13
|
+
},
|
14
|
+
allowed_groups: %i[admins group_admins]
|
15
|
+
)
|
16
|
+
|
17
|
+
route(
|
18
|
+
/^remove (.+) from (.+)$/,
|
19
|
+
:remove_from_group,
|
20
|
+
help: {
|
21
|
+
usage: "remove USER[,USER] from GROUP",
|
22
|
+
description: "Remove USER(s) from a GROUP"
|
23
|
+
},
|
24
|
+
allowed_groups: %i[admins group_admins]
|
25
|
+
)
|
26
|
+
|
27
|
+
route(
|
28
|
+
/^(describe|list|print|show) (all )?(groups|group memberships)$/,
|
29
|
+
:list_all_groups,
|
30
|
+
help: {
|
31
|
+
usage: "list all groups",
|
32
|
+
description: "List all groups and their members"
|
33
|
+
},
|
34
|
+
allowed_groups: %i[admins group_admins]
|
35
|
+
)
|
36
|
+
|
37
|
+
route(
|
38
|
+
/^cleanup groups$/,
|
39
|
+
:cleanup_groups,
|
40
|
+
help: {
|
41
|
+
usage: "cleanup groups",
|
42
|
+
description: "Remove empty groups"
|
43
|
+
},
|
44
|
+
allowed_groups: %i[admins group_admins]
|
45
|
+
)
|
46
|
+
|
47
|
+
route(
|
48
|
+
/^((list )?my )?groups$/,
|
49
|
+
:list_my_groups,
|
50
|
+
help: {
|
51
|
+
usage: "list my groups",
|
52
|
+
description: "List my group memberships"
|
53
|
+
}
|
54
|
+
)
|
55
|
+
|
56
|
+
def add_to_group # rubocop:disable Metrics/AbcSize
|
57
|
+
user_list = tokens.first
|
58
|
+
group_name = tokens.last
|
59
|
+
|
60
|
+
if group_name == "global admins"
|
61
|
+
reply "Sorry, I can't manipulate global admins this way..."
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
group = Group.new(group_name)
|
66
|
+
|
67
|
+
log "Adding #{user_list} to group #{group}"
|
68
|
+
|
69
|
+
failures = []
|
70
|
+
user_list.split(",").each do |this_user|
|
71
|
+
found = found_user(this_user)
|
72
|
+
failures << found unless group.add(found)
|
73
|
+
end
|
74
|
+
|
75
|
+
unless failures.empty?
|
76
|
+
text = failures.size > 1 ? "were already members" : "was already a member"
|
77
|
+
reply "Looks like [#{failures.map { |u| mention(u) }.join(", ")}] #{text} of #{group_name}"
|
78
|
+
end
|
79
|
+
|
80
|
+
reply("Done adding users to #{group_name}!")
|
81
|
+
end
|
82
|
+
|
83
|
+
def cleanup_groups
|
84
|
+
# perform a key scan in Redis for all group keys and find empty groups
|
85
|
+
group_keys = all_group_keys.select do |group|
|
86
|
+
name = group.split(".").last
|
87
|
+
Group.new(name).to_a.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
# delete the empty group keys
|
91
|
+
group_keys.each { |g| db.delete(g) }
|
92
|
+
|
93
|
+
group_names = group_keys.map { |g| g.split(".").last }
|
94
|
+
|
95
|
+
reply "I removed these empty groups: #{group_names.join(", ")}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_from_group # rubocop:disable Metrics/AbcSize
|
99
|
+
user_list = tokens.first
|
100
|
+
group_name = tokens.last
|
101
|
+
|
102
|
+
if group_name == "global admins"
|
103
|
+
reply "Sorry, I can't manipulate global admins this way..."
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
group = Group.new(group_name)
|
108
|
+
|
109
|
+
log "Removing #{user_list} from group '#{group_name}'", :debug
|
110
|
+
|
111
|
+
failures = []
|
112
|
+
user_list.split(",").each do |this_user|
|
113
|
+
found = found_user(this_user)
|
114
|
+
failures << found unless group.remove(found)
|
115
|
+
end
|
116
|
+
|
117
|
+
unless failures.empty?
|
118
|
+
text = failures.size > 1 ? "were members" : "was a member"
|
119
|
+
reply("I don't think [#{failures.map { |u| mention(u) }.join(", ")}] #{text} of #{group_name}")
|
120
|
+
end
|
121
|
+
reply("Done removing users from groups!")
|
122
|
+
end
|
123
|
+
|
124
|
+
def list_all_groups
|
125
|
+
groups = {}
|
126
|
+
groups["global admins"] = global_admins unless global_admins.empty?
|
127
|
+
all_group_keys.each do |group|
|
128
|
+
name = group.split(".").last
|
129
|
+
groups[name] = Group.new(name).members
|
130
|
+
end
|
131
|
+
|
132
|
+
reply(codify(groups.to_yaml))
|
133
|
+
end
|
134
|
+
|
135
|
+
def list_my_groups
|
136
|
+
groups = []
|
137
|
+
groups << "global admins" if global_admins.include?(message.author.email)
|
138
|
+
all_group_keys.each do |group|
|
139
|
+
name = group.split(".").last
|
140
|
+
groups << name if Group.new(name).include?(message.author)
|
141
|
+
end
|
142
|
+
|
143
|
+
reply(codify(groups.to_yaml))
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def all_group_keys
|
149
|
+
if db.adapter.instance_of?(Moneta::Adapters::Redis)
|
150
|
+
db.adapter.backend.keys("groups.*")
|
151
|
+
else
|
152
|
+
groups = []
|
153
|
+
db.each_key { |key| groups << key if key.start_with?("groups.") }
|
154
|
+
groups
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def found_user(user_string)
|
159
|
+
if user_string =~ /^.+@.+/
|
160
|
+
# If provided an email
|
161
|
+
sense.user_class.find_by_email(user_string)
|
162
|
+
else
|
163
|
+
# Otherwise assume we're provided their user ID
|
164
|
+
sense.user_class.from_mention(user_string)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def global_admins
|
169
|
+
Config.instance.admins
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|