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.
- data/.gitignore +6 -0
- data/.travis.yml +8 -0
- data/CHANGES.md +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.md +134 -0
- data/Rakefile +16 -0
- data/ircsupport.gemspec +26 -0
- data/lib/ircsupport.rb +10 -0
- data/lib/ircsupport/case.rb +84 -0
- data/lib/ircsupport/encoding.rb +74 -0
- data/lib/ircsupport/formatting.rb +149 -0
- data/lib/ircsupport/masks.rb +72 -0
- data/lib/ircsupport/message.rb +616 -0
- data/lib/ircsupport/modes.rb +101 -0
- data/lib/ircsupport/numerics.rb +242 -0
- data/lib/ircsupport/parser.rb +340 -0
- data/lib/ircsupport/validations.rb +33 -0
- data/lib/ircsupport/version.rb +4 -0
- data/test/case_test.rb +41 -0
- data/test/encoding_test.rb +27 -0
- data/test/formatting_test.rb +65 -0
- data/test/masks_test.rb +36 -0
- data/test/message_test.rb +416 -0
- data/test/modes_test.rb +34 -0
- data/test/numerics_test.rb +14 -0
- data/test/parser_test.rb +55 -0
- data/test/test_coverage.rb +8 -0
- data/test/test_helper.rb +5 -0
- data/test/validations_test.rb +35 -0
- metadata +143 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
module IRCSupport
|
2
|
+
module Modes
|
3
|
+
# @param [Array] modes The modes you want to parse.
|
4
|
+
# @return [Array] Each element will be a hash with two keys: `:set`,
|
5
|
+
# a boolean indicating whether the mode is being set (instead of unset);
|
6
|
+
# and `:mode`, the mode character.
|
7
|
+
def parse_modes(modes)
|
8
|
+
mode_changes = []
|
9
|
+
modes.scan(/[-+]\w+/).each do |modegroup|
|
10
|
+
set, modegroup = modegroup.split '', 2
|
11
|
+
set = set == '+' ? true : false
|
12
|
+
modegroup.split('').each do |mode|
|
13
|
+
mode_changes << { set: set, mode: mode }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
return mode_changes
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Array] modes The modes you want to parse.
|
20
|
+
# @option opts [Hash] :chanmodes The channel modes which are allowed. This is
|
21
|
+
# the same as the "CHANMODES" isupport option.
|
22
|
+
# @option opts [Hash] :statmodes The channel modes which are allowed. This is
|
23
|
+
# the same as the keys of the "PREFIX" isupport option.
|
24
|
+
# @return [Array] Each element will be a hash with three keys: `:set`,
|
25
|
+
# a boolean indicating whether the mode is being set (instead of unset);
|
26
|
+
# `:mode`, the mode character; and `:argument`, the argument to the mode,
|
27
|
+
# if any.
|
28
|
+
def parse_channel_modes(modeparts, opts = {})
|
29
|
+
chanmodes = opts[:chanmodes] || {
|
30
|
+
'A' => %w{b e I},
|
31
|
+
'B' => %w{k},
|
32
|
+
'C' => %w{l},
|
33
|
+
'D' => %w{i m n p s t a q r},
|
34
|
+
}
|
35
|
+
statmodes = opts[:statmodes] || %w{o h v}
|
36
|
+
|
37
|
+
mode_changes = []
|
38
|
+
modes, *args = modeparts
|
39
|
+
parse_modes(modes).each do |mode_change|
|
40
|
+
set, mode = mode_change[:set], mode_change[:mode]
|
41
|
+
case
|
42
|
+
when chanmodes["A"].include?(mode) || chanmodes["B"].include?(mode)
|
43
|
+
mode_changes << {
|
44
|
+
mode: mode,
|
45
|
+
set: set,
|
46
|
+
argument: args.shift
|
47
|
+
}
|
48
|
+
when chanmodes["C"].include?(mode)
|
49
|
+
mode_changes << {
|
50
|
+
mode: mode,
|
51
|
+
set: set,
|
52
|
+
argument: args.shift.to_i
|
53
|
+
}
|
54
|
+
when chanmodes["D"].include?(mode)
|
55
|
+
mode_changes << {
|
56
|
+
mode: mode,
|
57
|
+
set: set,
|
58
|
+
}
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Unknown mode: #{mode}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
return mode_changes
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param [String] modes A string of modes you want to condense
|
68
|
+
# (remove duplicates).
|
69
|
+
# @return [Strings] A condensed mode string.
|
70
|
+
def condense_modes(modes)
|
71
|
+
action = nil
|
72
|
+
result = ''
|
73
|
+
modes.split(//).each do |mode|
|
74
|
+
if mode =~ /[+-]/ and (!action or mode != action)
|
75
|
+
result += mode
|
76
|
+
action = mode
|
77
|
+
next
|
78
|
+
end
|
79
|
+
result += mode if mode =~ /[^+-]/
|
80
|
+
end
|
81
|
+
result.sub!(/[+-]\z/, '')
|
82
|
+
return result
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [String] before The "before" mode string.
|
86
|
+
# @param [String] after The "after" mode string.
|
87
|
+
# @return [String] A modestring representing the difference between the
|
88
|
+
# two mode strings.
|
89
|
+
def diff_modes(before, after)
|
90
|
+
before_modes = before.split(//)
|
91
|
+
after_modes = after.split(//)
|
92
|
+
removed = before_modes - after_modes
|
93
|
+
added = after_modes - before_modes
|
94
|
+
result = removed.map { |m| '-' + m }.join
|
95
|
+
result << added.map { |m| '+' + m }.join
|
96
|
+
return condense_modes(result)
|
97
|
+
end
|
98
|
+
|
99
|
+
module_function :parse_modes, :parse_channel_modes, :condense_modes, :diff_modes
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module IRCSupport
|
2
|
+
module Numerics
|
3
|
+
# @private
|
4
|
+
@@numeric_to_name_map = {
|
5
|
+
'001' => 'RPL_WELCOME', # RFC2812
|
6
|
+
'002' => 'RPL_YOURHOST', # RFC2812
|
7
|
+
'003' => 'RPL_CREATED', # RFC2812
|
8
|
+
'004' => 'RPL_MYINFO', # RFC2812
|
9
|
+
'005' => 'RPL_ISUPPORT', # draft-brocklesby-irc-isupport-03
|
10
|
+
'008' => 'RPL_SNOMASK', # Undernet
|
11
|
+
'009' => 'RPL_STATMEMTOT', # Undernet
|
12
|
+
'010' => 'RPL_STATMEM', # Undernet
|
13
|
+
'020' => 'RPL_CONNECTING', # IRCnet
|
14
|
+
'014' => 'RPL_YOURCOOKIE', # IRCnet
|
15
|
+
'042' => 'RPL_YOURID', # IRCnet
|
16
|
+
'043' => 'RPL_SAVENICK', # IRCnet
|
17
|
+
'050' => 'RPL_ATTEMPTINGJUNC', # aircd
|
18
|
+
'051' => 'RPL_ATTEMPTINGREROUTE', # aircd
|
19
|
+
'200' => 'RPL_TRACELINK', # RFC1459
|
20
|
+
'201' => 'RPL_TRACECONNECTING', # RFC1459
|
21
|
+
'202' => 'RPL_TRACEHANDSHAKE', # RFC1459
|
22
|
+
'203' => 'RPL_TRACEUNKNOWN', # RFC1459
|
23
|
+
'204' => 'RPL_TRACEOPERATOR', # RFC1459
|
24
|
+
'205' => 'RPL_TRACEUSER', # RFC1459
|
25
|
+
'206' => 'RPL_TRACESERVER', # RFC1459
|
26
|
+
'207' => 'RPL_TRACESERVICE', # RFC2812
|
27
|
+
'208' => 'RPL_TRACENEWTYPE', # RFC1459
|
28
|
+
'209' => 'RPL_TRACECLASS', # RFC2812
|
29
|
+
'210' => 'RPL_STATS', # aircd
|
30
|
+
'211' => 'RPL_STATSLINKINFO', # RFC1459
|
31
|
+
'212' => 'RPL_STATSCOMMANDS', # RFC1459
|
32
|
+
'213' => 'RPL_STATSCLINE', # RFC1459
|
33
|
+
'214' => 'RPL_STATSNLINE', # RFC1459
|
34
|
+
'215' => 'RPL_STATSILINE', # RFC1459
|
35
|
+
'216' => 'RPL_STATSKLINE', # RFC1459
|
36
|
+
'217' => 'RPL_STATSQLINE', # RFC1459
|
37
|
+
'218' => 'RPL_STATSYLINE', # RFC1459
|
38
|
+
'219' => 'RPL_ENDOFSTATS', # RFC1459
|
39
|
+
'221' => 'RPL_UMODEIS', # RFC1459
|
40
|
+
'231' => 'RPL_SERVICEINFO', # RFC1459
|
41
|
+
'233' => 'RPL_SERVICE', # RFC1459
|
42
|
+
'234' => 'RPL_SERVLIST', # RFC1459
|
43
|
+
'235' => 'RPL_SERVLISTEND', # RFC1459
|
44
|
+
'239' => 'RPL_STATSIAUTH', # IRCnet
|
45
|
+
'241' => 'RPL_STATSLLINE', # RFC1459
|
46
|
+
'242' => 'RPL_STATSUPTIME', # RFC1459
|
47
|
+
'243' => 'RPL_STATSOLINE', # RFC1459
|
48
|
+
'244' => 'RPL_STATSHLINE', # RFC1459
|
49
|
+
'245' => 'RPL_STATSSLINE', # Bahamut, IRCnet, Hybrid
|
50
|
+
'250' => 'RPL_STATSCONN', # ircu, Unreal
|
51
|
+
'251' => 'RPL_LUSERCLIENT', # RFC1459
|
52
|
+
'252' => 'RPL_LUSEROP', # RFC1459
|
53
|
+
'253' => 'RPL_LUSERUNKNOWN', # RFC1459
|
54
|
+
'254' => 'RPL_LUSERCHANNELS', # RFC1459
|
55
|
+
'255' => 'RPL_LUSERME', # RFC1459
|
56
|
+
'256' => 'RPL_ADMINME', # RFC1459
|
57
|
+
'257' => 'RPL_ADMINLOC1', # RFC1459
|
58
|
+
'258' => 'RPL_ADMINLOC2', # RFC1459
|
59
|
+
'259' => 'RPL_ADMINEMAIL', # RFC1459
|
60
|
+
'261' => 'RPL_TRACELOG', # RFC1459
|
61
|
+
'262' => 'RPL_TRACEEND', # RFC2812
|
62
|
+
'263' => 'RPL_TRYAGAIN', # RFC2812
|
63
|
+
'265' => 'RPL_LOCALUSERS', # aircd, Bahamut, Hybrid
|
64
|
+
'266' => 'RPL_GLOBALUSERS', # aircd, Bahamut, Hybrid
|
65
|
+
'267' => 'RPL_START_NETSTAT', # aircd
|
66
|
+
'268' => 'RPL_NETSTAT', # aircd
|
67
|
+
'269' => 'RPL_END_NETSTAT', # aircd
|
68
|
+
'270' => 'RPL_PRIVS', # ircu
|
69
|
+
'271' => 'RPL_SILELIST', # ircu
|
70
|
+
'272' => 'RPL_ENDOFSILELIST', # ircu
|
71
|
+
'300' => 'RPL_NONE', # RFC1459
|
72
|
+
'301' => 'RPL_AWAY', # RFC1459
|
73
|
+
'302' => 'RPL_USERHOST', # RFC1459
|
74
|
+
'303' => 'RPL_ISON', # RFC1459
|
75
|
+
'305' => 'RPL_UNAWAY', # RFC1459
|
76
|
+
'306' => 'RPL_NOWAWAY', # RFC1459
|
77
|
+
'307' => 'RPL_WHOISREGNICK', # Bahamut, Unreal, Plexus
|
78
|
+
'310' => 'RPL_WHOISMODES', # Plexus
|
79
|
+
'311' => 'RPL_WHOISUSER', # RFC1459
|
80
|
+
'312' => 'RPL_WHOISSERVER', # RFC1459
|
81
|
+
'313' => 'RPL_WHOISOPERATOR', # RFC1459
|
82
|
+
'314' => 'RPL_WHOWASUSER', # RFC1459
|
83
|
+
'315' => 'RPL_ENDOFWHO', # RFC1459
|
84
|
+
'317' => 'RPL_WHOISIDLE', # RFC1459
|
85
|
+
'318' => 'RPL_ENDOFWHOIS', # RFC1459
|
86
|
+
'319' => 'RPL_WHOISCHANNELS', # RFC1459
|
87
|
+
'321' => 'RPL_LISTSTART', # RFC1459
|
88
|
+
'322' => 'RPL_LIST', # RFC1459
|
89
|
+
'323' => 'RPL_LISTEND', # RFC1459
|
90
|
+
'324' => 'RPL_CHANNELMODEIS', # RFC1459
|
91
|
+
'325' => 'RPL_UNIQOPIS', # RFC2812
|
92
|
+
'328' => 'RPL_CHANNEL_URL', # Bahamut, AustHex
|
93
|
+
'329' => 'RPL_CREATIONTIME', # Bahamut
|
94
|
+
'330' => 'RPL_WHOISACCOUNT', # ircu
|
95
|
+
'331' => 'RPL_NOTOPIC', # RFC1459
|
96
|
+
'332' => 'RPL_TOPIC', # RFC1459
|
97
|
+
'333' => 'RPL_TOPICWHOTIME', # ircu
|
98
|
+
'338' => 'RPL_WHOISACTUALLY', # Bahamut, ircu
|
99
|
+
'340' => 'RPL_USERIP', # ircu
|
100
|
+
'341' => 'RPL_INVITING', # RFC1459
|
101
|
+
'342' => 'RPL_SUMMONING', # RFC1459
|
102
|
+
'345' => 'RPL_INVITED', # GameSurge
|
103
|
+
'346' => 'RPL_INVITELIST', # RFC2812
|
104
|
+
'347' => 'RPL_ENDOFINVITELIST', # RFC2812
|
105
|
+
'348' => 'RPL_EXCEPTLIST', # RFC2812
|
106
|
+
'349' => 'RPL_ENDOFEXCEPTLIST', # RFC2812
|
107
|
+
'351' => 'RPL_VERSION', # RFC1459
|
108
|
+
'352' => 'RPL_WHOREPLY', # RFC1459
|
109
|
+
'353' => 'RPL_NAMREPLY', # RFC1459
|
110
|
+
'354' => 'RPL_WHOSPCRPL', # ircu
|
111
|
+
'355' => 'RPL_NAMREPLY_', # QuakeNet
|
112
|
+
'361' => 'RPL_KILLDONE', # RFC1459
|
113
|
+
'362' => 'RPL_CLOSING', # RFC1459
|
114
|
+
'363' => 'RPL_CLOSEEND', # RFC1459
|
115
|
+
'364' => 'RPL_LINKS', # RFC1459
|
116
|
+
'365' => 'RPL_ENDOFLINKS', # RFC1459
|
117
|
+
'366' => 'RPL_ENDOFNAMES', # RFC1459
|
118
|
+
'367' => 'RPL_BANLIST', # RFC1459
|
119
|
+
'368' => 'RPL_ENDOFBANLIST', # RFC1459
|
120
|
+
'369' => 'RPL_ENDOFWHOWAS', # RFC1459
|
121
|
+
'371' => 'RPL_INFO', # RFC1459
|
122
|
+
'372' => 'RPL_MOTD', # RFC1459
|
123
|
+
'373' => 'RPL_INFOSTART', # RFC1459
|
124
|
+
'374' => 'RPL_ENDOFINFO', # RFC1459
|
125
|
+
'375' => 'RPL_MOTDSTART', # RFC1459
|
126
|
+
'376' => 'RPL_ENDOFMOTD', # RFC1459
|
127
|
+
'381' => 'RPL_YOUREOPER', # RFC1459
|
128
|
+
'382' => 'RPL_REHASHING', # RFC1459
|
129
|
+
'383' => 'RPL_YOURESERVICE', # RFC2812
|
130
|
+
'384' => 'RPL_MYPORTIS', # RFC1459
|
131
|
+
'385' => 'RPL_NOTOPERANYMORE', # AustHex, Hybrid, Unreal
|
132
|
+
'386' => 'RPL_QLIST', # Unreal
|
133
|
+
'387' => 'RPL_ENDOFQLIST', # Unreal
|
134
|
+
'391' => 'RPL_TIME', # RFC1459
|
135
|
+
'392' => 'RPL_USERSSTART', # RFC1459
|
136
|
+
'393' => 'RPL_USERS', # RFC1459
|
137
|
+
'394' => 'RPL_ENDOFUSERS', # RFC1459
|
138
|
+
'395' => 'RPL_NOUSERS', # RFC1459
|
139
|
+
'396' => 'RPL_HOSTHIDDEN', # Undernet
|
140
|
+
'401' => 'ERR_NOSUCHNICK', # RFC1459
|
141
|
+
'402' => 'ERR_NOSUCHSERVER', # RFC1459
|
142
|
+
'403' => 'ERR_NOSUCHCHANNEL', # RFC1459
|
143
|
+
'404' => 'ERR_CANNOTSENDTOCHAN', # RFC1459
|
144
|
+
'405' => 'ERR_TOOMANYCHANNELS', # RFC1459
|
145
|
+
'406' => 'ERR_WASNOSUCHNICK', # RFC1459
|
146
|
+
'407' => 'ERR_TOOMANYTARGETS', # RFC1459
|
147
|
+
'408' => 'ERR_NOSUCHSERVICE', # RFC2812
|
148
|
+
'409' => 'ERR_NOORIGIN', # RFC1459
|
149
|
+
'411' => 'ERR_NORECIPIENT', # RFC1459
|
150
|
+
'412' => 'ERR_NOTEXTTOSEND', # RFC1459
|
151
|
+
'413' => 'ERR_NOTOPLEVEL', # RFC1459
|
152
|
+
'414' => 'ERR_WILDTOPLEVEL', # RFC1459
|
153
|
+
'415' => 'ERR_BADMASK', # RFC2812
|
154
|
+
'421' => 'ERR_UNKNOWNCOMMAND', # RFC1459
|
155
|
+
'422' => 'ERR_NOMOTD', # RFC1459
|
156
|
+
'423' => 'ERR_NOADMININFO', # RFC1459
|
157
|
+
'424' => 'ERR_FILEERROR', # RFC1459
|
158
|
+
'425' => 'ERR_NOOPERMOTD', # Unreal
|
159
|
+
'429' => 'ERR_TOOMANYAWAY', # Bahamut
|
160
|
+
'430' => 'ERR_EVENTNICKCHANGE', # AustHex
|
161
|
+
'431' => 'ERR_NONICKNAMEGIVEN', # RFC1459
|
162
|
+
'432' => 'ERR_ERRONEUSNICKNAME', # RFC1459
|
163
|
+
'433' => 'ERR_NICKNAMEINUSE', # RFC1459
|
164
|
+
'436' => 'ERR_NICKCOLLISION', # RFC1459
|
165
|
+
'439' => 'ERR_TARGETTOOFAST', # ircu
|
166
|
+
'440' => 'ERR_SERCVICESDOWN', # Bahamut, Unreal
|
167
|
+
'441' => 'ERR_USERNOTINCHANNEL', # RFC1459
|
168
|
+
'442' => 'ERR_NOTONCHANNEL', # RFC1459
|
169
|
+
'443' => 'ERR_USERONCHANNEL', # RFC1459
|
170
|
+
'444' => 'ERR_NOLOGIN', # RFC1459
|
171
|
+
'445' => 'ERR_SUMMONDISABLED', # RFC1459
|
172
|
+
'446' => 'ERR_USERSDISABLED', # RFC1459
|
173
|
+
'447' => 'ERR_NONICKCHANGE', # Unreal
|
174
|
+
'449' => 'ERR_NOTIMPLEMENTED', # Undernet
|
175
|
+
'451' => 'ERR_NOTREGISTERED', # RFC1459
|
176
|
+
'455' => 'ERR_HOSTILENAME', # Unreal
|
177
|
+
'459' => 'ERR_NOHIDING', # Unreal
|
178
|
+
'460' => 'ERR_NOTFORHALFOPS', # Unreal
|
179
|
+
'461' => 'ERR_NEEDMOREPARAMS', # RFC1459
|
180
|
+
'462' => 'ERR_ALREADYREGISTRED', # RFC1459
|
181
|
+
'463' => 'ERR_NOPERMFORHOST', # RFC1459
|
182
|
+
'464' => 'ERR_PASSWDMISMATCH', # RFC1459
|
183
|
+
'465' => 'ERR_YOUREBANNEDCREEP', # RFC1459
|
184
|
+
'466' => 'ERR_YOUWILLBEBANNED', # RFC1459
|
185
|
+
'467' => 'ERR_KEYSET', # RFC1459
|
186
|
+
'469' => 'ERR_LINKSET', # Unreal
|
187
|
+
'471' => 'ERR_CHANNELISFULL', # RFC1459
|
188
|
+
'472' => 'ERR_UNKNOWNMODE', # RFC1459
|
189
|
+
'473' => 'ERR_INVITEONLYCHAN', # RFC1459
|
190
|
+
'474' => 'ERR_BANNEDFROMCHAN', # RFC1459
|
191
|
+
'475' => 'ERR_BADCHANNELKEY', # RFC1459
|
192
|
+
'476' => 'ERR_BADCHANMASK', # RFC2812
|
193
|
+
'477' => 'ERR_NOCHANMODES', # RFC2812
|
194
|
+
'478' => 'ERR_BANLISTFULL', # RFC2812
|
195
|
+
'481' => 'ERR_NOPRIVILEGES', # RFC1459
|
196
|
+
'482' => 'ERR_CHANOPRIVSNEEDED', # RFC1459
|
197
|
+
'483' => 'ERR_CANTKILLSERVER', # RFC1459
|
198
|
+
'484' => 'ERR_RESTRICTED', # RFC2812
|
199
|
+
'485' => 'ERR_UNIQOPPRIVSNEEDED', # RFC2812
|
200
|
+
'488' => 'ERR_TSLESSCHAN', # IRCnet
|
201
|
+
'491' => 'ERR_NOOPERHOST', # RFC1459
|
202
|
+
'492' => 'ERR_NOSERVICEHOST', # RFC1459
|
203
|
+
'493' => 'ERR_NOFEATURE', # ircu
|
204
|
+
'494' => 'ERR_BADFEATURE', # ircu
|
205
|
+
'495' => 'ERR_BADLOGTYPE', # ircu
|
206
|
+
'496' => 'ERR_BADLOGSYS', # ircu
|
207
|
+
'497' => 'ERR_BADLOGVALUE', # ircu
|
208
|
+
'498' => 'ERR_ISOPERLCHAN', # ircu
|
209
|
+
'501' => 'ERR_UMODEUNKNOWNFLAG', # RFC1459
|
210
|
+
'502' => 'ERR_USERSDONTMATCH', # RFC1459
|
211
|
+
'503' => 'ERR_GHOSTEDCLIENT', # Hybrid
|
212
|
+
'730' => 'RPL_MONONLINE', # ratbox
|
213
|
+
'731' => 'RPL_MONOFFLINE', # ratbox
|
214
|
+
'732' => 'RPL_MONLIST', # ratbox
|
215
|
+
'733' => 'RPL_ENDOFMONLIST', # ratbox
|
216
|
+
'732' => 'ERR_MONLISTFULL', # ratbox
|
217
|
+
'900' => 'RPL_SASLLOGIN', # charybdis, ircd-seven
|
218
|
+
'903' => 'RPL_SASLSUCCESS', # charybdis, ircd-seven
|
219
|
+
'904' => 'RPL_SASLFAILED', # charybdis, ircd-seven
|
220
|
+
'905' => 'RPL_SASLERROR', # charybdis, ircd-seven
|
221
|
+
'906' => 'RPL_SASLABORT', # charybdis, ircd-seven
|
222
|
+
'907' => 'RPL_SASLREADYAUTH', # charybdis, ircd-seven
|
223
|
+
}
|
224
|
+
|
225
|
+
# @private
|
226
|
+
@@name_to_numeric_map = @@numeric_to_name_map.invert
|
227
|
+
|
228
|
+
# @param [String] numeric A numeric to look up.
|
229
|
+
# @return [String] The name of the numeric.
|
230
|
+
def numeric_to_name(numeric)
|
231
|
+
return @@numeric_to_name_map[numeric]
|
232
|
+
end
|
233
|
+
|
234
|
+
# @param [String] name A name to look up.
|
235
|
+
# @return [String] The numeric corresponding to the name.
|
236
|
+
def name_to_numeric(name)
|
237
|
+
return @@name_to_numeric_map[name]
|
238
|
+
end
|
239
|
+
|
240
|
+
module_function :numeric_to_name, :name_to_numeric
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'ircsupport/message'
|
2
|
+
|
3
|
+
module IRCSupport
|
4
|
+
class Parser
|
5
|
+
# @private
|
6
|
+
@@eol = '\x00\x0a\x0d'
|
7
|
+
# @private
|
8
|
+
@@space = / +/
|
9
|
+
# @private
|
10
|
+
@@maybe_space = / */
|
11
|
+
# @private
|
12
|
+
@@non_space = /[^ ]+/
|
13
|
+
# @private
|
14
|
+
@@numeric = /[0-9]{3}/
|
15
|
+
# @private
|
16
|
+
@@command = /[a-zA-Z]+/
|
17
|
+
# @private
|
18
|
+
@@irc_name = /[^#@@eol :][^#@@eol ]*/
|
19
|
+
# @private
|
20
|
+
@@irc_line = /
|
21
|
+
\A
|
22
|
+
(?: : (?<prefix> #@@non_space ) #@@space )?
|
23
|
+
(?<command> #@@numeric | #@@command )
|
24
|
+
(?: #@@space (?<args> #@@irc_name (?: #@@space #@@irc_name )* ) )?
|
25
|
+
(?: #@@space : (?<trailing_arg> [^#@@eol]* ) | #@@maybe_space )?
|
26
|
+
\z
|
27
|
+
/x
|
28
|
+
# @private
|
29
|
+
@@low_quote_from = /[\x00\x0a\x0d\x10]/
|
30
|
+
# @private
|
31
|
+
@@low_quote_to = {
|
32
|
+
"\x00" => "\x100",
|
33
|
+
"\x0a" => "\x10n",
|
34
|
+
"\x0d" => "\x10r",
|
35
|
+
"\x10" => "\x10\x10",
|
36
|
+
}
|
37
|
+
# @private
|
38
|
+
@@low_dequote_from = /\x10[0nr\x10]/
|
39
|
+
# @private
|
40
|
+
@@low_dequote_to = {
|
41
|
+
"\x100" => "\x00",
|
42
|
+
"\x10n" => "\x0a",
|
43
|
+
"\x10r" => "\x0d",
|
44
|
+
"\x10\x10" => "\x10",
|
45
|
+
}
|
46
|
+
# @private
|
47
|
+
@@default_isupport = {
|
48
|
+
"PREFIX" => {"o" => "@", "v" => "+"},
|
49
|
+
"CHANTYPES" => ["#"],
|
50
|
+
"CHANMODES" => {
|
51
|
+
"A" => ["b"],
|
52
|
+
"B" => ["k"],
|
53
|
+
"C" => ["l"],
|
54
|
+
"D" => %w[i m n p s t r]
|
55
|
+
},
|
56
|
+
"MODES" => 1,
|
57
|
+
"NICKLEN" => 999,
|
58
|
+
"MAXBANS" => 999,
|
59
|
+
"TOPICLEN" => 999,
|
60
|
+
"KICKLEN" => 999,
|
61
|
+
"CHANNELLEN" => 999,
|
62
|
+
"CHIDLEN" => 5,
|
63
|
+
"AWAYLEN" => 999,
|
64
|
+
"MAXTARGETS" => 1,
|
65
|
+
"MAXCHANNELS" => 999,
|
66
|
+
"CHANLIMIT" => {"#" => 999},
|
67
|
+
"STATUSMSG" => ["@", "+"],
|
68
|
+
"CASEMAPPING" => :rfc1459,
|
69
|
+
"ELIST" => [],
|
70
|
+
"MONITOR" => 0,
|
71
|
+
}
|
72
|
+
|
73
|
+
# The isupport configuration of the IRC server.
|
74
|
+
# The configuration will be seeded with sane defaults, and updated in
|
75
|
+
# response to parsed {IRCSupport::Message::Numeric005 `005`} messages.
|
76
|
+
# @return [Hash]
|
77
|
+
attr_reader :isupport
|
78
|
+
|
79
|
+
# A list of currently enabled capabilities.
|
80
|
+
# It will be updated in response to parsed {IRCSupport::Message::CAP::ACK `CAP ACK`} messages.
|
81
|
+
# @return [Array]
|
82
|
+
attr_reader :capabilities
|
83
|
+
|
84
|
+
# @return [IRCSupport::Parser]
|
85
|
+
def initialize
|
86
|
+
@isupport = @@default_isupport
|
87
|
+
@capabilities = []
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param [String] line An IRC protocol line you wish to decompose.
|
91
|
+
# @return [Hash] A decomposed IRC protocol line with 3 keys:
|
92
|
+
# `command`, the IRC command; `prefix`, the prefix to the
|
93
|
+
# command, if any; `args`, an array of any arguments to the command
|
94
|
+
def decompose_line(line)
|
95
|
+
if line =~ @@irc_line
|
96
|
+
c = $~
|
97
|
+
elems = {}
|
98
|
+
elems[:prefix] = c[:prefix] if c[:prefix]
|
99
|
+
elems[:command] = c[:command].upcase
|
100
|
+
elems[:args] = []
|
101
|
+
elems[:args].concat c[:args].split(@@space) if c[:args]
|
102
|
+
elems[:args] << c[:trailing_arg] if c[:trailing_arg]
|
103
|
+
else
|
104
|
+
raise ArgumentError, "Line is not IRC protocol: #{line}"
|
105
|
+
end
|
106
|
+
|
107
|
+
return elems
|
108
|
+
end
|
109
|
+
|
110
|
+
# @param [Hash] elems The attributes of the message (as returned
|
111
|
+
# by {#decompose_line}).
|
112
|
+
# @return [String] An IRC protocol line.
|
113
|
+
def compose_line(elems)
|
114
|
+
line = ''
|
115
|
+
line << ":#{elems[:prefix]} " if elems[:prefix]
|
116
|
+
if !elems[:command]
|
117
|
+
raise ArgumentError, "You must specify a command"
|
118
|
+
end
|
119
|
+
line << elems[:command]
|
120
|
+
|
121
|
+
if elems[:args]
|
122
|
+
elems[:args].each_with_index do |arg, idx|
|
123
|
+
line << ' '
|
124
|
+
if idx != elems[:args].count-1 and arg.match(@@space)
|
125
|
+
raise ArgumentError, "Only the last argument may contain spaces"
|
126
|
+
end
|
127
|
+
if idx == elems[:args].count-1
|
128
|
+
line << ':' if arg.match(@@space)
|
129
|
+
end
|
130
|
+
line << arg
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
return line
|
135
|
+
end
|
136
|
+
|
137
|
+
# @param [String] line An IRC protocol line.
|
138
|
+
# @return [IRCSupport::Message] A parsed message object.
|
139
|
+
def parse(line)
|
140
|
+
elems = decompose_line(line)
|
141
|
+
elems[:isupport] = @isupport
|
142
|
+
elems[:capabilities] = @capabilities
|
143
|
+
|
144
|
+
if elems[:command] =~ /^(PRIVMSG|NOTICE)$/ && elems[:args][1] =~ /\x01/
|
145
|
+
return handle_ctcp_message(elems)
|
146
|
+
end
|
147
|
+
|
148
|
+
if elems[:command] =~ /^\d{3}$/
|
149
|
+
msg_class = "Numeric"
|
150
|
+
elsif elems[:command] == "MODE"
|
151
|
+
if @isupport['CHANTYPES'].include? elems[:args][0][0]
|
152
|
+
msg_class = "ChannelModeChange"
|
153
|
+
else
|
154
|
+
msg_class = "UserModeChange"
|
155
|
+
end
|
156
|
+
elsif elems[:command] == "NOTICE" && (!elems[:prefix] || elems[:prefix] !~ /!/)
|
157
|
+
msg_class = "ServerNotice"
|
158
|
+
elsif elems[:command] =~ /^(PRIVMSG|NOTICE)$/
|
159
|
+
msg_class = "Message"
|
160
|
+
elems[:is_notice] = true if elems[:command] == "NOTICE"
|
161
|
+
if @isupport['CHANTYPES'].include? elems[:args][0][0]
|
162
|
+
elems[:is_public] = true
|
163
|
+
end
|
164
|
+
if @capabilities.include?('identify-msg')
|
165
|
+
elems[:args][1], elems[:identified] = split_idmsg(elems[:args][1])
|
166
|
+
end
|
167
|
+
elsif elems[:command] == "CAP" && %w{LS LIST ACK}.include?(elems[:args][0])
|
168
|
+
msg_class = "CAP::#{elems[:args][0]}"
|
169
|
+
else
|
170
|
+
msg_class = elems[:command]
|
171
|
+
end
|
172
|
+
|
173
|
+
begin
|
174
|
+
if msg_class == "Numeric"
|
175
|
+
begin
|
176
|
+
msg_const = constantize("IRCSupport::Message::Numeric#{elems[:command]}")
|
177
|
+
rescue
|
178
|
+
msg_const = constantize("IRCSupport::Message::#{msg_class}")
|
179
|
+
end
|
180
|
+
else
|
181
|
+
begin
|
182
|
+
msg_const = constantize("IRCSupport::Message::#{msg_class}")
|
183
|
+
rescue
|
184
|
+
msg_const = constantize("IRCSupport::Message::#{msg_class.capitalize}")
|
185
|
+
end
|
186
|
+
end
|
187
|
+
rescue
|
188
|
+
msg_const = constantize("IRCSupport::Message")
|
189
|
+
end
|
190
|
+
|
191
|
+
message = msg_const.new(elems)
|
192
|
+
|
193
|
+
if message.type == '005'
|
194
|
+
@isupport.merge! message.isupport
|
195
|
+
elsif message.type == 'cap_ack'
|
196
|
+
message.capabilities.each do |capability, options|
|
197
|
+
if options.include?(:disable)
|
198
|
+
@capabilities = @capabilities - [capability]
|
199
|
+
elsif options.include?(:enable)
|
200
|
+
@capabilities = @capabilities + [capability]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
return message
|
206
|
+
end
|
207
|
+
|
208
|
+
# @param [String] type The CTCP type.
|
209
|
+
# @param [String] message The text of the CTCP message.
|
210
|
+
# @return [String] A CTCP-quoted message.
|
211
|
+
def ctcp_quote(type, message)
|
212
|
+
line = low_quote(message)
|
213
|
+
line.gsub!(/\x01/, '\a')
|
214
|
+
return "\x01#{type} #{line}\x01"
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
# from ActiveSupport
|
220
|
+
def constantize(camel_cased_word)
|
221
|
+
names = camel_cased_word.split('::')
|
222
|
+
names.shift if names.empty? || names.first.empty?
|
223
|
+
|
224
|
+
constant = Object
|
225
|
+
names.each do |name|
|
226
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
227
|
+
end
|
228
|
+
constant
|
229
|
+
end
|
230
|
+
|
231
|
+
def split_idmsg(line)
|
232
|
+
identified, line = line.split(//, 2)
|
233
|
+
identified = identified == '+' ? true : false
|
234
|
+
return line, identified
|
235
|
+
end
|
236
|
+
|
237
|
+
def handle_ctcp_message(elems)
|
238
|
+
ctcp_type = elems[:command] == 'PRIVMSG' ? 'CTCP' : 'CTCPReply'
|
239
|
+
ctcps, texts = ctcp_dequote(elems[:args][1])
|
240
|
+
|
241
|
+
# We only process the first CTCP, ignoring extra CTCPs and any
|
242
|
+
# non-CTCPs. Those who send anything in addition to that first CTCP
|
243
|
+
# are probably up to no good (e.g. trying to flood a bot by having it
|
244
|
+
# reply to 20 CTCP VERSIONs at a time).
|
245
|
+
ctcp = ctcps.first
|
246
|
+
|
247
|
+
if @capabilities.include?('identify-msg') && ctcp =~ /^.ACTION/
|
248
|
+
ctcp, elems[:identified] = split_idmsg(ctcp)
|
249
|
+
end
|
250
|
+
|
251
|
+
if ctcp !~ /^(\w+)(?: (.*))?/
|
252
|
+
warn "Received malformed CTCP from #{elems[:prefix]}: #{ctcp}"
|
253
|
+
return
|
254
|
+
end
|
255
|
+
ctcp_name, ctcp_args = $~.captures
|
256
|
+
|
257
|
+
if ctcp_name == 'DCC'
|
258
|
+
if ctcp_args !~ /^(\w+) +(.+)/
|
259
|
+
warn "Received malformed DCC request from #{elems[:prefix]}: #{ctcp}"
|
260
|
+
return
|
261
|
+
end
|
262
|
+
dcc_name, dcc_args = $~.captures
|
263
|
+
elems[:args][1] = dcc_args
|
264
|
+
elems[:dcc_type] = dcc_name
|
265
|
+
|
266
|
+
begin
|
267
|
+
message_class = constantize("IRCSupport::Message::DCC::" + dcc_name.capitalize)
|
268
|
+
rescue
|
269
|
+
message_class = constantize("IRCSupport::Message::DCC")
|
270
|
+
end
|
271
|
+
|
272
|
+
return message_class.new(elems)
|
273
|
+
else
|
274
|
+
elems[:args][1] = ctcp_args || ''
|
275
|
+
|
276
|
+
if @isupport['CHANTYPES'].include? elems[:args][0][0]
|
277
|
+
elems[:is_public] = true
|
278
|
+
end
|
279
|
+
|
280
|
+
# treat CTCP ACTIONs as normal messages with a special attribute
|
281
|
+
if ctcp_name == 'ACTION'
|
282
|
+
elems[:is_action] = true
|
283
|
+
return IRCSupport::Message::Message.new(elems)
|
284
|
+
end
|
285
|
+
|
286
|
+
begin
|
287
|
+
message_class = constantize("IRCSupport::Message::#{ctcp_type}_" + ctcp_name.capitalize)
|
288
|
+
rescue
|
289
|
+
message_class = constantize("IRCSupport::Message::#{ctcp_type}")
|
290
|
+
end
|
291
|
+
|
292
|
+
elems[:ctcp_type] = ctcp_name
|
293
|
+
return message_class.new(elems)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def ctcp_dequote(line)
|
298
|
+
line = low_dequote(line)
|
299
|
+
|
300
|
+
# filter misplaced \x01 before processing
|
301
|
+
if line.count("\x01") % 2 != 0
|
302
|
+
line[line.rindex("\x01")] = '\a'
|
303
|
+
end
|
304
|
+
|
305
|
+
return if line !~ /\x01/
|
306
|
+
|
307
|
+
chunks = line.split(/\x01/)
|
308
|
+
chunks.shift if chunks.first.empty?
|
309
|
+
|
310
|
+
chunks.each do |chunk|
|
311
|
+
# Dequote unnecessarily quoted chars, and convert escaped \'s and ^A's.
|
312
|
+
chunk.gsub!(/\\([^\\a])/, "\\1")
|
313
|
+
chunk.gsub!(/\\\\/, "\\")
|
314
|
+
chunk.gsub!(/\\a/, "\x01")
|
315
|
+
end
|
316
|
+
|
317
|
+
ctcp, text = [], []
|
318
|
+
|
319
|
+
# If the line begins with a control-A, the first chunk is a CTCP
|
320
|
+
# line. Otherwise, it starts with text and alternates with CTCP
|
321
|
+
# lines. Really stupid protocol.
|
322
|
+
ctcp << chunks.shift if line =~ /^\x01/
|
323
|
+
|
324
|
+
while not chunks.empty?
|
325
|
+
text << chunks.shift
|
326
|
+
ctcp << chunks.shift if not chunks.empty?
|
327
|
+
end
|
328
|
+
|
329
|
+
return ctcp, text
|
330
|
+
end
|
331
|
+
|
332
|
+
def low_quote(line)
|
333
|
+
return line.sub(@@low_quote_from, @@low_quote_to)
|
334
|
+
end
|
335
|
+
|
336
|
+
def low_dequote(line)
|
337
|
+
return line.sub(@@low_dequote_from, @@low_dequote_to)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|