waylon-core 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.roxanne.yml +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +137 -0
- data/LICENSE.txt +21 -0
- data/README.md +57 -0
- data/Rakefile +23 -0
- data/bin/console +17 -0
- data/bin/setup +8 -0
- data/lib/waylon/base_component.rb +124 -0
- data/lib/waylon/condition.rb +64 -0
- data/lib/waylon/conditions/default.rb +34 -0
- data/lib/waylon/conditions/permission_denied.rb +34 -0
- data/lib/waylon/conditions/regex.rb +22 -0
- data/lib/waylon/config.rb +134 -0
- data/lib/waylon/core.rb +46 -0
- data/lib/waylon/exceptions/not_implemented_error.rb +9 -0
- data/lib/waylon/exceptions/validation_error.rb +9 -0
- data/lib/waylon/generic_exception.rb +7 -0
- data/lib/waylon/group.rb +70 -0
- data/lib/waylon/logger.rb +41 -0
- data/lib/waylon/message.rb +17 -0
- data/lib/waylon/route.rb +71 -0
- data/lib/waylon/routes/default.rb +17 -0
- data/lib/waylon/routes/permission_denied.rb +17 -0
- data/lib/waylon/rspec/matchers/route_matcher.rb +43 -0
- data/lib/waylon/rspec/skill.rb +71 -0
- data/lib/waylon/rspec/test_channel.rb +83 -0
- data/lib/waylon/rspec/test_message.rb +63 -0
- data/lib/waylon/rspec/test_sense.rb +110 -0
- data/lib/waylon/rspec/test_server.rb +120 -0
- data/lib/waylon/rspec/test_user.rb +165 -0
- data/lib/waylon/rspec/test_worker.rb +15 -0
- data/lib/waylon/rspec.rb +50 -0
- data/lib/waylon/sense.rb +74 -0
- data/lib/waylon/sense_registry.rb +30 -0
- data/lib/waylon/skill.rb +132 -0
- data/lib/waylon/skill_registry.rb +74 -0
- data/lib/waylon/skills/default.rb +48 -0
- data/lib/waylon/skills/fun.rb +26 -0
- data/lib/waylon/user.rb +61 -0
- data/lib/waylon/version.rb +12 -0
- data/lib/waylon/webhook.rb +73 -0
- data/lib/waylon.rb +5 -0
- data/scripts/test.sh +5 -0
- data/waylon-core.gemspec +50 -0
- metadata +312 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
# The global configuration
|
5
|
+
class Config
|
6
|
+
include Singleton
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_accessor :schema
|
10
|
+
|
11
|
+
delegate delete: :@config
|
12
|
+
|
13
|
+
# Stores schema metadata about config items
|
14
|
+
# @api private
|
15
|
+
# @param key [String] The config key to define a schema for
|
16
|
+
# @param required [Boolean] Is this key required on startup?
|
17
|
+
# @param type [Class] The class type for the stored value
|
18
|
+
# @param default [Object] The optional default value
|
19
|
+
# @return [Boolean] Was the schema update successful?
|
20
|
+
def add_schema(key, default: nil, required: false, type: String)
|
21
|
+
@schema ||= {}
|
22
|
+
@schema[key] = { default: default, required: required, type: type }
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
# A list of emails specified via the CONF_GLOBAL_ADMINS environment variable
|
27
|
+
# @return [Array<String>] a list of emails
|
28
|
+
def admins
|
29
|
+
admin_emails = self["global.admins"]
|
30
|
+
admin_emails ? admin_emails.split(",") : []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Load in the config from env variables
|
34
|
+
# @return [Boolean] Was the configuration loaded?
|
35
|
+
def load_env
|
36
|
+
@schema ||= {}
|
37
|
+
self["global.log.level"] = ENV.fetch("LOG_LEVEL", "info")
|
38
|
+
self["global.redis.host"] = ENV.fetch("REDIS_HOST", "redis")
|
39
|
+
self["global.redis.port"] = ENV.fetch("REDIS_PORT", "6379")
|
40
|
+
ENV.keys.grep(/CONF_/).each do |env_key|
|
41
|
+
conf_key = env_key.downcase.split("_")[1..].join(".")
|
42
|
+
::Waylon::Logger.log("Attempting to set #{conf_key} from #{env_key}", :debug)
|
43
|
+
self[conf_key] = ENV[env_key]
|
44
|
+
end
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
# Provides the redis host used for most of Waylon's brain
|
49
|
+
# @return [String] The redis host
|
50
|
+
def redis_host
|
51
|
+
self["global.redis.host"]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Provides the redis port used for most of Waylon's brain
|
55
|
+
# @return [String] The redis host
|
56
|
+
def redis_port
|
57
|
+
self["global.redis.port"]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Clear the configuration
|
61
|
+
# @return [Boolean] Was the configuration reset?
|
62
|
+
def reset
|
63
|
+
@config = {}
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if a given key is explicitly set (not including defaults)
|
68
|
+
# @param key [String] The key to check
|
69
|
+
# @return [Boolean] Is the key set?
|
70
|
+
def key?(key)
|
71
|
+
@config ||= {}
|
72
|
+
@config.key?(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
alias set? key?
|
76
|
+
|
77
|
+
# Check if a given key is has _any_ value (default or otherwise)
|
78
|
+
# @param key [String] The key to look for
|
79
|
+
# @return [Boolean] Does the key have a value?
|
80
|
+
def value?(key)
|
81
|
+
@config ||= {}
|
82
|
+
!self[key].nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Set the value for a key
|
86
|
+
# @param key [String] The key to use for storing the value
|
87
|
+
# @param [Object] value The value for the key
|
88
|
+
def []=(key, value)
|
89
|
+
if (@schema[key] && validate_config(@schema[key], value)) || key.start_with?("global.")
|
90
|
+
@config ||= {}
|
91
|
+
@config[key] = value
|
92
|
+
elsif @schema[key]
|
93
|
+
::Waylon::Logger.log("Ignoring invalid config value for key: #{key}", :warn)
|
94
|
+
else
|
95
|
+
::Waylon::Logger.log("Ignoring unknown config key: #{key}", :warn)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Retrieve the value for the given key
|
100
|
+
# @param key [String] The key to lookup
|
101
|
+
# @return [String,nil] The requested value
|
102
|
+
def [](key)
|
103
|
+
@config ||= {}
|
104
|
+
if @config.key?(key)
|
105
|
+
@config[key].dup
|
106
|
+
elsif @schema.key?(key) && @schema[key][:default]
|
107
|
+
@schema[key][:default].dup
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Is the state of the config valid?
|
112
|
+
# @return [Boolean]
|
113
|
+
def valid?
|
114
|
+
missing = @schema.select { |_k, v| v[:required] }.reject { |k, _v| @config[k] }
|
115
|
+
missing.each do |key, _value|
|
116
|
+
::Waylon::Logger.log("Missing config: #{key}", :debug)
|
117
|
+
end
|
118
|
+
missing.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
# Checks if a value aligns with a schema
|
122
|
+
# @param schema [Hash] The schema definition for this value
|
123
|
+
# @param value The value to compare to the schema
|
124
|
+
# @return [Boolean]
|
125
|
+
def validate_config(schema, value)
|
126
|
+
type = schema[:type]
|
127
|
+
if type == :boolean
|
128
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
129
|
+
else
|
130
|
+
value.is_a?(type)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/waylon/core.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Standard Library dependencies
|
4
|
+
require "base64"
|
5
|
+
require "digest"
|
6
|
+
require "English"
|
7
|
+
require "fileutils"
|
8
|
+
require "forwardable"
|
9
|
+
require "logger"
|
10
|
+
require "yaml"
|
11
|
+
require "singleton"
|
12
|
+
require "ostruct"
|
13
|
+
require "timeout"
|
14
|
+
require "resolv"
|
15
|
+
require "set"
|
16
|
+
|
17
|
+
# External dependencies
|
18
|
+
require "addressable/uri"
|
19
|
+
require "faraday"
|
20
|
+
require "i18n"
|
21
|
+
require "json"
|
22
|
+
require "moneta"
|
23
|
+
require "resque"
|
24
|
+
|
25
|
+
# Internal requirements
|
26
|
+
require "waylon/version"
|
27
|
+
require "waylon/config"
|
28
|
+
require "waylon/generic_exception"
|
29
|
+
require "waylon/exceptions/not_implemented_error"
|
30
|
+
require "waylon/exceptions/validation_error"
|
31
|
+
require "waylon/group"
|
32
|
+
require "waylon/logger"
|
33
|
+
require "waylon/base_component"
|
34
|
+
require "waylon/condition"
|
35
|
+
require "waylon/message"
|
36
|
+
require "waylon/route"
|
37
|
+
require "waylon/sense_registry"
|
38
|
+
require "waylon/sense"
|
39
|
+
require "waylon/skill_registry"
|
40
|
+
require "waylon/skill"
|
41
|
+
require "waylon/user"
|
42
|
+
require "waylon/conditions/default"
|
43
|
+
require "waylon/conditions/permission_denied"
|
44
|
+
require "waylon/conditions/regex"
|
45
|
+
require "waylon/routes/default"
|
46
|
+
require "waylon/routes/permission_denied"
|
data/lib/waylon/group.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
# The basic, built-in Group class. Relies on Redis for storage and is managed directly by Waylon.
|
5
|
+
# @note This class can be subclassed for external authentication mechanisms per Sense
|
6
|
+
class Group
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# @param name [String,Symbol] The name of the group
|
10
|
+
def initialize(name)
|
11
|
+
@name = name.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add a user to the group
|
15
|
+
# @param user [User] User to add
|
16
|
+
# @return [Boolean]
|
17
|
+
def add(user)
|
18
|
+
return false if include?(user)
|
19
|
+
|
20
|
+
users = members
|
21
|
+
users << user.email.downcase
|
22
|
+
storage.store(key, users)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
# Remove a user from the group
|
27
|
+
# @param user [User] User to remove
|
28
|
+
# @return [Boolean]
|
29
|
+
def remove(user)
|
30
|
+
return false unless include?(user)
|
31
|
+
|
32
|
+
users = members
|
33
|
+
users.delete(user)
|
34
|
+
storage.store(key, users)
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Waylon Users in this group
|
39
|
+
# @return [Array<User>] The members of this Group
|
40
|
+
def members
|
41
|
+
# all actions on Group funnel through here, so always make sure the key exists first
|
42
|
+
storage.store(key, []) unless storage.key?(key)
|
43
|
+
|
44
|
+
storage.load(key).sort.uniq
|
45
|
+
end
|
46
|
+
|
47
|
+
alias to_a members
|
48
|
+
|
49
|
+
# Checks if a user a member
|
50
|
+
# @param user [User] User to look for
|
51
|
+
# @return [Boolean]
|
52
|
+
def include?(user)
|
53
|
+
members.include?(user.email.downcase)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Provides access to the top-level storage singleton
|
59
|
+
# @return [Waylon::Storage]
|
60
|
+
def storage
|
61
|
+
Waylon::Storage
|
62
|
+
end
|
63
|
+
|
64
|
+
# A quick way to find the config/storage key for this Group
|
65
|
+
# @return [String]
|
66
|
+
def key
|
67
|
+
"groups.#{name}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
# A simple way to abstract logging
|
5
|
+
module Logger
|
6
|
+
# The log level as defined in the global Config singleton
|
7
|
+
# @return [String] The current log level
|
8
|
+
def self.level
|
9
|
+
Config.instance["global.log.level"]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Abstraction for sending logs to the logger at some level
|
13
|
+
# @param [Symbol] level The log level this message corresponds to
|
14
|
+
# @param [String] message The message to log at this specified level
|
15
|
+
def self.log(message, level = :info)
|
16
|
+
logger.send(level, message)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Provides an easy way to access the underlying logger
|
20
|
+
# @return [Logger] The Logger instance
|
21
|
+
def self.logger
|
22
|
+
return @logger if @logger
|
23
|
+
|
24
|
+
@logger = ::Logger.new($stderr)
|
25
|
+
@logger.level = level
|
26
|
+
@logger.progname = "Waylon"
|
27
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
28
|
+
json_data = JSON.dump(
|
29
|
+
ts: datetime,
|
30
|
+
severity: severity.ljust(5).to_s,
|
31
|
+
progname: progname,
|
32
|
+
pid: Process.pid,
|
33
|
+
message: msg,
|
34
|
+
v: Waylon::Core::VERSION
|
35
|
+
)
|
36
|
+
"#{json_data}\n"
|
37
|
+
end
|
38
|
+
@logger
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
# Abstract Message module
|
5
|
+
# @abstract
|
6
|
+
module Message
|
7
|
+
# Message author (meant to be overwritten by mixing classes)
|
8
|
+
def author
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
# Message channel (meant to be overwritten by mixing classes)
|
13
|
+
def channel
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/waylon/route.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
# A Route is a way of connecting a Sense to the right Skill, allowing for things like
|
5
|
+
# permissions, prioritization, and scopes.
|
6
|
+
class Route
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :name, :destination, :condition, :priority
|
10
|
+
|
11
|
+
# @param name [String] The name of this route (for use in logging and exceptions)
|
12
|
+
# @param destination [Class] The Skill subclass to send matching requests to
|
13
|
+
# @param condition [Condition] The Condition used to see if this Route matches a request
|
14
|
+
# @param priority [Integer] The priority value (for resolving conflicts). Highest value wins.
|
15
|
+
def initialize(name:, destination:, condition:, priority: 10)
|
16
|
+
validate_name(name)
|
17
|
+
validate_destination(destination)
|
18
|
+
validate_condition(condition)
|
19
|
+
validate_priority(priority)
|
20
|
+
@name = name
|
21
|
+
@destination = destination
|
22
|
+
@condition = condition
|
23
|
+
@priority = priority
|
24
|
+
end
|
25
|
+
|
26
|
+
delegate %i[action help matches? permits? tokens] => :@condition
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Validates the Condition
|
31
|
+
# @param condition [Condition]
|
32
|
+
# @raise [Exceptions::ValidationError]
|
33
|
+
# @return [Boolean]
|
34
|
+
def validate_condition(condition)
|
35
|
+
raise Exceptions::ValidationError, "Route condition must be a Condition" unless condition.is_a?(Condition)
|
36
|
+
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Validates the destination
|
41
|
+
# @param destination [Class]
|
42
|
+
# @raise [Exceptions::ValidationError]
|
43
|
+
# @return [Boolean]
|
44
|
+
def validate_destination(destination)
|
45
|
+
unless destination.ancestors.include?(Skill)
|
46
|
+
raise Exceptions::ValidationError, "Route destination must be a Skill"
|
47
|
+
end
|
48
|
+
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
# Validates the Route name
|
53
|
+
# @param name [String]
|
54
|
+
# @raise [Exceptions::ValidationError]
|
55
|
+
# @return [Boolean]
|
56
|
+
def validate_name(name)
|
57
|
+
raise Exceptions::ValidationError, "Route name must be a String" unless name.is_a?(String)
|
58
|
+
raise Exceptions::ValidationError, "Route name must not be empty" if name.empty?
|
59
|
+
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Validates the priority
|
64
|
+
# @param priority [Integer]
|
65
|
+
# @raise [Exceptions::ValidationError]
|
66
|
+
# @return [Boolean]
|
67
|
+
def validate_priority(priority)
|
68
|
+
raise Exceptions::ValidationError, "Route priority must be between 0 and 99" unless (0..99).include?(priority)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module Routes
|
5
|
+
# The default route for unrouted messages
|
6
|
+
class Default < Route
|
7
|
+
def initialize(
|
8
|
+
name: "default_route",
|
9
|
+
destination: Skills::Default,
|
10
|
+
condition: Conditions::Default.new,
|
11
|
+
priority: 0
|
12
|
+
)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module Routes
|
5
|
+
# This route is used when a route exists but the current user doesn't have permission
|
6
|
+
class PermissionDenied < Route
|
7
|
+
def initialize(
|
8
|
+
name: "permission_denied",
|
9
|
+
destination: Skills::Default,
|
10
|
+
condition: Conditions::PermissionDenied.new,
|
11
|
+
priority: 99
|
12
|
+
)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module RSpec
|
5
|
+
module Matchers
|
6
|
+
# RSpec matchers for Routes defined in Skills
|
7
|
+
module RouteMatcher
|
8
|
+
extend ::RSpec::Matchers::DSL
|
9
|
+
|
10
|
+
# Validates that the provided message is routable
|
11
|
+
matcher :route do |body|
|
12
|
+
match do
|
13
|
+
message = chatroom.post_message(body, from: testuser)
|
14
|
+
|
15
|
+
if defined?(@group)
|
16
|
+
# Add the test user to the group
|
17
|
+
Group.new(@group.to_s).add(testuser)
|
18
|
+
end
|
19
|
+
|
20
|
+
found_route = SkillRegistry.instance.route(message)
|
21
|
+
|
22
|
+
if defined?(@method_name)
|
23
|
+
# Verify that the route sends to the appropriate place
|
24
|
+
found_route &&
|
25
|
+
found_route.destination == described_class &&
|
26
|
+
found_route.action == @method_name.to_sym
|
27
|
+
else
|
28
|
+
found_route && found_route.destination == described_class
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
chain :as_member_of do |group|
|
33
|
+
@group = group
|
34
|
+
end
|
35
|
+
|
36
|
+
chain :to do |method_name|
|
37
|
+
@method_name = method_name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "waylon/rspec/matchers/route_matcher"
|
4
|
+
|
5
|
+
module Waylon
|
6
|
+
module RSpec
|
7
|
+
# Extras for RSpec to facilitate testing Waylon Skills
|
8
|
+
module Skill
|
9
|
+
include Matchers::RouteMatcher
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Sets up the RSpec environment
|
13
|
+
def included(base)
|
14
|
+
base.send(:include, Waylon::RSpec)
|
15
|
+
|
16
|
+
init_let_blocks(base)
|
17
|
+
init_subject(base)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Create common test objects.
|
23
|
+
def init_let_blocks(base)
|
24
|
+
base.class_eval do
|
25
|
+
let(:bot) { TestUser.new(0) }
|
26
|
+
let(:testuser) { TestUser.new(1) }
|
27
|
+
let(:chatroom) { TestChannel.new(0) }
|
28
|
+
let(:adminuser) do
|
29
|
+
@adminuser ||= TestUser.find_or_create(name: "Charles Montgomery Burns", handle: "monty")
|
30
|
+
Group.new("admins").add(@adminuser)
|
31
|
+
@adminuser
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set up a working test subject.
|
37
|
+
def init_subject(base)
|
38
|
+
base.class_eval do
|
39
|
+
subject { described_class }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# An array of strings that have been sent by the bot during throughout a test
|
45
|
+
# @return [Array<String>] The replies.
|
46
|
+
def replies
|
47
|
+
TestSense.sent_messages
|
48
|
+
end
|
49
|
+
|
50
|
+
# Sends a message to the bot
|
51
|
+
# @param body [String] The message to send
|
52
|
+
# @param from [TestUser] The user sending the message
|
53
|
+
# @param channel [TestChannel] Where the message is received
|
54
|
+
# @param privately [Boolean] Is the message a DM
|
55
|
+
# @return [void]
|
56
|
+
def send_message(body, from: testuser, channel: nil, privately: false)
|
57
|
+
msg_details = { person_id: from.id, text: body, created_at: Time.now }
|
58
|
+
if privately
|
59
|
+
msg_details[:type] = :private
|
60
|
+
msg_details[:receiver_id] = robot.id
|
61
|
+
else
|
62
|
+
msg_details[:type] = :channel
|
63
|
+
msg_details[:channel_id] = channel ? channel.id : chatroom.id
|
64
|
+
end
|
65
|
+
|
66
|
+
TestSense.process(msg_details)
|
67
|
+
TestWorker.handle(TestSense.fake_queue)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Waylon
|
4
|
+
module RSpec
|
5
|
+
# The TestChannel
|
6
|
+
class TestChannel
|
7
|
+
attr_reader :id
|
8
|
+
|
9
|
+
# Simple way to list all TestChannels
|
10
|
+
# @return [Array<TestChannel>]
|
11
|
+
def self.all
|
12
|
+
TestSense.channel_list.each_index.map { |id| new(id) }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Always provides a TestChannel, either by finding an existing or creating a new one
|
16
|
+
# @return [TestChannel]
|
17
|
+
def self.find_or_create(name)
|
18
|
+
existing_channel = find_by_name(name)
|
19
|
+
if existing_channel
|
20
|
+
existing_channel
|
21
|
+
else
|
22
|
+
channel_details = { name: name, created_at: Time.now }
|
23
|
+
TestSense.channel_list << channel_details
|
24
|
+
new(TestSense.channel_list.size - 1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Looks up an existing TestChannel by name
|
29
|
+
# @return [TestChannel,nil]
|
30
|
+
def self.find_by_name(name)
|
31
|
+
channel_id = TestSense.channel_list.index { |channel| channel[:name] == name }
|
32
|
+
channel_id ? new(channel_id) : nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param channel_id [Integer] The Channel ID for the new TestChannel
|
36
|
+
# @param details [Hash] Details (namely 'name' and 'created_at') for the new TestChannel
|
37
|
+
def initialize(channel_id, details = {})
|
38
|
+
@id = channel_id.to_i
|
39
|
+
@details = details
|
40
|
+
end
|
41
|
+
|
42
|
+
# Easy access to when the TestChannel was created
|
43
|
+
# @return [Time]
|
44
|
+
def created_at
|
45
|
+
details[:created_at]
|
46
|
+
end
|
47
|
+
|
48
|
+
# The name of the TestChannel
|
49
|
+
# @return [String]
|
50
|
+
def name
|
51
|
+
details[:name]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Send a TestMessage to a TestChannel
|
55
|
+
# @param content [String,Message] The Message to send
|
56
|
+
# @return [Message] The sent Message object
|
57
|
+
def post_message(content, from: TestUser.whoami)
|
58
|
+
msg = if content.is_a?(Message)
|
59
|
+
content.text
|
60
|
+
else
|
61
|
+
content
|
62
|
+
end
|
63
|
+
msg_details = {
|
64
|
+
user_id: from.id,
|
65
|
+
text: msg,
|
66
|
+
type: :channel,
|
67
|
+
channel_id: id,
|
68
|
+
created_at: Time.now
|
69
|
+
}
|
70
|
+
TestSense.message_list << msg_details
|
71
|
+
TestMessage.new(TestSense.message_list.size - 1)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Lazily provides the details for a TestUser
|
75
|
+
# @api private
|
76
|
+
# @return [Hash] details for this instance
|
77
|
+
def details
|
78
|
+
@details = TestSense.room_list[id] if @details.empty?
|
79
|
+
@details.dup
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|