lijab 0.1.1
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.
- data/bin/lijab +9 -0
- data/ext/extconf.rb +20 -0
- data/ext/readline_extra.c +172 -0
- data/ext/test.rb +29 -0
- data/lib/bleh.rb +45 -0
- data/lib/configs/commands/cowsay.rb +33 -0
- data/lib/configs/hooks/ting.rb +23 -0
- data/lib/lijab.rb +7 -0
- data/lib/lijab/commands.rb +139 -0
- data/lib/lijab/commands/contacts.rb +101 -0
- data/lib/lijab/commands/options.rb +49 -0
- data/lib/lijab/commands/simple.rb +132 -0
- data/lib/lijab/commands/status.rb +63 -0
- data/lib/lijab/commands/subscription.rb +78 -0
- data/lib/lijab/config.rb +174 -0
- data/lib/lijab/contacts.rb +324 -0
- data/lib/lijab/history.rb +122 -0
- data/lib/lijab/hooks.rb +109 -0
- data/lib/lijab/input.rb +234 -0
- data/lib/lijab/main.rb +248 -0
- data/lib/lijab/out.rb +174 -0
- data/lib/lijab/term/ansi.rb +20 -0
- data/lib/lijab/version.rb +4 -0
- data/lib/lijab/xmpp4r/message.rb +45 -0
- data/lib/readline/extra.rb +7 -0
- metadata +122 -0
@@ -0,0 +1,324 @@
|
|
1
|
+
|
2
|
+
class Jabber::Presence
|
3
|
+
PRETTY = Hash.new(["", []]).update(
|
4
|
+
{ :available => ["available", [:green, :bold]],
|
5
|
+
:away => ["away", [:magenta]],
|
6
|
+
:chat => ["chatty", [:green]],
|
7
|
+
:dnd => ["busy", [:red, :bold]],
|
8
|
+
:xa => ["not available", [:red]],
|
9
|
+
:offline => ["offline", [:blue]]
|
10
|
+
})
|
11
|
+
|
12
|
+
def pretty_show
|
13
|
+
case type()
|
14
|
+
when nil
|
15
|
+
show() || :available
|
16
|
+
when :unavailable
|
17
|
+
:offline
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def pretty(colorize=false)
|
22
|
+
sh = pretty_show()
|
23
|
+
|
24
|
+
s, colors = PRETTY[sh]
|
25
|
+
s = s.colored(*colors) if colorize
|
26
|
+
message = status() && !status().empty? ? " [#{status()}]" : ""
|
27
|
+
"#{s}#{message}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Lijab
|
32
|
+
module Contacts
|
33
|
+
|
34
|
+
class Contact
|
35
|
+
attr_accessor :simple_name, :history
|
36
|
+
attr_writer :color
|
37
|
+
attr_reader :roster_item
|
38
|
+
|
39
|
+
COLORS = [:red, :blue, :yellow, :green, :magenta, :cyan].sort_by { rand() }
|
40
|
+
@@cur_color = 0
|
41
|
+
|
42
|
+
def initialize(simple_name, roster_item)
|
43
|
+
@simple_name = simple_name
|
44
|
+
@roster_item = roster_item
|
45
|
+
@resource_jid = @roster_item.jid
|
46
|
+
@history = HistoryHandler::get(jid())
|
47
|
+
end
|
48
|
+
|
49
|
+
def presence(jid=nil)
|
50
|
+
if jid
|
51
|
+
p = @roster_item.presences.select { |p| p.jid == jid }.first
|
52
|
+
else
|
53
|
+
p = @roster_item.presences.inject(nil) do |max, presence|
|
54
|
+
!max.nil? && max.priority.to_i > presence.priority.to_i ? max : presence
|
55
|
+
end
|
56
|
+
end
|
57
|
+
p || Jabber::Presence.new.set_type(:unavailable)
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_message(msg)
|
61
|
+
@thread = msg.thread
|
62
|
+
|
63
|
+
if msg.body && !msg.body.empty?
|
64
|
+
@resource_jid = msg.from
|
65
|
+
Out::message_in(@simple_name, msg.body, [color(), :bold])
|
66
|
+
@history.log(msg.body, :from)
|
67
|
+
end
|
68
|
+
|
69
|
+
if msg.chat_state
|
70
|
+
s = ""
|
71
|
+
case msg.chat_state
|
72
|
+
when :composing
|
73
|
+
s = "is typing"
|
74
|
+
when :active
|
75
|
+
Out::clear_infoline
|
76
|
+
when :gone
|
77
|
+
s = "went away"
|
78
|
+
when :paused
|
79
|
+
s = "paused typing"
|
80
|
+
end
|
81
|
+
Out::infoline("* #{@simple_name} #{s}".red) unless s.empty?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_message(msg, jid=nil)
|
86
|
+
if msg.kind_of?(Jabber::Message)
|
87
|
+
msg.thread = @thread unless msg.thread
|
88
|
+
message = msg
|
89
|
+
elsif msg.kind_of?(String) && !msg.empty?
|
90
|
+
# TODO: send chat_state only in the first message
|
91
|
+
if jid
|
92
|
+
@resource_jid = jid
|
93
|
+
else
|
94
|
+
jid = @resource_jid
|
95
|
+
end
|
96
|
+
Out::message_out(@simple_name, msg, color())
|
97
|
+
message = Jabber::Message.new(jid, msg).set_type(:chat) \
|
98
|
+
.set_chat_state(:active) \
|
99
|
+
.set_thread(@thread)
|
100
|
+
|
101
|
+
@chat_state_timer.kill if @chat_state_timer && @chat_state_timer.alive?
|
102
|
+
end
|
103
|
+
|
104
|
+
message = HooksHandler::handle_pre_send_message(self, message)
|
105
|
+
return unless message
|
106
|
+
|
107
|
+
Main.client.send(message)
|
108
|
+
|
109
|
+
HooksHandler::handle_post_send_message(self, message)
|
110
|
+
|
111
|
+
@history.log(message.body, :to)
|
112
|
+
@chat_state = :active
|
113
|
+
end
|
114
|
+
|
115
|
+
def send_chat_state(state)
|
116
|
+
return if state == @chat_state
|
117
|
+
msg = Jabber::Message.new(jid(), nil).set_type(:chat).set_chat_state(state)
|
118
|
+
Main.client.send(msg)
|
119
|
+
@chat_state = state
|
120
|
+
end
|
121
|
+
|
122
|
+
def typed_stuff
|
123
|
+
send_chat_state(:composing)
|
124
|
+
|
125
|
+
@chat_state_timer.kill if @chat_state_timer && @chat_state_timer.alive?
|
126
|
+
@chat_state_timer = Thread.new do
|
127
|
+
sleep(3); return if !Main.connected
|
128
|
+
send_chat_state(:paused)
|
129
|
+
sleep(10); return if !Main.connected
|
130
|
+
send_chat_state(:active)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def presence_changed(old_p, new_p)
|
135
|
+
@resource_jid = @roster_item.jid if old_p && old_p.from == @resource_jid
|
136
|
+
end
|
137
|
+
|
138
|
+
def jid
|
139
|
+
@roster_item.jid
|
140
|
+
end
|
141
|
+
|
142
|
+
def online?
|
143
|
+
@roster_item.online?
|
144
|
+
end
|
145
|
+
|
146
|
+
def color
|
147
|
+
@color = COLORS[(@@cur_color = (@@cur_color + 1) % COLORS.length)] unless @color
|
148
|
+
@color
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_s;
|
152
|
+
@simple_name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Contacts < Hash
|
157
|
+
attr_reader :roster, :subscription_requests
|
158
|
+
|
159
|
+
def initialize(roster)
|
160
|
+
super()
|
161
|
+
@roster = roster
|
162
|
+
|
163
|
+
# why does everything always has to be so hackish?
|
164
|
+
self_ri = Jabber::Roster::Helper::RosterItem.new(Main.client)
|
165
|
+
self_ri.jid = Config.jid.strip
|
166
|
+
@roster.items[self_ri.jid] = self_ri
|
167
|
+
|
168
|
+
@roster.add_presence_callback(&method(:handle_presence))
|
169
|
+
@roster.add_subscription_callback(&method(:handle_subscription))
|
170
|
+
@roster.add_subscription_request_callback(&method(:handle_subscription))
|
171
|
+
@roster.wait_for_roster
|
172
|
+
|
173
|
+
@subscription_requests = {}
|
174
|
+
@short = {}
|
175
|
+
|
176
|
+
@roster.items.each do |jid, item|
|
177
|
+
add(jid, Contact.new(jid.node, item))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def [](k)
|
182
|
+
return @short[k] if @short.key?(k)
|
183
|
+
|
184
|
+
k = Jabber::JID.new(k) unless k.is_a?(Jabber::JID)
|
185
|
+
|
186
|
+
super(k) || super(k.strip)
|
187
|
+
end
|
188
|
+
|
189
|
+
def key?(k)
|
190
|
+
return true if @short.key?(k)
|
191
|
+
|
192
|
+
k = Jabber::JID.new(k) unless k.is_a?(Jabber::JID)
|
193
|
+
|
194
|
+
super(k) || super(k.strip)
|
195
|
+
end
|
196
|
+
|
197
|
+
def add(jid, contact=nil)
|
198
|
+
return unless jid.node # XXX: apparently transports have no node, do something here?
|
199
|
+
|
200
|
+
if contact
|
201
|
+
self[jid] = contact
|
202
|
+
if @short.key?(jid.node)
|
203
|
+
prev = @short[jid.node]
|
204
|
+
|
205
|
+
self[jid].simple_name = jid.strip.to_s
|
206
|
+
self[prev.jid].simple_name = prev.jid.strip.to_s
|
207
|
+
|
208
|
+
@short[prev.jid.strip.to_s] = @short.delete(jid.node)
|
209
|
+
@short[jid.strip.to_s] = self[jid]
|
210
|
+
else
|
211
|
+
@short[jid.node] = self[jid]
|
212
|
+
end
|
213
|
+
else
|
214
|
+
jid = Jabber::JID.new(jid) unless jid.is_a?(Jabber::JID)
|
215
|
+
jid.strip!
|
216
|
+
|
217
|
+
@roster.add(jid, nil, true)
|
218
|
+
end
|
219
|
+
|
220
|
+
contact || self[jid]
|
221
|
+
end
|
222
|
+
|
223
|
+
def remove(jid)
|
224
|
+
return false unless key?(jid)
|
225
|
+
|
226
|
+
contact = self[jid]
|
227
|
+
contact.roster_item.remove()
|
228
|
+
@short.delete(contact.simple_name)
|
229
|
+
self.delete(contact.jid)
|
230
|
+
|
231
|
+
true
|
232
|
+
end
|
233
|
+
|
234
|
+
def process_request(jid, action)
|
235
|
+
jid = Jabber::JID.new(jid) unless jid.is_a?(Jabber::JID)
|
236
|
+
jid.strip!
|
237
|
+
if @subscription_requests.include?(jid)
|
238
|
+
|
239
|
+
@subscription_requests.delete(jid)
|
240
|
+
case action
|
241
|
+
when :accept
|
242
|
+
Main.contacts.roster.accept_subscription(jid)
|
243
|
+
when :decline
|
244
|
+
Main.contacts.roster.decline_subscription(jid)
|
245
|
+
end
|
246
|
+
|
247
|
+
true
|
248
|
+
else
|
249
|
+
false
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def has_subscription_requests?
|
254
|
+
!@subscription_requests.empty?
|
255
|
+
end
|
256
|
+
|
257
|
+
def subscription_requests
|
258
|
+
@subscription_requests.keys.map { |jid| jid.to_s }
|
259
|
+
end
|
260
|
+
|
261
|
+
def completer(line, end_with_colon=true)
|
262
|
+
if line.include?(?@)
|
263
|
+
matches = @roster.items.values.collect { |ri| ri.presences }.flatten.select do |p|
|
264
|
+
p.from.to_s =~ /^#{Regexp.escape(line)}/
|
265
|
+
end.map { |p| p.from.to_s }
|
266
|
+
else
|
267
|
+
if Config.opts[:autocomplete_online_first]
|
268
|
+
matches = @short.keys.find_all do |name|
|
269
|
+
@short[name].online? && name =~ /^#{Regexp.escape(line)}/
|
270
|
+
end
|
271
|
+
end
|
272
|
+
if matches.empty? || !Config.opts[:autocomplete_online_first]
|
273
|
+
matches = @short.keys.find_all { |name| name =~ /^#{Regexp.escape(line)}/ }
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end_with_colon && matches.length == 1 ? "#{matches.first}:" : matches
|
277
|
+
end
|
278
|
+
|
279
|
+
def handle_non_contact_message(msg)
|
280
|
+
# TODO: improve this, maybe show the contact differentiated in /contacts
|
281
|
+
|
282
|
+
jid = msg.from.strip
|
283
|
+
|
284
|
+
ri = Jabber::Roster::Helper::RosterItem.new(Main.client)
|
285
|
+
ri.jid = jid
|
286
|
+
|
287
|
+
self[jid] = Contact.new(jid.to_s, ri)
|
288
|
+
self[jid].handle_message(msg)
|
289
|
+
#add(jid, Contact.new(jid.to_s, ri)).handle_message(msg)
|
290
|
+
end
|
291
|
+
|
292
|
+
def handle_presence(roster_item, old_p, new_p)
|
293
|
+
contact = self[new_p.from]
|
294
|
+
if Config.opts[:show_status_changes]
|
295
|
+
type = new_p.type
|
296
|
+
if type == nil || type == :unavailable && (!contact || !contact.online?)
|
297
|
+
Out::presence(new_p.from.to_s, new_p)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
if contact
|
302
|
+
contact.roster_item.presences.delete_if { |p| p.type == :unavailable } rescue nil
|
303
|
+
contact.presence_changed(old_p, new_p)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def handle_subscription(roster_item, presence)
|
308
|
+
show = true
|
309
|
+
if presence.type == :subscribe
|
310
|
+
show = !@subscription_requests.key?(presence.from.strip)
|
311
|
+
@subscription_requests[presence.from.strip] = presence
|
312
|
+
elsif presence.type == :subscribed
|
313
|
+
jid = presence.from.strip
|
314
|
+
|
315
|
+
@roster.add(jid)
|
316
|
+
add(jid, Contact.new(jid.node, @roster[jid]))
|
317
|
+
#p = Jabber::Presence.new.set_type(:probe)
|
318
|
+
end
|
319
|
+
|
320
|
+
Out::subscription(presence.from.to_s, presence.type, presence.status) if show
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class File
|
4
|
+
include File::Tail
|
5
|
+
end
|
6
|
+
|
7
|
+
module Lijab
|
8
|
+
|
9
|
+
module HistoryHandler
|
10
|
+
class DummyHistory
|
11
|
+
def log(*a)
|
12
|
+
end
|
13
|
+
|
14
|
+
def last(*a)
|
15
|
+
Out::put("warning: logs are disabled")
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class History
|
21
|
+
MEMORY_LOG_LENGTH = 50
|
22
|
+
|
23
|
+
def initialize(path, target=nil, log_to_session=false)
|
24
|
+
@path, @target, @log_to_session = path, target, log_to_session
|
25
|
+
@m = []
|
26
|
+
end
|
27
|
+
|
28
|
+
def init_logfile
|
29
|
+
@w = File.open(@path, 'a')
|
30
|
+
@r = File.open(@path, 'r')
|
31
|
+
@r.return_if_eof = true
|
32
|
+
end
|
33
|
+
|
34
|
+
def log(msg, direction, target=nil)
|
35
|
+
init_logfile() unless @w
|
36
|
+
|
37
|
+
time = Time.now.utc
|
38
|
+
target ||= @target
|
39
|
+
arrow = direction == :from ? "<-" : "->"
|
40
|
+
quoted = [msg].pack("M").gsub(/=?\n/) { |m| m[0] == ?= ? "" : "=0A" }
|
41
|
+
|
42
|
+
@w.puts("#{time.iso8601} #{target} #{arrow} #{quoted}")
|
43
|
+
@w.flush
|
44
|
+
|
45
|
+
@m.push({:time=>time.localtime, :target=>target, :direction=>direction, :msg=>msg})
|
46
|
+
@m.shift if @m.length > MEMORY_LOG_LENGTH
|
47
|
+
|
48
|
+
@m = []
|
49
|
+
|
50
|
+
HistoryHandler::log(msg, direction, target) if @log_to_session
|
51
|
+
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def last(n)
|
56
|
+
return [] if n <= 0
|
57
|
+
|
58
|
+
init_logfile() unless @r
|
59
|
+
|
60
|
+
if n <= @m.length
|
61
|
+
@m[-n..-1]
|
62
|
+
else
|
63
|
+
ret = []
|
64
|
+
@r.seek(0, File::SEEK_END)
|
65
|
+
@r.backward(n)
|
66
|
+
@r.tail(n-@m.length) do |l|
|
67
|
+
time, target, direction, msg = l.split(" ", 4)
|
68
|
+
ret << {:time => Time.parse(time).localtime,
|
69
|
+
:target => target,
|
70
|
+
:direction => direction == "<-" ? :from : :to,
|
71
|
+
:msg => msg.strip.unpack("M").first}
|
72
|
+
end
|
73
|
+
ret += @m
|
74
|
+
if @m.length < MEMORY_LOG_LENGTH
|
75
|
+
@m = (ret[0...n-@m.length] + @m)
|
76
|
+
@m = @m[-MEMORY_LOG_LENGTH..-1] if @m.length > MEMORY_LOG_LENGTH
|
77
|
+
end
|
78
|
+
ret
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module_function
|
84
|
+
|
85
|
+
@histories = {}
|
86
|
+
|
87
|
+
def get(jid)
|
88
|
+
name = jid.strip.to_s
|
89
|
+
if Config.account[:log]
|
90
|
+
path = File.join(Config.account[:log_dir], "#{name}.log")
|
91
|
+
@histories[name] ||= History.new(path, name, true)
|
92
|
+
else
|
93
|
+
@dummy ||= DummyHistory.new
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def log(msg, direction, target)
|
98
|
+
return unless Config.account[:log]
|
99
|
+
|
100
|
+
init_session_log() unless @session
|
101
|
+
@session.log(msg, direction, target)
|
102
|
+
end
|
103
|
+
|
104
|
+
def last(n)
|
105
|
+
unless Config.account[:log]
|
106
|
+
Out::put("warning: logs are disabled")
|
107
|
+
return []
|
108
|
+
end
|
109
|
+
|
110
|
+
init_session_log() unless @session
|
111
|
+
@session.last(n)
|
112
|
+
end
|
113
|
+
|
114
|
+
def init_session_log
|
115
|
+
return unless Config.account[:log]
|
116
|
+
|
117
|
+
@session = History.new(path = File.join(Config.account[:log_dir], "session.log"))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
data/lib/lijab/hooks.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
module Lijab
|
3
|
+
|
4
|
+
module HooksHandler
|
5
|
+
module_function
|
6
|
+
|
7
|
+
@on_connect = []
|
8
|
+
@on_disconnect = []
|
9
|
+
@on_incoming_message = []
|
10
|
+
@on_presence = []
|
11
|
+
@on_pre_send_message = []
|
12
|
+
@on_post_send_message = []
|
13
|
+
|
14
|
+
def init
|
15
|
+
@on_connect, @on_disconnect, @on_incoming_message, @on_presence = [], [], [], []
|
16
|
+
@on_pre_send_message, @on_post_send_message = [], []
|
17
|
+
|
18
|
+
Dir[File.join(Config.dirs[:hooks], '**', '*.rb')].each { |f| load f }
|
19
|
+
|
20
|
+
Main.client.add_message_callback(&method(:handle_message))
|
21
|
+
Main.contacts.roster.add_presence_callback(&method(:handle_presence))
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle_message(msg)
|
25
|
+
return unless msg.body && !msg.body.empty?
|
26
|
+
|
27
|
+
@on_incoming_message.each do |b|
|
28
|
+
b.call(Main.contacts[msg.from.strip], msg.body)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_presence(roster_item, old_p, new_p)
|
33
|
+
@on_presence.each do |b|
|
34
|
+
b.call(Main.contacts[roster_item.jid.strip], old_p, new_p)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def handle_pre_send_message(contact, msg)
|
39
|
+
return msg if @on_pre_send_message.empty? || !msg.body || msg.body.empty?
|
40
|
+
|
41
|
+
@on_pre_send_message.inject(msg) do |ret_msg, block|
|
42
|
+
args = [contact, ret_msg.body]
|
43
|
+
args.push(msg) if block.arity == 3
|
44
|
+
|
45
|
+
m = block.call(*args)
|
46
|
+
break if !m
|
47
|
+
|
48
|
+
if m.is_a?(Jabber::Message)
|
49
|
+
m
|
50
|
+
else
|
51
|
+
ret_msg.body = m.to_s
|
52
|
+
ret_msg
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_post_send_message(contact, msg)
|
58
|
+
return if !msg.body || msg.body.empty?
|
59
|
+
|
60
|
+
@on_post_send_message.each do |block|
|
61
|
+
args = [contact, msg.body]
|
62
|
+
args.push(msg) if block.arity == 3
|
63
|
+
block.call(*args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_connect
|
68
|
+
@on_connect.each { |b| b.call }
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_disconnect
|
72
|
+
@on_disconnect.each { |b| b.call }
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :on_connect, :on_disconnect, :on_incoming_message, :on_presence,
|
76
|
+
:on_pre_send_message, :on_post_send_message
|
77
|
+
module_function :on_connect, :on_disconnect, :on_incoming_message,:on_presence,
|
78
|
+
:on_pre_send_message, :on_post_send_message
|
79
|
+
end
|
80
|
+
|
81
|
+
module Hooks
|
82
|
+
module_function
|
83
|
+
|
84
|
+
def on_incoming_message(&block)
|
85
|
+
HooksHandler::on_incoming_message.push(block)
|
86
|
+
end
|
87
|
+
|
88
|
+
def on_presence(&block)
|
89
|
+
HooksHandler::on_presence.push(block)
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_pre_send_message(&block)
|
93
|
+
HooksHandler::on_pre_send_message.push(block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def on_post_send_message(&block)
|
97
|
+
HooksHandler::on_post_send_message.push(block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def on_connect(&block)
|
101
|
+
HooksHandler::on_connect.push(block)
|
102
|
+
end
|
103
|
+
|
104
|
+
def on_disconnect(&block)
|
105
|
+
HooksHandler::on_disconnect.push(block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|