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
         |