ircsupport 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.
@@ -0,0 +1,72 @@
1
+ require 'ircsupport/case'
2
+
3
+ module IRCSupport
4
+ module Masks
5
+ # @private
6
+ @@mask_wildcard = '[\x01-\xFF]{0,}'
7
+ # @private
8
+ @@mask_optional = '[\x01-\xFF]{1,1}'
9
+
10
+ # @param [String] mask The mask to match against.
11
+ # @param [String] string The string to match against the mask.
12
+ # @param [Symbol] casemapping The IRC casemapping to use in the match.
13
+ # @return [Boolean] Will be true of the string matches the mask.
14
+ def matches_mask(mask, string, casemapping = :rfc1459)
15
+ if mask =~ /\$/
16
+ raise ArgumentError, "Extended bans are not supported"
17
+ end
18
+ string = IRCSupport::Case.irc_upcase(string, casemapping)
19
+ mask = Regexp.quote(irc_upcase(mask, casemapping))
20
+ mask.gsub!('\*', @@mask_wildcard)
21
+ mask.gsub!('\?', @@mask_optional)
22
+ mask = Regexp.new(mask, nil, 'n')
23
+ return true if string =~ /\A#{mask}\z/
24
+ return false
25
+ end
26
+
27
+ # @param [Array] mask The masks to match against.
28
+ # @param [Array] strings The strings to match against the masks.
29
+ # @param [Symbol] casemapping The IRC casemapping to use in the match.
30
+ # @return [Hash] Each mask that was matched will be present as a key,
31
+ # and the values will be arrays of the strings that matched.
32
+ def matches_mask_array(masks, strings, casemapping = :rfc1459)
33
+ results = {}
34
+ masks.each do |mask|
35
+ strings.each do |string|
36
+ if matches_mask(mask, string, casemapping)
37
+ results[mask] ||= []
38
+ results[mask] << string
39
+ end
40
+ end
41
+ end
42
+ return results
43
+ end
44
+
45
+ # @param [String] mask A partial mask (e.g. 'foo*').
46
+ # @return [String] A normalized mask (e.g. 'foo*!*@*).
47
+ def normalize_mask(mask)
48
+ mask = mask.dup
49
+ mask.gsub!(/\*{2,}/, '*')
50
+ parts = []
51
+ remainder = nil
52
+
53
+ if mask !~ /!/ && mask =~ /@/
54
+ remainder = mask
55
+ parts[0] = '*'
56
+ else
57
+ parts[0], remainder = mask.split(/!/, 2)
58
+ end
59
+
60
+ if remainder
61
+ remainder.gsub!(/!/, '')
62
+ parts[1..2] = remainder.split(/@/, 2)
63
+ end
64
+ parts[2].gsub!(/@/, '') if parts[2]
65
+
66
+ (1..2).each { |i| parts[i] ||= '*' }
67
+ return parts[0] + "!" + parts[1] + "@" + parts[2]
68
+ end
69
+
70
+ module_function :matches_mask, :matches_mask_array, :normalize_mask
71
+ end
72
+ end
@@ -0,0 +1,616 @@
1
+ require 'ircsupport/numerics'
2
+ require 'ipaddr'
3
+ require 'pathname'
4
+
5
+ module IRCSupport
6
+ class Message
7
+ # @return [String] The sender prefix of the IRC message, if any.
8
+ attr_accessor :prefix
9
+
10
+ # @return [String] The IRC command.
11
+ attr_accessor :command
12
+
13
+ # @return [Array] The arguments to the IRC command.
14
+ attr_accessor :args
15
+
16
+ # @private
17
+ def initialize(args)
18
+ @prefix = args[:prefix]
19
+ @command = args[:command]
20
+ @args = args[:args]
21
+ end
22
+
23
+ # @return [String] The type of the IRC message.
24
+ def type
25
+ return @type if @type
26
+ return @command.downcase if self.class.name == 'IRCSupport::Message'
27
+ type = self.class.name.match(/^IRCSupport::Message::(.*)/)[1]
28
+ return type.gsub(/::|(?<=[[:lower:]])(?=[[:upper:]])/, '_').downcase
29
+ end
30
+
31
+ class Numeric < Message
32
+ # @return [String] The IRC command numeric.
33
+ attr_accessor :numeric
34
+
35
+ # @return [String] The name of the IRC command numeric.
36
+ attr_accessor :numeric_name
37
+
38
+ # @return [String] The arguments to the numeric command.
39
+ attr_accessor :numeric_args
40
+
41
+ # @private
42
+ def initialize(args)
43
+ super(args)
44
+ @numeric = args[:command]
45
+ @numeric_args = args[:args]
46
+ @numeric_name = IRCSupport::Numerics.numeric_to_name(@numeric)
47
+ @type = @numeric
48
+ end
49
+
50
+ # @return [Boolean] Will be true if this is an error numeric.
51
+ def is_error?
52
+ return @numeric_name =~ /^ERR/ ? true : false
53
+ end
54
+ end
55
+
56
+ class Numeric005 < Numeric
57
+ # @private
58
+ @@isupport_mappings = {
59
+ %w[MODES MAXCHANNELS NICKLEN MAXBANS TOPICLEN
60
+ KICKLEN CHANNELLEN CHIDLEN SILENCE AWAYLEN
61
+ MAXTARGETS WATCH MONITOR] => ->(v) { v.to_i },
62
+
63
+ %w[STATUSMSG ELIST CHANTYPES] => ->(v) { v.split("") },
64
+
65
+ %w[CASEMAPPING] => ->(v) { v.to_sym },
66
+
67
+ %w[NETWORK] => ->(v) { v },
68
+
69
+ %w[PREFIX] => ->(v) {
70
+ modes, prefixes = v.match(/^\((.+)\)(.+)$/)[1..2]
71
+ h = {}
72
+ modes.split("").each_with_index do |c, i|
73
+ h[c] = prefixes[i]
74
+ end
75
+ h
76
+ },
77
+
78
+ %w[CHANMODES] => ->(v) {
79
+ h = {}
80
+ h["A"], h["B"], h["C"], h["D"] = v.split(",").map {|l| l.split("")}
81
+ h
82
+ },
83
+
84
+ %w[CHANLIMIT MAXLIST IDCHAN] => ->(v) {
85
+ h = {}
86
+ v.split(",").each do |pair|
87
+ args, num = pair.split(":")
88
+ args.split("").each do |arg|
89
+ h[arg] = num.to_i
90
+ end
91
+ end
92
+ h
93
+ },
94
+
95
+ %w[TARGMAX] => ->(v) {
96
+ h = {}
97
+ v.split(",").each do |pair|
98
+ name, value = pair.split(":")
99
+ h[name] = value.to_i
100
+ end
101
+ h
102
+ },
103
+ }
104
+
105
+ # @return [Hash] The isupport options contained in the command.
106
+ attr_accessor :isupport
107
+
108
+ # @private
109
+ def initialize(args)
110
+ super(args)
111
+ @isupport = {}
112
+ args[:args].each do |value|
113
+ name, value = value.split(/=/, 2)
114
+ if value
115
+ proc = @@isupport_mappings.find {|key, _| key.include?(name)}
116
+ @isupport[name] = (proc && proc[1].call(value)) || value
117
+ else
118
+ @isupport[name] = true
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ class Numeric353 < Numeric
125
+ # @return [String] The channel.
126
+ attr_accessor :channel
127
+
128
+ # @return [String] The channel type.
129
+ attr_accessor :channel_type
130
+
131
+ # @return [Array] Each element is an array of two elements: the user
132
+ # prefix (if any), and the name of the user.
133
+ attr_accessor :users
134
+
135
+ # @private
136
+ def initialize(args)
137
+ super(args)
138
+ data = @args.last(@args.size - 1)
139
+ @channel_type = data.shift if data[0] =~ /^[@=*]$/
140
+ @channel = data[0]
141
+ @users = []
142
+ prefixes = args[:isupport]["PREFIX"].values.map { |p| Regexp.quote p }
143
+
144
+ data[1].split(/\s+/).each do |user|
145
+ user.sub! /^(#{prefixes.join '|'})/, ''
146
+ @users.push [$1, user]
147
+ end
148
+ end
149
+ end
150
+
151
+ class Numeric352 < Numeric
152
+ # @return [String] The target of the who reply, either a nickname or
153
+ # a channel name.
154
+ attr_accessor :target
155
+
156
+ # @return [String] The username.
157
+ attr_accessor :username
158
+
159
+ # @return [String] The host name.
160
+ attr_accessor :hostname
161
+
162
+ # @return [String] The server name.
163
+ attr_accessor :server
164
+
165
+ # @return [String] The nickname.
166
+ attr_accessor :nickname
167
+
168
+ # @return [Array] The user's prefixes.
169
+ attr_accessor :prefixes
170
+
171
+ # @return [Boolean] The away status.
172
+ attr_accessor :away
173
+
174
+ # @return [Fixnum] The user's hop count.
175
+ attr_accessor :hops
176
+
177
+ # @return [String] The user's realname.
178
+ attr_accessor :realname
179
+
180
+ # @private
181
+ def initialize(args)
182
+ super(args)
183
+ @target, @username, @hostname, @server, @nickname, status, rest =
184
+ @args.last(@args.size - 1)
185
+ status.sub! /[GH]/, ''
186
+ @away = $1 == 'G' ? true : false
187
+ @prefixes = status.split ''
188
+ @hops, @realname = rest.split /\s/, 2
189
+ @hops = @hops.to_i
190
+ end
191
+ end
192
+
193
+ class DCC < Message
194
+ # @return [String] The sender of the DCC message.
195
+ attr_accessor :sender
196
+
197
+ # @return [String] The argument string to the DCC message.
198
+ attr_accessor :dcc_args
199
+
200
+ # @private
201
+ def initialize(args)
202
+ super(args)
203
+ @sender = args[:prefix]
204
+ @dcc_args = args[:args][1]
205
+ @dcc_type = args[:dcc_type]
206
+ @type = "dcc_#{@dcc_type.downcase}"
207
+ end
208
+ end
209
+
210
+ class DCC::Chat < DCC
211
+ # @return [IPAddr] The sender's IP address.
212
+ attr_accessor :address
213
+
214
+ # @return [Fixnum] The sender's port number.
215
+ attr_accessor :port
216
+
217
+ # @private
218
+ def initialize(args)
219
+ super(args)
220
+ return if @dcc_args !~ /^(?:".+"|[^ ]+) +(\d+) +(\d+)/
221
+ @address = IPAddr.new($1.to_i, Socket::AF_INET)
222
+ @port = $2.to_i
223
+ end
224
+ end
225
+
226
+ class DCC::Send < DCC
227
+ # @return [IPAddr] The sender's IP address.
228
+ attr_accessor :address
229
+
230
+ # @return [Fixnum] The sender's port number.
231
+ attr_accessor :port
232
+
233
+ # @return [Pathname] The source filename.
234
+ attr_accessor :filename
235
+
236
+ # @return [Fixnum] The size of the source file, in bytes.
237
+ attr_accessor :size
238
+
239
+ # @private
240
+ def initialize(args)
241
+ super(args)
242
+ return if @dcc_args !~ /^(".+"|[^ ]+) +(\d+) +(\d+)(?: +(\d+))?/
243
+ @filename = $1
244
+ @address = IPAddr.new($2.to_i, Socket::AF_INET)
245
+ @port = $3.to_i
246
+ @size = $4.to_i
247
+
248
+ if @filename =~ /^"/
249
+ @filename.gsub!(/^"|"$/, '')
250
+ @filename.gsub!(/\\"/, '"');
251
+ end
252
+
253
+ @filename = Pathname.new(@filename).basename
254
+ end
255
+ end
256
+
257
+ class DCC::Accept < DCC
258
+ # @return [Pathname] The source filename.
259
+ attr_accessor :filename
260
+
261
+ # @return [Fixnum] The sender's port number.
262
+ attr_accessor :port
263
+
264
+ # @return [Fixnum] The byte position in the file.
265
+ attr_accessor :position
266
+
267
+ # @private
268
+ def initialize(args)
269
+ super(args)
270
+ return if @dcc_args !~ /^(".+"|[^ ]+) +(\d+) +(\d+)/
271
+ @filename = $1
272
+ @port = $2.to_i
273
+ @position = $3.to_i
274
+
275
+ if @filename =~ /^"/
276
+ @filename.gsub!(/^"|"$/, '')
277
+ @filename.gsub!(/\\"/, '"');
278
+ end
279
+
280
+ @filename = Pathname.new(@filename).basename
281
+ end
282
+ end
283
+
284
+ class DCC::Resume < DCC::Accept; end
285
+
286
+ class Error < Message
287
+ # @return [String] The error message.
288
+ attr_accessor :error
289
+
290
+ # @private
291
+ def initialize(args)
292
+ super(args)
293
+ @error = args[:args][0]
294
+ end
295
+ end
296
+
297
+ class Invite < Message
298
+ # @return [String] The user who sent the invite.
299
+ attr_accessor :inviter
300
+
301
+ # @return [String] The name of the channel you're being invited to.
302
+ attr_accessor :channel
303
+
304
+ # @private
305
+ def initialize(args)
306
+ super(args)
307
+ @inviter = args[:prefix]
308
+ @channel = args[:args][1]
309
+ end
310
+ end
311
+
312
+ class Join < Message
313
+ # @return [String] The user who is joining.
314
+ attr_accessor :joiner
315
+
316
+ # @return [String] The name of the channel being joined.
317
+ attr_accessor :channel
318
+
319
+ # @private
320
+ def initialize(args)
321
+ super(args)
322
+ @joiner = args[:prefix]
323
+ @channel = args[:args][0]
324
+ end
325
+ end
326
+
327
+ class Part < Message
328
+ # @return [String] The user who is parting.
329
+ attr_accessor :parter
330
+
331
+ # @return [String] The name of the channel being parted.
332
+ attr_accessor :channel
333
+
334
+ # @return [String] The part message, if any.
335
+ attr_accessor :message
336
+
337
+ # @private
338
+ def initialize(args)
339
+ super(args)
340
+ @parter = args[:prefix]
341
+ @channel = args[:args][0]
342
+ @message = args[:args][1]
343
+ @message = nil if @message && @message.empty?
344
+ end
345
+ end
346
+
347
+ class Kick < Message
348
+ # @return [String] The user who is doing the kicking.
349
+ attr_accessor :kicker
350
+
351
+ # @return [String] The name of the channel.
352
+ attr_accessor :channel
353
+
354
+ # @return [String] The user being kicked.
355
+ attr_accessor :kickee
356
+
357
+ # @return [String] The kick message, if any.
358
+ attr_accessor :message
359
+
360
+ # @private
361
+ def initialize(args)
362
+ super(args)
363
+ @kicker = args[:prefix]
364
+ @channel = args[:args][0]
365
+ @kickee = args[:args][1]
366
+ @message = args[:args][2]
367
+ @message = nil if @message && @message.empty?
368
+ end
369
+ end
370
+
371
+ class UserModeChange < Message
372
+ # @return [Array] The mode changes as returned by
373
+ # {IRCSupport::Modes#parse_modes}.
374
+ attr_accessor :mode_changes
375
+
376
+ # @private
377
+ def initialize(args)
378
+ super(args)
379
+ @mode_changes = IRCSupport::Modes.parse_modes(args[:args][0])
380
+ end
381
+ end
382
+
383
+ class ChannelModeChange < Message
384
+ # @return [String] The user or server doing the mode change(s).
385
+ attr_accessor :changer
386
+
387
+ # @return [String] The channel name.
388
+ attr_accessor :channel
389
+
390
+ # @return [Array] The mode changes as returned by
391
+ # {IRCSupport::Modes#parse_modes}.
392
+ attr_accessor :mode_changes
393
+
394
+ # @private
395
+ def initialize(args)
396
+ super(args)
397
+ @changer = args[:prefix]
398
+ @channel = args[:args][0]
399
+ @mode_changes = IRCSupport::Modes.parse_channel_modes(
400
+ args[:args].last(args[:args].size - 1),
401
+ chanmodes: args[:isupport]["CHANMODES"],
402
+ statmodes: args[:isupport]["PREFIX"].keys,
403
+ )
404
+ end
405
+ end
406
+
407
+ class Nick < Message
408
+ # @return [String] The user who is changing their nick.
409
+ attr_accessor :changer
410
+
411
+ # @return [String] The new nickname.
412
+ attr_accessor :nickname
413
+
414
+ # @private
415
+ def initialize(args)
416
+ super(args)
417
+ @changer = args[:prefix]
418
+ @nickname = args[:args][0]
419
+ end
420
+ end
421
+
422
+ class Topic < Message
423
+ # @return [String] The user or server which is changing the topic.
424
+ attr_accessor :changer
425
+
426
+ # @return [String] The name of the channel.
427
+ attr_accessor :channel
428
+
429
+ # @return [String] The new topic.
430
+ attr_accessor :topic
431
+
432
+ # @private
433
+ def initialize(args)
434
+ super(args)
435
+ @changer = args[:prefix]
436
+ @channel = args[:args][0]
437
+ @topic = args[:args][1]
438
+ @topic = nil if @topic && @topic.empty?
439
+ end
440
+ end
441
+
442
+ class Quit < Message
443
+ # @return [String] The user who is quitting.
444
+ attr_accessor :quitter
445
+
446
+ # @return [String] The quit message, if any.
447
+ attr_accessor :message
448
+
449
+ # @private
450
+ def initialize(args)
451
+ super(args)
452
+ @quitter = args[:prefix]
453
+ @message = args[:args][0]
454
+ @message = nil if @message && @message.empty?
455
+ end
456
+ end
457
+
458
+ class Ping < Message
459
+ # @return [String] The ping message, if any.
460
+ attr_accessor :message
461
+
462
+ # @private
463
+ def initialize(args)
464
+ super(args)
465
+ @message = args[:args][0]
466
+ @message = nil if @message && @message.empty?
467
+ end
468
+ end
469
+
470
+ class CAP < Message
471
+ # @return [String] The CAP subcommand.
472
+ attr_accessor :subcommand
473
+
474
+ # @return [Boolean] Will be true if this is a multipart reply.
475
+ attr_accessor :multipart
476
+
477
+ # @return [String] The text of the CAP reply.
478
+ attr_accessor :reply
479
+
480
+ # @private
481
+ def initialize(args)
482
+ super(args)
483
+ @subcommand = args[:args][0]
484
+ @type = "cap_#{@subcommand.downcase}"
485
+ if args[:args][1] == '*'
486
+ @multipart = true
487
+ @reply = args[:args][2]
488
+ else
489
+ @multipart = false
490
+ @reply = args[:args][1]
491
+ end
492
+ end
493
+ end
494
+
495
+ class CAP::LS < CAP
496
+ # @return [Hash] The capabilities referenced in the CAP reply. The keys
497
+ # are the capability names, and the values are arrays of modifiers
498
+ # (`:enable`, `:disable`, `:sticky`).
499
+ attr_accessor :capabilities
500
+
501
+ # @private
502
+ @@modifiers = {
503
+ '-' => :disable,
504
+ '~' => :enable,
505
+ '=' => :sticky,
506
+ }
507
+
508
+ # @private
509
+ def initialize(args)
510
+ super(args)
511
+ @capabilities = {}
512
+
513
+ reply.split.each do |chunk|
514
+ mods, capability = chunk.match(/\A([-=~]*)(.*)/).captures
515
+ modifiers = []
516
+ mods.split('').each do |modifier|
517
+ modifiers << @@modifiers[modifier] if @@modifiers[modifier]
518
+ end
519
+ modifiers << :enable if mods.empty?
520
+ @capabilities[capability] = modifiers
521
+ end
522
+ end
523
+ end
524
+
525
+ class CAP::LIST < CAP::LS; end
526
+ class CAP::ACK < CAP::LS; end
527
+
528
+ class ServerNotice < Message
529
+ # @return [String] The sender of the notice. Could be a server name,
530
+ # a service, or nothing at all.
531
+ attr_accessor :sender
532
+
533
+ # @return [String] The target of the server notice. Could be '*' or
534
+ # 'AUTH' or something else entirely.
535
+ attr_accessor :target
536
+
537
+ # @return [String] The text of the notice.
538
+ attr_accessor :message
539
+
540
+ # @private
541
+ def initialize(args)
542
+ super(args)
543
+ @sender = args[:prefix]
544
+ if args[:args].size == 2
545
+ @target = args[:args][0]
546
+ @message = args[:args][1]
547
+ else
548
+ @message = args[:args][0]
549
+ end
550
+ end
551
+ end
552
+
553
+ class Message < Message
554
+ # @return [String] The user who sent the message.
555
+ attr_accessor :sender
556
+
557
+ # @return [String] The text of the message.
558
+ attr_accessor :message
559
+
560
+ # @return [String] The name of the channel this message was sent to,
561
+ # if any.
562
+ attr_accessor :channel
563
+
564
+ # @private
565
+ def initialize(args)
566
+ super(args)
567
+ @sender = args[:prefix]
568
+ @message = args[:args][1]
569
+ @is_action = args[:is_action] || false
570
+ @is_notice = args[:is_notice] || false
571
+
572
+ if args[:is_public]
573
+ # broadcast messages are so 90s
574
+ @channel = args[:args][0].split(/,/).first
575
+ end
576
+
577
+ if args[:capabilities].include?('identify-msg')
578
+ @identified = args[:identified]
579
+ def self.identified?; @identified; end
580
+ end
581
+ end
582
+
583
+ # @return [Boolean] Will be true if this message is an action.
584
+ def is_action?; @is_action; end
585
+
586
+ # @return [Boolean] Will be true if this message is a notice.
587
+ def is_notice?; @is_notice; end
588
+ end
589
+
590
+ class CTCP < Message
591
+ # @return [String] The arguments to the CTCP.
592
+ attr_accessor :ctcp_args
593
+
594
+ # @private
595
+ def initialize(args)
596
+ super(args)
597
+ @sender = args[:prefix]
598
+ @ctcp_args = args[:args][1]
599
+ @ctcp_type = args[:ctcp_type]
600
+ @type = "ctcp_#{@ctcp_type.downcase}"
601
+
602
+ if args[:is_public]
603
+ @channel = args[:args][0].split(/,/).first
604
+ end
605
+ end
606
+ end
607
+
608
+ class CTCPReply < CTCP
609
+ # @private
610
+ def initialize(args)
611
+ super(args)
612
+ @type = "ctcpreply_#{@ctcp_type.downcase}"
613
+ end
614
+ end
615
+ end
616
+ end