kanal 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +8 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +12 -0
- data/kanal.gemspec +39 -0
- data/lib/kanal/core/conditions/condition.rb +36 -0
- data/lib/kanal/core/conditions/condition_creator.rb +32 -0
- data/lib/kanal/core/conditions/condition_pack.rb +52 -0
- data/lib/kanal/core/conditions/condition_pack_creator.rb +41 -0
- data/lib/kanal/core/conditions/condition_storage.rb +58 -0
- data/lib/kanal/core/core.rb +214 -0
- data/lib/kanal/core/helpers/condition_finder.rb +26 -0
- data/lib/kanal/core/helpers/parameter_bag.rb +22 -0
- data/lib/kanal/core/helpers/parameter_bag_with_registrator.rb +46 -0
- data/lib/kanal/core/helpers/parameter_finder_with_method_missing_mixin.rb +38 -0
- data/lib/kanal/core/helpers/parameter_registrator.rb +48 -0
- data/lib/kanal/core/helpers/router_proc_parser.rb +47 -0
- data/lib/kanal/core/hooks/hook_storage.rb +109 -0
- data/lib/kanal/core/input/input.rb +20 -0
- data/lib/kanal/core/interfaces/interface.rb +65 -0
- data/lib/kanal/core/logger/logger.rb +12 -0
- data/lib/kanal/core/output/output.rb +31 -0
- data/lib/kanal/core/output/output_creator.rb +17 -0
- data/lib/kanal/core/plugins/plugin.rb +44 -0
- data/lib/kanal/core/router/router.rb +97 -0
- data/lib/kanal/core/router/router_node.rb +131 -0
- data/lib/kanal/core/router/router_storage.rb +33 -0
- data/lib/kanal/core/services/service_container.rb +97 -0
- data/lib/kanal/interfaces/simple_cli/simple_cli_interface.rb +51 -0
- data/lib/kanal/plugins/batteries/batteries_plugin.rb +116 -0
- data/lib/kanal/version.rb +5 -0
- data/lib/kanal.rb +9 -0
- data/sig/kanal/core/conditions/condition_pack.rbs +9 -0
- data/sig/kanal/core/conditions/condition_storage.rbs +9 -0
- data/sig/kanal.rbs +4 -0
- metadata +90 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Kanal
|
2
|
+
module Core
|
3
|
+
module Output
|
4
|
+
# This class helps creating output with the help
|
5
|
+
# of handy dsl format
|
6
|
+
class OutputCreator
|
7
|
+
def initialize(input)
|
8
|
+
@input = input
|
9
|
+
end
|
10
|
+
|
11
|
+
def create(&block)
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
|
5
|
+
module Kanal
|
6
|
+
module Core
|
7
|
+
module Plugins
|
8
|
+
# Base class for plugins that can be registered in the core
|
9
|
+
class Plugin
|
10
|
+
#
|
11
|
+
# Name of the plugin, for it to be available
|
12
|
+
# for finding and getting
|
13
|
+
#
|
14
|
+
# @return [Symbol] <description>
|
15
|
+
#
|
16
|
+
def name
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# This method is for the setting up, it will be executed when plugin
|
22
|
+
# is being added to the core
|
23
|
+
#
|
24
|
+
# @param [Kanal::Core::Core] core <description>
|
25
|
+
#
|
26
|
+
# @return [void] <description>
|
27
|
+
#
|
28
|
+
def setup(core)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# If plugins does have rake tasks available for execution,
|
34
|
+
# require them here. They will be used
|
35
|
+
#
|
36
|
+
# @return [Array<Rake::TaskLib>] <description>
|
37
|
+
#
|
38
|
+
def rake_tasks
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./router_node"
|
4
|
+
|
5
|
+
module Kanal
|
6
|
+
module Core
|
7
|
+
module Router
|
8
|
+
# Router serves as a container class for
|
9
|
+
# root node of router nodes, also as somewhat
|
10
|
+
# namespace. Basically class router stores all the
|
11
|
+
# router nodes and have a name.
|
12
|
+
class Router
|
13
|
+
attr_reader :name,
|
14
|
+
:core
|
15
|
+
|
16
|
+
def initialize(name, core)
|
17
|
+
@name = name
|
18
|
+
@core = core
|
19
|
+
@root_node = nil
|
20
|
+
@default_node = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure(&block)
|
24
|
+
# Root node does not have parent
|
25
|
+
@root_node ||= RouterNode.new router: self, parent: nil, root: true
|
26
|
+
|
27
|
+
@root_node.instance_eval(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_response(&block)
|
31
|
+
raise "default node for router #{@name} already defined" if @default_node
|
32
|
+
|
33
|
+
@default_node = RouterNode.new parent: nil, router: self, default: true
|
34
|
+
|
35
|
+
@default_node.respond(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Main method for creating output if it is found or going to default output
|
39
|
+
def create_output_for_input(input)
|
40
|
+
# Checking if default node with output exists throw error if not
|
41
|
+
raise "Please provide default response for router before you try and throw input against it ;)" unless @default_node
|
42
|
+
|
43
|
+
raise "You did not actually .configure router, didn't you? There is no even root node! Use .configure method" unless @root_node
|
44
|
+
|
45
|
+
unless @root_node.children?
|
46
|
+
raise "Hey your router actually does not have ANY routes to work with. Did you even try adding them?"
|
47
|
+
end
|
48
|
+
|
49
|
+
@core.hooks.call :input_before_router, input
|
50
|
+
|
51
|
+
node = test_input_against_router_node input, @root_node
|
52
|
+
|
53
|
+
# No result means no route node was found for that input
|
54
|
+
# using default response
|
55
|
+
node ||= @default_node
|
56
|
+
|
57
|
+
output = node.construct_response input
|
58
|
+
|
59
|
+
@core.hooks.call :output_before_returned, input, output
|
60
|
+
|
61
|
+
output
|
62
|
+
end
|
63
|
+
|
64
|
+
# Recursive method for searching router nodes
|
65
|
+
def test_input_against_router_node(input, router_node)
|
66
|
+
# Allow root node because it does not have any conditions and does not have
|
67
|
+
# any responses, but it does have children. Well, it should have children...
|
68
|
+
# Basically:
|
69
|
+
# if router_node is root - proceed with code
|
70
|
+
# if router_node is not root and condition is not met - stop right here.
|
71
|
+
# Cannot proceed inside of this node.
|
72
|
+
return if !router_node.root? && !router_node.condition_met?(input, @core)
|
73
|
+
|
74
|
+
# Check if node has children first. Router node with children SHOULD NOT HAVE RESPONSE.
|
75
|
+
# There is an exception for this case so don't worry, it's not protected only by
|
76
|
+
# this comment
|
77
|
+
if router_node.children?
|
78
|
+
node = nil
|
79
|
+
|
80
|
+
router_node.children.each do |c|
|
81
|
+
node = test_input_against_router_node input, c
|
82
|
+
|
83
|
+
break if node
|
84
|
+
end
|
85
|
+
|
86
|
+
node
|
87
|
+
elsif router_node.response?
|
88
|
+
# Router node without children can have response
|
89
|
+
router_node
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private :test_input_against_router_node
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../output/output"
|
4
|
+
|
5
|
+
module Kanal
|
6
|
+
module Core
|
7
|
+
# This module stores RouterNode and helper classes which are
|
8
|
+
# important for building router nodes tree
|
9
|
+
module Router
|
10
|
+
# This class is used as a node in router
|
11
|
+
# tree, containing conditions and responses
|
12
|
+
class RouterNode
|
13
|
+
include Output
|
14
|
+
|
15
|
+
attr_reader :parent,
|
16
|
+
:children
|
17
|
+
|
18
|
+
# parameter default: is for knowing that this node
|
19
|
+
# is for default response
|
20
|
+
# default response cannot have child nodes
|
21
|
+
def initialize(*args, router:, parent:, default: false, root: false)
|
22
|
+
@router = router
|
23
|
+
@parent = parent
|
24
|
+
|
25
|
+
@children = []
|
26
|
+
|
27
|
+
@response_block = nil
|
28
|
+
|
29
|
+
@condition_pack_name = nil
|
30
|
+
@condition_name = nil
|
31
|
+
@condition_argument = nil
|
32
|
+
|
33
|
+
# We omit setting conditions because default router node does not need any conditions
|
34
|
+
# Also root node does not have conditions so we basically omit them if arguments are empty
|
35
|
+
return if default || root
|
36
|
+
|
37
|
+
# With this we attach names of condition pack and condition to this router
|
38
|
+
# node, so we will be able to find them later at runtime and use them
|
39
|
+
assign_condition_pack_and_condition_names_from_args!(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def on(*args, &block)
|
43
|
+
raise "You cannot add children to nodes with response ready. Response is a final line" if response?
|
44
|
+
|
45
|
+
child = RouterNode.new(*args, router: @router, parent: self)
|
46
|
+
add_child child
|
47
|
+
|
48
|
+
child.instance_eval(&block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def construct_response(input)
|
52
|
+
raise "no response block configured for this node. router: #{@router.name}. debug: #{debug_info}" unless @response_block
|
53
|
+
|
54
|
+
output = Output::Output.new @router.core.output_parameter_registrator, input, @router.core
|
55
|
+
|
56
|
+
output.instance_eval(&@response_block)
|
57
|
+
|
58
|
+
output
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond(&block)
|
62
|
+
raise "Router node with children cannot have response" unless @children.empty?
|
63
|
+
|
64
|
+
@response_block = block
|
65
|
+
end
|
66
|
+
|
67
|
+
def response?
|
68
|
+
!@response_block.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
# This method processes args to populate condition and condition pack
|
72
|
+
# in this router node
|
73
|
+
def assign_condition_pack_and_condition_names_from_args!(*args)
|
74
|
+
condition_pack_name = args[0]
|
75
|
+
condition_name = args[1]
|
76
|
+
|
77
|
+
# We assume we got condition that requires argument
|
78
|
+
if condition_name.is_a? Hash
|
79
|
+
# We search for arguments inside kwargs
|
80
|
+
@condition_argument = condition_name.values.first
|
81
|
+
|
82
|
+
condition_name = condition_name.keys.first
|
83
|
+
end
|
84
|
+
|
85
|
+
# This calls will raise errors if there is problem with pack or condition
|
86
|
+
# inside of it
|
87
|
+
pack = @router.core.condition_storage.get_condition_pack_by_name! condition_pack_name
|
88
|
+
condition = pack.get_condition_by_name! condition_name
|
89
|
+
|
90
|
+
if condition.with_argument? && !@condition_argument
|
91
|
+
raise "Condition requires argument, though you wrote it as :symbol, not as positional_arg:
|
92
|
+
Please check route with condition pack: #{condition_pack_name} and condition: #{condition_name}"
|
93
|
+
end
|
94
|
+
|
95
|
+
@condition_pack_name = condition_pack_name
|
96
|
+
@condition_name = condition_name
|
97
|
+
end
|
98
|
+
|
99
|
+
def debug_info
|
100
|
+
"RouterNode with condition pack: #{@condition_pack_name}, condition: #{@condition_name}, condition argument: #{@condition_argument}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def condition_met?(input, core)
|
104
|
+
c = condition
|
105
|
+
|
106
|
+
c.met? input, core, @condition_argument
|
107
|
+
end
|
108
|
+
|
109
|
+
def condition
|
110
|
+
pack = @router.core.condition_storage.get_condition_pack_by_name! @condition_pack_name
|
111
|
+
|
112
|
+
pack.get_condition_by_name! @condition_name
|
113
|
+
end
|
114
|
+
|
115
|
+
def root?
|
116
|
+
parent.nil?
|
117
|
+
end
|
118
|
+
|
119
|
+
def children?
|
120
|
+
!@children.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_child(node)
|
124
|
+
@children.append node
|
125
|
+
end
|
126
|
+
|
127
|
+
private :add_child
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "./router"
|
2
|
+
|
3
|
+
module Kanal
|
4
|
+
module Core
|
5
|
+
module Router
|
6
|
+
# Class with helper methods for creating and getting routers
|
7
|
+
class RouterStorage
|
8
|
+
def initialize(core)
|
9
|
+
@routers = []
|
10
|
+
@core = core
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Creates router by name and stores it for further access
|
15
|
+
#
|
16
|
+
# @param [Symbol] name <description>
|
17
|
+
#
|
18
|
+
# @return [Kanal::Core::Router::Router] <description>
|
19
|
+
#
|
20
|
+
def get_or_create_router(name)
|
21
|
+
router = @routers.find { |r| r.name == name }
|
22
|
+
|
23
|
+
unless router
|
24
|
+
router = Router.new name, @core
|
25
|
+
@routers.append router
|
26
|
+
end
|
27
|
+
|
28
|
+
router
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Kanal
|
2
|
+
module Core
|
3
|
+
module Services
|
4
|
+
# Stores info about service registration
|
5
|
+
class ServiceRegistration
|
6
|
+
attr_reader :service_class,
|
7
|
+
:type,
|
8
|
+
:block
|
9
|
+
|
10
|
+
def initialize(service_class, type, block)
|
11
|
+
@service_class = service_class
|
12
|
+
@type = type
|
13
|
+
@block = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def block?
|
17
|
+
!@block.nil?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Container allows service registration as well as
|
23
|
+
# getting those services. You can register service with different
|
24
|
+
# lifespan types.
|
25
|
+
#
|
26
|
+
class ServiceContainer
|
27
|
+
TYPE_SINGLETON = :singleton
|
28
|
+
TYPE_TRANSIENT = :transient
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@registrations = {}
|
32
|
+
@services = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Registering service so container knows about it and it's type and it's
|
37
|
+
# optional initialization block
|
38
|
+
#
|
39
|
+
# @param [Symbol] name <description>
|
40
|
+
# @param [class] service_class <description>
|
41
|
+
# @param [Symbol] type <description>
|
42
|
+
# @yield Initialization block. It should be used if your service
|
43
|
+
# requires postponed initialization
|
44
|
+
#
|
45
|
+
# @return [void] <description>
|
46
|
+
#
|
47
|
+
def register_service(name, service_class, type: TYPE_SINGLETON, &block)
|
48
|
+
return if @registrations.key? name
|
49
|
+
|
50
|
+
raise "Unrecognized service type #{type}. Allowed types: #{allowed_types}" unless allowed_types.include? type
|
51
|
+
|
52
|
+
registration = ServiceRegistration.new service_class, type, block
|
53
|
+
|
54
|
+
@registrations[name] = registration
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Gets the registered service by name
|
59
|
+
#
|
60
|
+
# @param [Symbol] name <description>
|
61
|
+
#
|
62
|
+
# @return [Object] <description>
|
63
|
+
#
|
64
|
+
def get(name)
|
65
|
+
raise "Service named #{name} was not registered in container" unless @registrations.key? name
|
66
|
+
|
67
|
+
registration = @registrations[name]
|
68
|
+
|
69
|
+
if registration.type == TYPE_SINGLETON
|
70
|
+
# Created once and reused after creation
|
71
|
+
if @services[name].nil?
|
72
|
+
@services[name] = create_service_from_registration registration
|
73
|
+
end
|
74
|
+
|
75
|
+
@services[name]
|
76
|
+
elsif registration.type == TYPE_TRANSIENT
|
77
|
+
# Created every time
|
78
|
+
create_service_from_registration registration
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_service_from_registration(registration)
|
83
|
+
return registration.block.call if registration.block?
|
84
|
+
|
85
|
+
registration.service_class.new
|
86
|
+
end
|
87
|
+
|
88
|
+
def allowed_types
|
89
|
+
[TYPE_SINGLETON, TYPE_TRANSIENT]
|
90
|
+
end
|
91
|
+
|
92
|
+
private :create_service_from_registration,
|
93
|
+
:allowed_types
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../core/interfaces/interface"
|
4
|
+
require_relative "../../plugins/batteries/batteries_plugin"
|
5
|
+
|
6
|
+
module Kanal
|
7
|
+
module Interfaces
|
8
|
+
module SimpleCli
|
9
|
+
# This interface provides input/output with the cli
|
10
|
+
class SimpleCliInterface < Kanal::Core::Interfaces::Interface
|
11
|
+
#
|
12
|
+
# <Description>
|
13
|
+
#
|
14
|
+
# @param [Kanal::Core::Core] core <description>
|
15
|
+
#
|
16
|
+
def initialize(core)
|
17
|
+
super
|
18
|
+
|
19
|
+
# For simple cli we need body
|
20
|
+
@core.register_plugin Kanal::Plugins::Batteries::BatteriesPlugin.new
|
21
|
+
|
22
|
+
@core.register_output_parameter :quit
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
loop do
|
27
|
+
puts ">>>"
|
28
|
+
input = @core.create_input
|
29
|
+
input.body = gets
|
30
|
+
|
31
|
+
output = router.create_output_for_input input
|
32
|
+
|
33
|
+
if output.quit
|
34
|
+
puts "Undestood! Quitting"
|
35
|
+
break
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "[bot]: #{output.body}"
|
39
|
+
rescue Interrupt
|
40
|
+
puts "Got it! Hard stop. Bye bye!"
|
41
|
+
break
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "End of conversation!"
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop; end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "kanal/core/plugins/plugin"
|
4
|
+
|
5
|
+
module Kanal
|
6
|
+
module Plugins
|
7
|
+
# This module stores needed classes and helpers for batteries plugin
|
8
|
+
module Batteries
|
9
|
+
# Plugin with some batteries like .body property etc
|
10
|
+
class BatteriesPlugin < Core::Plugins::Plugin
|
11
|
+
def name
|
12
|
+
:batteries
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup(core)
|
16
|
+
source_batteries core
|
17
|
+
body_batteries core
|
18
|
+
flow_batteries core
|
19
|
+
end
|
20
|
+
|
21
|
+
def flow_batteries(core)
|
22
|
+
core.add_condition_pack :flow do
|
23
|
+
add_condition :any do
|
24
|
+
met? do |_, _, _|
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def source_batteries(core)
|
32
|
+
# This parameter can be filled by different plugins/interfaces
|
33
|
+
# to point from which source message came
|
34
|
+
core.register_input_parameter :source
|
35
|
+
|
36
|
+
core.add_condition_pack :source do
|
37
|
+
add_condition :from do
|
38
|
+
with_argument
|
39
|
+
|
40
|
+
met? do |input, _, argument|
|
41
|
+
input.source == argument
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def body_batteries(core)
|
48
|
+
core.register_input_parameter :body
|
49
|
+
core.register_output_parameter :body
|
50
|
+
|
51
|
+
core.add_condition_pack :body do
|
52
|
+
add_condition :starts_with do
|
53
|
+
with_argument
|
54
|
+
|
55
|
+
met? do |input, _, argument|
|
56
|
+
if input.body.is_a? String
|
57
|
+
input.body.start_with? argument
|
58
|
+
else
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
add_condition :ends_with do
|
65
|
+
with_argument
|
66
|
+
|
67
|
+
met? do |input, _, argument|
|
68
|
+
if input.body.is_a? String
|
69
|
+
input.body.end_with? argument
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
add_condition :contains do
|
77
|
+
with_argument
|
78
|
+
|
79
|
+
met? do |input, _, argument|
|
80
|
+
if input.body.is_a? String
|
81
|
+
input.body.include? argument
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
add_condition :contains_one_of do
|
89
|
+
with_argument
|
90
|
+
|
91
|
+
met? do |input, _, argument|
|
92
|
+
met = false
|
93
|
+
|
94
|
+
argument.each do |word|
|
95
|
+
met = input.body.include? word
|
96
|
+
|
97
|
+
break if met
|
98
|
+
end
|
99
|
+
|
100
|
+
met
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
add_condition :equals do
|
105
|
+
with_argument
|
106
|
+
|
107
|
+
met? do |input, _, argument|
|
108
|
+
input.body == argument
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/kanal.rb
ADDED
data/sig/kanal.rbs
ADDED