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