waylon-core 0.1.1 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|