cinch 0.3.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +53 -43
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +15 -0
- data/examples/basic/join_part.rb +38 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +16 -0
- data/examples/basic/seen.rb +36 -0
- data/examples/basic/urban_dict.rb +35 -0
- data/examples/basic/url_shorten.rb +35 -0
- data/examples/plugins/autovoice.rb +40 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/google.rb +37 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/memo.rb +50 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +41 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/urban_dict.rb +30 -0
- data/examples/plugins/url_shorten.rb +32 -0
- data/lib/cinch.rb +7 -20
- data/lib/cinch/ban.rb +41 -0
- data/lib/cinch/bot.rb +479 -0
- data/lib/cinch/callback.rb +11 -0
- data/lib/cinch/channel.rb +419 -0
- data/lib/cinch/constants.rb +369 -0
- data/lib/cinch/exceptions.rb +25 -0
- data/lib/cinch/helpers.rb +21 -0
- data/lib/cinch/irc.rb +344 -38
- data/lib/cinch/isupport.rb +96 -0
- data/lib/cinch/logger/formatted_logger.rb +80 -0
- data/lib/cinch/logger/logger.rb +44 -0
- data/lib/cinch/logger/null_logger.rb +18 -0
- data/lib/cinch/mask.rb +46 -0
- data/lib/cinch/message.rb +183 -0
- data/lib/cinch/message_queue.rb +62 -0
- data/lib/cinch/plugin.rb +205 -0
- data/lib/cinch/rubyext/infinity.rb +1 -0
- data/lib/cinch/rubyext/module.rb +18 -0
- data/lib/cinch/rubyext/queue.rb +19 -0
- data/lib/cinch/rubyext/string.rb +24 -0
- data/lib/cinch/syncable.rb +55 -0
- data/lib/cinch/user.rb +325 -0
- data/spec/bot_spec.rb +5 -0
- data/spec/channel_spec.rb +5 -0
- data/spec/cinch_spec.rb +5 -0
- data/spec/irc_spec.rb +5 -0
- data/spec/message_spec.rb +5 -0
- data/spec/plugin_spec.rb +5 -0
- data/spec/{helper.rb → spec_helper.rb} +0 -0
- data/spec/user_spec.rb +5 -0
- metadata +69 -51
- data/README.rdoc +0 -195
- data/examples/autovoice.rb +0 -32
- data/examples/custom_patterns.rb +0 -19
- data/examples/custom_prefix.rb +0 -25
- data/examples/google.rb +0 -31
- data/examples/hello.rb +0 -13
- data/examples/join_part.rb +0 -26
- data/examples/memo.rb +0 -40
- data/examples/msg.rb +0 -14
- data/examples/named-param-types.rb +0 -19
- data/examples/seen.rb +0 -41
- data/examples/urban_dict.rb +0 -31
- data/examples/url_shorten.rb +0 -34
- data/lib/cinch/base.rb +0 -368
- data/lib/cinch/irc/message.rb +0 -135
- data/lib/cinch/irc/parser.rb +0 -141
- data/lib/cinch/irc/socket.rb +0 -329
- data/lib/cinch/names.rb +0 -54
- data/lib/cinch/rules.rb +0 -171
- data/spec/base_spec.rb +0 -94
- data/spec/irc/helper.rb +0 -8
- data/spec/irc/message_spec.rb +0 -61
- data/spec/irc/parser_spec.rb +0 -103
- data/spec/irc/socket_spec.rb +0 -90
- data/spec/names_spec.rb +0 -393
- data/spec/options_spec.rb +0 -45
- data/spec/rules_spec.rb +0 -109
data/lib/cinch/names.rb
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
module Cinch
|
2
|
-
module Names
|
3
|
-
attr_reader :channel_names
|
4
|
-
|
5
|
-
def tracking_names?
|
6
|
-
!!@tracking_names
|
7
|
-
end
|
8
|
-
|
9
|
-
def track_names
|
10
|
-
return if tracking_names?
|
11
|
-
|
12
|
-
@tracking_names = true
|
13
|
-
|
14
|
-
@channel_names = {}
|
15
|
-
|
16
|
-
on(:join) do |m|
|
17
|
-
if m.nick == nick
|
18
|
-
names(m.channel)
|
19
|
-
else
|
20
|
-
channel_names[m.channel] ||= []
|
21
|
-
channel_names[m.channel].push m.nick
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
on(353) do |m|
|
26
|
-
channel = m.params.detect { |c| c.match(/^#/) }
|
27
|
-
names = m.text.split.collect { |n| n.sub(/^@/, '') }
|
28
|
-
channel_names[channel] ||= []
|
29
|
-
channel_names[channel] += names
|
30
|
-
end
|
31
|
-
|
32
|
-
on(:part) do |m|
|
33
|
-
if m.nick == nick
|
34
|
-
channel_names.delete(m.channel)
|
35
|
-
else
|
36
|
-
channel_names[m.channel] ||= []
|
37
|
-
channel_names[m.channel].delete m.nick
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
on(:quit, :kill) do |m|
|
42
|
-
channel_names.each_value { |names| names.delete m.nick }
|
43
|
-
end
|
44
|
-
|
45
|
-
on(:nick) do |m|
|
46
|
-
channel_names.each_value { |names| names.push m.recipient if names.delete m.nick }
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Cinch::Base
|
53
|
-
include Cinch::Names
|
54
|
-
end
|
data/lib/cinch/rules.rb
DELETED
@@ -1,171 +0,0 @@
|
|
1
|
-
module Cinch
|
2
|
-
|
3
|
-
# == Description
|
4
|
-
# Every rule defined through the Cinch::Base#plugin method becomes an instance
|
5
|
-
# of this class. Each rule consists of keys used for named parameters, a hash
|
6
|
-
# of options, and an Array of callbacks.
|
7
|
-
#
|
8
|
-
# When a rule matches an IRC message, all options with be checked, then all
|
9
|
-
# callbacks will be invoked.
|
10
|
-
#
|
11
|
-
# == Author
|
12
|
-
# * Lee Jarvis - ljjarvis@gmail.com
|
13
|
-
class Rule < Struct.new(:rule, :keys, :options, :callbacks)
|
14
|
-
def initialize(rule, keys, options, callback)
|
15
|
-
callbacks = [callback]
|
16
|
-
super(rule, keys, options, callbacks)
|
17
|
-
end
|
18
|
-
|
19
|
-
# Execute all callbacks, passing a Cinch::IRC::Message to them
|
20
|
-
def execute(message)
|
21
|
-
options.keys.each do |key|
|
22
|
-
case key
|
23
|
-
when :nick, :nicks
|
24
|
-
return unless validate(options[:nick] || options[:nicks], message.nick)
|
25
|
-
when :host, :hosts
|
26
|
-
return unless validate(options[:host] || options[:hosts], message.host)
|
27
|
-
when :user, :users
|
28
|
-
return unless validate(options[:user] || options[:users], message.user)
|
29
|
-
when :channel, :channels
|
30
|
-
if message.channel
|
31
|
-
return unless validate(options[:channel] || options[:channels], message.channel)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
callbacks.each do |blk|
|
37
|
-
blk.call(message)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# Validate rule attributes
|
42
|
-
def validate(option, attr)
|
43
|
-
if option.is_a?(Array)
|
44
|
-
return unless option.any?{|o| o == attr }
|
45
|
-
else
|
46
|
-
return unless option.to_s == attr
|
47
|
-
end
|
48
|
-
true
|
49
|
-
end
|
50
|
-
|
51
|
-
# The rule as a String
|
52
|
-
def to_s
|
53
|
-
rule
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# == Description
|
58
|
-
# This class provides an interface to manage rules. A rule should only ever be
|
59
|
-
# added using the Rules#add_rule method and retrieved using the Rules#get_rule
|
60
|
-
# method, or an alias of these.
|
61
|
-
#
|
62
|
-
# This class provides an easy way to add options or callbacks to an existing
|
63
|
-
# rule.
|
64
|
-
#
|
65
|
-
# Essentially the add_callback, add_option, and merge_options methods are just sugar
|
66
|
-
# so you don't have to edit Rule attributes directly
|
67
|
-
#
|
68
|
-
# == Example
|
69
|
-
# rules = Cinch::Rules.new
|
70
|
-
#
|
71
|
-
# rules.add('foo', [], {}, Proc.new{})
|
72
|
-
#
|
73
|
-
# rules.add_callback('foo', Proc.new{})
|
74
|
-
# rules['foo'].callbacks #=> [#<Proc:0x9f1e110@(main):100>, #<Proc:0x9f1e0f4@(main):150>]
|
75
|
-
#
|
76
|
-
# # Or assign directly
|
77
|
-
# rules.get('foo').callbacks << Proc.new {}
|
78
|
-
#
|
79
|
-
# rules['foo'].options #=> {}
|
80
|
-
# rules.add_option('foo', :nick, 'injekt')
|
81
|
-
# rules['foo'].options #=> {:nick => 'injekt'}
|
82
|
-
#
|
83
|
-
# # Or retrieve the rule first and assign directly
|
84
|
-
# rules.get_rule('foo')
|
85
|
-
# rules.options = {:foo => 'bar'}
|
86
|
-
# rules.options[:bar] = 'baz'
|
87
|
-
#
|
88
|
-
# == Author
|
89
|
-
# * Lee Jarvis - ljjarvis@gmail.com
|
90
|
-
class Rules
|
91
|
-
def initialize
|
92
|
-
@rules = {}
|
93
|
-
end
|
94
|
-
|
95
|
-
# Add a new rule, overwrites an already existing rule
|
96
|
-
def add_rule(rule, keys, options, callback)
|
97
|
-
@rules[rule] = Rule.new(rule, keys, options, callback)
|
98
|
-
end
|
99
|
-
alias :add :add_rule
|
100
|
-
|
101
|
-
# Return a Cinch::Rule by its rule, or nil it one does not exist
|
102
|
-
def get_rule(rule)
|
103
|
-
@rules[rule]
|
104
|
-
end
|
105
|
-
alias :get :get_rule
|
106
|
-
alias :[] :get_rule
|
107
|
-
|
108
|
-
# Remove a rule
|
109
|
-
def remove_rule(rule)
|
110
|
-
@rules.delete(rule)
|
111
|
-
end
|
112
|
-
alias :remove :remove_rule
|
113
|
-
|
114
|
-
# Check if a rule exists
|
115
|
-
def include?(rule)
|
116
|
-
@rules.key?(rule)
|
117
|
-
end
|
118
|
-
alias :has_rule? :include?
|
119
|
-
|
120
|
-
# Add a callback for an already existing rule
|
121
|
-
def add_callback(rule, blk)
|
122
|
-
return unless include?(rule)
|
123
|
-
@rules[rule].callbacks << blk
|
124
|
-
end
|
125
|
-
|
126
|
-
# Add an option for an already existing rule
|
127
|
-
def add_option(rule, key, value)
|
128
|
-
return unless include?(rule)
|
129
|
-
@rules[rule].options[key] = value
|
130
|
-
end
|
131
|
-
|
132
|
-
# Merge rule options
|
133
|
-
def merge_options(rule, ops={})
|
134
|
-
return unless include?(rule)
|
135
|
-
@rules[rule].options.merge!(ops)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Iterate over the rules
|
139
|
-
def each
|
140
|
-
@rules.each {|rule, obj| yield obj }
|
141
|
-
end
|
142
|
-
|
143
|
-
# Remove all rules
|
144
|
-
def clear
|
145
|
-
@rules.clear
|
146
|
-
end
|
147
|
-
|
148
|
-
# Check if any rules exist
|
149
|
-
def empty?
|
150
|
-
@rules.empty?
|
151
|
-
end
|
152
|
-
|
153
|
-
# Return how many rules exist
|
154
|
-
def count
|
155
|
-
@rules.size
|
156
|
-
end
|
157
|
-
alias :size :count
|
158
|
-
|
159
|
-
# Return the hash of rules
|
160
|
-
def all
|
161
|
-
@rules
|
162
|
-
end
|
163
|
-
|
164
|
-
# Return an Array of rules
|
165
|
-
def to_a
|
166
|
-
@rules.keys
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
end
|
171
|
-
|
data/spec/base_spec.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/helper'
|
2
|
-
|
3
|
-
describe "Cinch::Base" do
|
4
|
-
before do
|
5
|
-
@base = Cinch::Base.new
|
6
|
-
|
7
|
-
@full = Cinch::Base.new(
|
8
|
-
:server => 'irc.freenode.org',
|
9
|
-
:nick => 'CinchBot',
|
10
|
-
:channels => ['#cinch']
|
11
|
-
)
|
12
|
-
end
|
13
|
-
|
14
|
-
describe "::new" do
|
15
|
-
it "should add a ctcp version listener" do
|
16
|
-
@base.listeners.should include :ctcp
|
17
|
-
@base.listeners[:ctcp].should include :version
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
describe "#plugin" do
|
22
|
-
it "should compile and add a rule" do
|
23
|
-
@base.plugin('foo')
|
24
|
-
@base.rules.include?("^foo$").should == true
|
25
|
-
end
|
26
|
-
|
27
|
-
it "should add options to an existing rule" do
|
28
|
-
@base.plugin('foo') { }
|
29
|
-
@base.plugin('foo', :bar => 'baz') { }
|
30
|
-
rule = @base.rules.get('^foo$')
|
31
|
-
rule.options.should include :bar
|
32
|
-
end
|
33
|
-
|
34
|
-
it "should add its block to an existing rule" do
|
35
|
-
@base.plugin('foo') { }
|
36
|
-
@base.plugin('foo') { }
|
37
|
-
rule = @base.rules.get_rule('^foo$')
|
38
|
-
rule.callbacks.size.should == 2
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
describe "#on" do
|
43
|
-
it "should save a listener" do
|
44
|
-
@base.on(:foo) {}
|
45
|
-
@base.listeners.should include :foo
|
46
|
-
end
|
47
|
-
|
48
|
-
it "should store listener blocks in an Array" do
|
49
|
-
@base.listeners[:ping].should be_kind_of Array
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
describe "#compile" do
|
54
|
-
it "should return an Array of 2 values" do
|
55
|
-
ret = @base.compile("foo")
|
56
|
-
ret.should be_kind_of(Array)
|
57
|
-
ret.size.should == 2
|
58
|
-
end
|
59
|
-
|
60
|
-
it "should return an empty set of keys if no named parameters are labeled" do
|
61
|
-
rule, keys = @base.compile("foo")
|
62
|
-
keys.should be_empty
|
63
|
-
end
|
64
|
-
|
65
|
-
it "should return a key for each named parameter labeled" do
|
66
|
-
rule, keys = @base.compile("foo :bar :baz")
|
67
|
-
keys.size.should == 2
|
68
|
-
keys.should include "bar"
|
69
|
-
keys.should include "baz"
|
70
|
-
end
|
71
|
-
|
72
|
-
it "should return a rule of type String, unless Regexp is given" do
|
73
|
-
rule, keys = @base.compile(:foo)
|
74
|
-
rule.should be_kind_of(String)
|
75
|
-
|
76
|
-
rule, keys = @base.compile(/foo/)
|
77
|
-
rule.should be_kind_of(Regexp)
|
78
|
-
keys.should be_empty
|
79
|
-
end
|
80
|
-
|
81
|
-
it "should convert a custom pattern" do
|
82
|
-
@base.add_custom_pattern(:people, "foo|bar|baz")
|
83
|
-
rule, keys = @base.compile(":foo-people")
|
84
|
-
rule.should == "^(foo|bar|baz)$"
|
85
|
-
end
|
86
|
-
|
87
|
-
it "should automatically add start and end anchors" do
|
88
|
-
rule, keys = @base.compile("foo bar baz")
|
89
|
-
rule[0].chr.should == "^"
|
90
|
-
rule[-1].chr.should == "$"
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
data/spec/irc/helper.rb
DELETED
data/spec/irc/message_spec.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/helper'
|
2
|
-
|
3
|
-
describe "IRC::Message" do
|
4
|
-
before do
|
5
|
-
@message = Cinch::IRC::Message.new('rawline', 'prefix', 'COMMAND', ['#chan', 'hello world'])
|
6
|
-
end
|
7
|
-
|
8
|
-
describe "#add, #[]=" do
|
9
|
-
it "should add an attribute" do
|
10
|
-
@message.add(:custom, 'something')
|
11
|
-
@message.data.should include :custom
|
12
|
-
@message.data[:custom].should == "something"
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
describe "#delete" do
|
17
|
-
it "should remove an attribute" do
|
18
|
-
@message.add(:custom, 'something')
|
19
|
-
@message.delete(:custom)
|
20
|
-
@message.data.should_not include :custom
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
describe "#to_s" do
|
25
|
-
it "should return the raw IRC message" do
|
26
|
-
@message.to_s.should == @message.raw
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
describe "#method_missing" do
|
31
|
-
it "should return an attribute if it exists" do
|
32
|
-
@message.add(:custom, 'something')
|
33
|
-
@message.custom.should == 'something'
|
34
|
-
end
|
35
|
-
|
36
|
-
it "should return nil if no attribute exists" do
|
37
|
-
@message.foobar.should == nil
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
describe "default attributes" do
|
42
|
-
it "should contain a prefix" do
|
43
|
-
@message.prefix.should == 'prefix'
|
44
|
-
end
|
45
|
-
|
46
|
-
it "should contain a command" do
|
47
|
-
@message.command.should == "COMMAND"
|
48
|
-
end
|
49
|
-
|
50
|
-
it "should contain params" do
|
51
|
-
@message.params.should be_kind_of(Array)
|
52
|
-
@message.params.size.should == 2
|
53
|
-
end
|
54
|
-
|
55
|
-
it "should contain a symbolized command" do
|
56
|
-
@message.symbol.should == :command
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|
61
|
-
|
data/spec/irc/parser_spec.rb
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/helper'
|
2
|
-
|
3
|
-
# Common commands
|
4
|
-
commands = {
|
5
|
-
:ping => "PING :foobar",
|
6
|
-
:nick => ":foo!~baz@host.com NICK Baz",
|
7
|
-
:join => ":foo!~bar@host.com JOIN #baz",
|
8
|
-
:ctcp => ":foo!~bar@host.com PRIVMSG Baz :\001VERSION\001",
|
9
|
-
|
10
|
-
:privmsg => {
|
11
|
-
"to a channel" => ":foo!~bar@host.com PRIVMSG #baz :hello world",
|
12
|
-
"to a user" => ":foo!~bar@host.com PRIVMSG Baz :hello world",
|
13
|
-
},
|
14
|
-
|
15
|
-
:notice => {
|
16
|
-
"to a channel" => ":foo!~bar@host.com NOTICE #baz :hello world",
|
17
|
-
"to a user" => ":foo!~bar@host.com NOTICE Baz :hello world",
|
18
|
-
},
|
19
|
-
|
20
|
-
:part => {
|
21
|
-
"without a message" => ":foo!~bar@host.com PART #baz",
|
22
|
-
"with a message" => ":foo!~bar@host.com PART #baz :beer",
|
23
|
-
},
|
24
|
-
|
25
|
-
:quit => {
|
26
|
-
"without a message" => ":foo!~bar@host.com QUIT",
|
27
|
-
"with a message" => ":foo!~bar@host.com QUIT :baz"
|
28
|
-
}
|
29
|
-
}
|
30
|
-
|
31
|
-
describe "IRC::Parser" do
|
32
|
-
before do
|
33
|
-
@parser = Cinch::IRC::Parser.new
|
34
|
-
end
|
35
|
-
|
36
|
-
describe "#add_pattern" do
|
37
|
-
it "should add a pattern" do
|
38
|
-
@parser.add_pattern(:custom, /foo/)
|
39
|
-
@parser.patterns.key?(:custom)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
describe "#remove_pattern" do
|
44
|
-
it "should remove a pattern" do
|
45
|
-
@parser.add_pattern(:custom, /foo/)
|
46
|
-
@parser.remove_pattern(:custom)
|
47
|
-
@parser.patterns.keys.should_not include(:custom)
|
48
|
-
end
|
49
|
-
|
50
|
-
it "should return nil if a pattern doesn't exist" do
|
51
|
-
@parser.remove_pattern(:foo).should be nil
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
describe "#parse" do
|
56
|
-
it "should return an IRC::Message" do
|
57
|
-
@parser.parse("foo :bar").should be_kind_of(Cinch::IRC::Message)
|
58
|
-
end
|
59
|
-
|
60
|
-
it "should raise if given an invalid message" do
|
61
|
-
lambda { @parser.parse("#") }.should raise_error(ArgumentError)
|
62
|
-
end
|
63
|
-
|
64
|
-
commands.each do |cmd, passes|
|
65
|
-
if passes.is_a?(Hash)
|
66
|
-
passes.each do |extra, pass|
|
67
|
-
it "should parse a #{cmd.to_s.upcase} command #{extra}" do
|
68
|
-
m = @parser.parse(pass)
|
69
|
-
m.symbol.should == cmd
|
70
|
-
end
|
71
|
-
end
|
72
|
-
else
|
73
|
-
it "should parse a #{cmd.to_s.upcase} command" do
|
74
|
-
m = @parser.parse(passes)
|
75
|
-
m.symbol.should == cmd
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
end
|
81
|
-
|
82
|
-
describe "#parse_userhost" do
|
83
|
-
it "should return an Array" do
|
84
|
-
@parser.parse_userhost(":foo!bar@baz").should be_kind_of(Array)
|
85
|
-
end
|
86
|
-
|
87
|
-
it "should return 3 values" do
|
88
|
-
@parser.parse_userhost(":foo!bar@baz").size.should be 3
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
describe "#valid_channel?" do
|
93
|
-
it "should return true with a valid channel name" do
|
94
|
-
@parser.valid_channel?("#foo").should be true
|
95
|
-
end
|
96
|
-
|
97
|
-
it "should return false with an invalid channel name" do
|
98
|
-
@parser.valid_channel?("foo").should be false
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
end
|
103
|
-
|