| Class | Irc::Client |
| In: |
lib/rbot/rfc2812.rb
|
| Parent: | Object |
# File lib/rbot/rfc2812.rb, line 961
961: def initialize
962: @server = Server.new # The Server
963: @user = @server.user("*!*@*") # The User representing the client on this Server
964:
965: @handlers = Hash.new
966:
967: # This is used by some messages to build lists of users that
968: # will be delegated when the ENDOF... message is received
969: @tmpusers = []
970:
971: # Same as above, just for bans
972: @tmpbans = []
973: end
| key: | server event to handle |
| value: | proc object called when event occurs |
set a handler for a server event
TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
| welcome: | server welcome message on connect |
| yourhost: | your host details (on connection) |
| created: | when the server was started |
| isupport: | information about what this server supports |
| ping: | server pings you (default handler returns a pong) |
| nicktaken: | you tried to change nick to one that‘s in use |
| badnick: | you tried to change nick to one that‘s invalid |
| topic: | someone changed the topic of a channel |
| topicinfo: | on joining a channel or asking for the topic, tells you who set it and when |
| names: | server sends list of channel members when you join |
| motd: | server message of the day |
| privmsg: | privmsg, the core of IRC, a message to you from someone |
| public: | optionally instead of getting privmsg you can hook to only the public ones… |
| msg: | or only the private ones, or both |
| kick: | someone got kicked from a channel |
| part: | someone left a channel |
| quit: | someone quit IRC |
| join: | someone joined a channel |
| changetopic: | the topic of a channel changed |
| invite: | you are invited to a channel |
| nick: | someone changed their nick |
| mode: | a mode change |
| notice: | someone sends you a notice |
| unknown: | any other message not handled by the above |
# File lib/rbot/rfc2812.rb, line 1015
1015: def []=(key, value)
1016: @handlers[key] = value
1017: end
| key: | event name |
remove a handler for a server event
# File lib/rbot/rfc2812.rb, line 1021
1021: def deletehandler(key)
1022: @handlers.delete(key)
1023: end
takes a server string, checks for PING, PRIVMSG, NOTIFY, etc, and parses numeric server replies, calling the appropriate handler for each, and sending it a hash containing the data from the server
# File lib/rbot/rfc2812.rb, line 1028
1028: def process(serverstring)
1029: data = Hash.new
1030: data[:serverstring] = serverstring
1031:
1032: unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/
1033: raise "Unparseable Server Message!!!: #{serverstring.inspect}"
1034: end
1035:
1036: prefix, command, params = $2, $3, $5
1037:
1038: if prefix != nil
1039: # Most servers will send a full nick!user@host prefix for
1040: # messages from users. Therefore, when the prefix doesn't match this
1041: # syntax it's usually the server hostname.
1042: #
1043: # This is not always true, though, since some servers do not send a
1044: # full hostmask for user messages.
1045: #
1046: if prefix =~ /^#{Regexp::Irc::BANG_AT}$/
1047: data[:source] = @server.user(prefix)
1048: else
1049: if @server.hostname
1050: if @server.hostname != prefix
1051: # TODO do we want to be able to differentiate messages that are passed on to us from /other/ servers?
1052: debug "Origin #{prefix} for message\n\t#{serverstring.inspect}\nis neither a user hostmask nor the server hostname\nI'll pretend that it's from the server anyway"
1053: data[:source] = @server
1054: else
1055: data[:source] = @server
1056: end
1057: else
1058: @server.instance_variable_set(:@hostname, prefix)
1059: data[:source] = @server
1060: end
1061: end
1062: end
1063:
1064: # split parameters in an array
1065: argv = []
1066: params.scan(/(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params
1067:
1068: if command =~ /^(\d+)$/ # Numeric replies
1069: data[:target] = argv[0]
1070: # A numeric reply /should/ be directed at the client, except when we're connecting with a used nick, in which case
1071: # it's directed at '*'
1072: not_us = !([@user.nick, '*'].include?(data[:target]))
1073: if not_us
1074: warning "Server reply #{serverstring.inspect} directed at #{data[:target]} instead of client (#{@user.nick})"
1075: end
1076:
1077: num=command.to_i
1078: case num
1079: when RPL_WELCOME
1080: data[:message] = argv[1]
1081: # "Welcome to the Internet Relay Network
1082: # <nick>!<user>@<host>"
1083: if not_us
1084: warning "Server thinks client (#{@user.inspect}) has a different nick"
1085: @user.nick = data[:target]
1086: end
1087: if data[:message] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/
1088: nick = $1
1089: user = $2
1090: host = $3
1091: warning "Welcome message nick mismatch (#{nick} vs #{data[:target]})" if nick != data[:target]
1092: @user.user = user if user
1093: @user.host = host if host
1094: end
1095: handle(:welcome, data)
1096: when RPL_YOURHOST
1097: # "Your host is <servername>, running version <ver>"
1098: data[:message] = argv[1]
1099: handle(:yourhost, data)
1100: when RPL_CREATED
1101: # "This server was created <date>"
1102: data[:message] = argv[1]
1103: handle(:created, data)
1104: when RPL_MYINFO
1105: # "<servername> <version> <available user modes>
1106: # <available channel modes>"
1107: @server.parse_my_info(params.split(' ', 2).last)
1108: data[:servername] = @server.hostname
1109: data[:version] = @server.version
1110: data[:usermodes] = @server.usermodes
1111: data[:chanmodes] = @server.chanmodes
1112: handle(:myinfo, data)
1113: when RPL_ISUPPORT
1114: # "PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server"
1115: # "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63
1116: # TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=#
1117: # PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer :are available
1118: # on this server"
1119: #
1120: @server.parse_isupport(argv[1..-2].join(' '))
1121: handle(:isupport, data)
1122: when ERR_NICKNAMEINUSE
1123: # "* <nick> :Nickname is already in use"
1124: data[:nick] = argv[1]
1125: data[:message] = argv[2]
1126: handle(:nicktaken, data)
1127: when ERR_ERRONEUSNICKNAME
1128: # "* <nick> :Erroneous nickname"
1129: data[:nick] = argv[1]
1130: data[:message] = argv[2]
1131: handle(:badnick, data)
1132: when RPL_TOPIC
1133: data[:channel] = @server.channel(argv[1])
1134: data[:topic] = argv[2]
1135: data[:channel].topic.text = data[:topic]
1136:
1137: handle(:topic, data)
1138: when RPL_TOPIC_INFO
1139: data[:nick] = @server.user(argv[0])
1140: data[:channel] = @server.channel(argv[1])
1141:
1142: # This must not be an IRC::User because it might not be an actual User,
1143: # and we risk overwriting valid User data
1144: data[:source] = argv[2].to_irc_netmask(:server => @server)
1145:
1146: data[:time] = Time.at(argv[3].to_i)
1147:
1148: data[:channel].topic.set_by = data[:source]
1149: data[:channel].topic.set_on = data[:time]
1150:
1151: handle(:topicinfo, data)
1152: when RPL_NAMREPLY
1153: # "( "=" / "*" / "@" ) <channel>
1154: # :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
1155: # - "@" is used for secret channels, "*" for private
1156: # channels, and "=" for others (public channels).
1157: data[:channeltype] = argv[1]
1158: data[:channel] = chan = @server.channel(argv[2])
1159:
1160: users = []
1161: argv[3].scan(/\S+/).each { |u|
1162: # FIXME beware of servers that allow multiple prefixes
1163: if(u =~ /^([#{@server.supports[:prefix][:prefixes].join}])?(.*)$/)
1164: umode = $1
1165: user = $2
1166: users << [user, umode]
1167: end
1168: }
1169:
1170: users.each { |ar|
1171: u = @server.user(ar[0])
1172: chan.add_user(u, :silent => true)
1173: debug "Adding user #{u}"
1174: if ar[1]
1175: ms = @server.mode_for_prefix(ar[1].to_sym)
1176: debug "\twith mode #{ar[1]} (#{ms})"
1177: chan.mode[ms].set(u)
1178: end
1179: }
1180: @tmpusers += users
1181: when RPL_ENDOFNAMES
1182: data[:channel] = @server.channel(argv[1])
1183: data[:users] = @tmpusers
1184: handle(:names, data)
1185: @tmpusers = Array.new
1186: when RPL_BANLIST
1187: data[:channel] = @server.channel(argv[1])
1188: data[:mask] = argv[2]
1189: data[:by] = argv[3]
1190: data[:at] = argv[4]
1191: @tmpbans << data
1192: when RPL_ENDOFBANLIST
1193: data[:channel] = @server.channel(argv[1])
1194: data[:bans] = @tmpbans
1195: handle(:banlist, data)
1196: @tmpbans = Array.new
1197: when RPL_LUSERCLIENT
1198: # ":There are <integer> users and <integer>
1199: # services on <integer> servers"
1200: data[:message] = argv[1]
1201: handle(:luserclient, data)
1202: when RPL_LUSEROP
1203: # "<integer> :operator(s) online"
1204: data[:ops] = argv[1].to_i
1205: handle(:luserop, data)
1206: when RPL_LUSERUNKNOWN
1207: # "<integer> :unknown connection(s)"
1208: data[:unknown] = argv[1].to_i
1209: handle(:luserunknown, data)
1210: when RPL_LUSERCHANNELS
1211: # "<integer> :channels formed"
1212: data[:channels] = argv[1].to_i
1213: handle(:luserchannels, data)
1214: when RPL_LUSERME
1215: # ":I have <integer> clients and <integer> servers"
1216: data[:message] = argv[1]
1217: handle(:luserme, data)
1218: when ERR_NOMOTD
1219: # ":MOTD File is missing"
1220: data[:message] = argv[1]
1221: handle(:motd_missing, data)
1222: when RPL_LOCALUSERS
1223: # ":Current local users: 3 Max: 4"
1224: data[:message] = argv[1]
1225: handle(:localusers, data)
1226: when RPL_GLOBALUSERS
1227: # ":Current global users: 3 Max: 4"
1228: data[:message] = argv[1]
1229: handle(:globalusers, data)
1230: when RPL_STATSCONN
1231: # ":Highest connection count: 4 (4 clients) (251 since server was
1232: # (re)started)"
1233: data[:message] = argv[1]
1234: handle(:statsconn, data)
1235: when RPL_MOTDSTART
1236: # "<nick> :- <server> Message of the Day -"
1237: if argv[1] =~ /^-\s+(\S+)\s/
1238: server = $1
1239: else
1240: warning "Server doesn't have an RFC compliant MOTD start."
1241: end
1242: @motd = ""
1243: when RPL_MOTD
1244: if(argv[1] =~ /^-\s+(.*)$/)
1245: @motd << $1
1246: @motd << "\n"
1247: end
1248: when RPL_ENDOFMOTD
1249: data[:motd] = @motd
1250: handle(:motd, data)
1251: when RPL_DATASTR
1252: data[:text] = argv[1]
1253: handle(:datastr, data)
1254: when RPL_AWAY
1255: data[:nick] = user = @server.user(argv[1])
1256: data[:message] = argv[-1]
1257: user.away = data[:message]
1258: handle(:away, data)
1259: when RPL_WHOREPLY
1260: data[:channel] = channel = @server.channel(argv[1])
1261: data[:user] = argv[2]
1262: data[:host] = argv[3]
1263: data[:userserver] = argv[4]
1264: data[:nick] = user = @server.user(argv[5])
1265: if argv[6] =~ /^(H|G)(\*)?(.*)?$/
1266: data[:away] = ($1 == 'G')
1267: data[:ircop] = $2
1268: data[:modes] = $3.scan(/./).map { |mode|
1269: m = @server.supports[:prefix][:prefixes].index(mode.to_sym)
1270: @server.supports[:prefix][:modes][m]
1271: } rescue []
1272: else
1273: warning "Strange WHO reply: #{serverstring.inspect}"
1274: end
1275: data[:hopcount], data[:real_name] = argv[7].split(" ", 2)
1276:
1277: user.user = data[:user]
1278: user.host = data[:host]
1279: user.away = data[:away] # FIXME doesn't provide the actual message
1280: # TODO ircop status
1281: # TODO userserver
1282: # TODO hopcount
1283: user.real_name = data[:real_name]
1284:
1285: channel.add_user(user, :silent=>true)
1286: data[:modes].map { |mode|
1287: channel.mode[mode].set(user)
1288: }
1289:
1290: handle(:who, data)
1291: when RPL_ENDOFWHO
1292: handle(:eowho, data)
1293: when RPL_WHOISUSER
1294: @whois ||= Hash.new
1295: @whois[:nick] = argv[1]
1296: @whois[:user] = argv[2]
1297: @whois[:host] = argv[3]
1298: @whois[:real_name] = argv[-1]
1299:
1300: user = @server.user(@whois[:nick])
1301: user.user = @whois[:user]
1302: user.host = @whois[:host]
1303: user.real_name = @whois[:real_name]
1304: when RPL_WHOISSERVER
1305: @whois ||= Hash.new
1306: @whois[:nick] = argv[1]
1307: @whois[:server] = argv[2]
1308: @whois[:server_info] = argv[-1]
1309: # TODO update user info
1310: when RPL_WHOISOPERATOR
1311: @whois ||= Hash.new
1312: @whois[:nick] = argv[1]
1313: @whois[:operator] = argv[-1]
1314: # TODO update user info
1315: when RPL_WHOISIDLE
1316: @whois ||= Hash.new
1317: @whois[:nick] = argv[1]
1318: user = @server.user(@whois[:nick])
1319: @whois[:idle] = argv[2].to_i
1320: user.idle_since = Time.now - @whois[:idle]
1321: if argv[-1] == 'seconds idle, signon time'
1322: @whois[:signon] = Time.at(argv[3].to_i)
1323: user.signon = @whois[:signon]
1324: end
1325: when RPL_ENDOFWHOIS
1326: @whois ||= Hash.new
1327: @whois[:nick] = argv[1]
1328: data[:whois] = @whois.dup
1329: @whois.clear
1330: handle(:whois, data)
1331: when RPL_WHOISCHANNELS
1332: @whois ||= Hash.new
1333: @whois[:nick] = argv[1]
1334: @whois[:channels] ||= []
1335: user = @server.user(@whois[:nick])
1336: argv[-1].split.each do |prechan|
1337: pfx = prechan.scan(/[#{@server.supports[:prefix][:prefixes].join}]/)
1338: modes = pfx.map { |p| @server.mode_for_prefix p }
1339: chan = prechan[pfx.length..prechan.length]
1340:
1341: channel = @server.channel(chan)
1342: channel.add_user(user, :silent => true)
1343: modes.map { |mode| channel.mode[mode].set(user) }
1344:
1345: @whois[:channels] << [chan, modes]
1346: end
1347: when RPL_CHANNELMODEIS
1348: parse_mode(serverstring, argv[1..-1], data)
1349: handle(:mode, data)
1350: when RPL_CREATIONTIME
1351: data[:channel] = @server.channel(argv[1])
1352: data[:time] = Time.at(argv[2].to_i)
1353: data[:channel].creation_time=data[:time]
1354: handle(:creationtime, data)
1355: when RPL_CHANNEL_URL
1356: data[:channel] = @server.channel(argv[1])
1357: data[:url] = argv[2]
1358: data[:channel].url=data[:url].dup
1359: handle(:channel_url, data)
1360: when ERR_NOSUCHNICK
1361: data[:target] = argv[1]
1362: data[:message] = argv[2]
1363: handle(:nosuchtarget, data)
1364: if user = @server.get_user(data[:target])
1365: @server.delete_user(user)
1366: end
1367: when ERR_NOSUCHCHANNEL
1368: data[:target] = argv[1]
1369: data[:message] = argv[2]
1370: handle(:nosuchtarget, data)
1371: if channel = @server.get_channel(data[:target])
1372: @server.delete_channel(channel)
1373: end
1374: else
1375: warning "Unknown message #{serverstring.inspect}"
1376: handle(:unknown, data)
1377: end
1378: return # We've processed the numeric reply
1379: end
1380:
1381: # Otherwise, the command should be a single word
1382: case command.to_sym
1383: when :PING
1384: data[:pingid] = argv[0]
1385: handle(:ping, data)
1386: when :PONG
1387: data[:pingid] = argv[0]
1388: handle(:pong, data)
1389: when :PRIVMSG
1390: # you can either bind to 'PRIVMSG', to get every one and
1391: # parse it yourself, or you can bind to 'MSG', 'PUBLIC',
1392: # etc and get it all nicely split up for you.
1393:
1394: begin
1395: data[:target] = @server.user_or_channel(argv[0])
1396: rescue
1397: # The previous may fail e.g. when the target is a server or something
1398: # like that (e.g. $<mask>). In any of these cases, we just use the
1399: # String as a target
1400: # FIXME we probably want to explicitly check for the #<mask> $<mask>
1401: data[:target] = argv[0]
1402: end
1403: data[:message] = argv[1]
1404: handle(:privmsg, data)
1405:
1406: # Now we split it
1407: if data[:target].kind_of?(Channel)
1408: handle(:public, data)
1409: else
1410: handle(:msg, data)
1411: end
1412: when :NOTICE
1413: begin
1414: data[:target] = @server.user_or_channel(argv[0])
1415: rescue
1416: # The previous may fail e.g. when the target is a server or something
1417: # like that (e.g. $<mask>). In any of these cases, we just use the
1418: # String as a target
1419: # FIXME we probably want to explicitly check for the #<mask> $<mask>
1420: data[:target] = argv[0]
1421: end
1422: data[:message] = argv[1]
1423: case data[:source]
1424: when User
1425: handle(:notice, data)
1426: else
1427: # "server notice" (not from user, noone to reply to)
1428: handle(:snotice, data)
1429: end
1430: when :KICK
1431: data[:channel] = @server.channel(argv[0])
1432: data[:target] = @server.user(argv[1])
1433: data[:message] = argv[2]
1434:
1435: @server.delete_user_from_channel(data[:target], data[:channel])
1436: if data[:target] == @user
1437: @server.delete_channel(data[:channel])
1438: end
1439:
1440: handle(:kick, data)
1441: when :PART
1442: data[:channel] = @server.channel(argv[0])
1443: data[:message] = argv[1]
1444:
1445: @server.delete_user_from_channel(data[:source], data[:channel])
1446: if data[:source] == @user
1447: @server.delete_channel(data[:channel])
1448: end
1449:
1450: handle(:part, data)
1451: when :QUIT
1452: data[:message] = argv[0]
1453: data[:was_on] = @server.channels.inject(ChannelList.new) { |list, ch|
1454: list << ch if ch.has_user?(data[:source])
1455: list
1456: }
1457:
1458: @server.delete_user(data[:source])
1459:
1460: handle(:quit, data)
1461: when :JOIN
1462: data[:channel] = @server.channel(argv[0])
1463: data[:channel].add_user(data[:source])
1464:
1465: handle(:join, data)
1466: when :TOPIC
1467: data[:channel] = @server.channel(argv[0])
1468: data[:topic] = Channel::Topic.new(argv[1], data[:source], Time.new)
1469: data[:channel].topic.replace(data[:topic])
1470:
1471: handle(:changetopic, data)
1472: when :INVITE
1473: data[:target] = @server.user(argv[0])
1474: data[:channel] = @server.channel(argv[1])
1475:
1476: handle(:invite, data)
1477: when :NICK
1478: data[:is_on] = @server.channels.inject(ChannelList.new) { |list, ch|
1479: list << ch if ch.has_user?(data[:source])
1480: list
1481: }
1482:
1483: data[:newnick] = argv[0]
1484: data[:oldnick] = data[:source].nick.dup
1485: data[:source].nick = data[:newnick]
1486:
1487: debug "#{data[:oldnick]} (now #{data[:newnick]}) was on #{data[:is_on].join(', ')}"
1488:
1489: handle(:nick, data)
1490: when :MODE
1491: parse_mode(serverstring, argv, data)
1492: handle(:mode, data)
1493: when :ERROR
1494: data[:message] = argv[1]
1495: handle(:error, data)
1496: else
1497: warning "Unknown message #{serverstring.inspect}"
1498: handle(:unknown, data)
1499: end
1500: end
| key: | server event name |
| data: | hash containing data about the event, passed to the proc |
call client‘s proc for an event, if they set one as a handler
# File lib/rbot/rfc2812.rb, line 1507
1507: def handle(key, data)
1508: if(@handlers.has_key?(key))
1509: @handlers[key].call(data)
1510: end
1511: end
RPL_CHANNELMODEIS MODE ([+-]<modes> (<params>)*)* When a MODE message is received by a server, Type C will have parameters too, so we must be able to consume parameters for all but Type D modes
# File lib/rbot/rfc2812.rb, line 1519
1519: def parse_mode(serverstring, argv, data)
1520: data[:target] = @server.user_or_channel(argv[0])
1521: data[:modestring] = argv[1..-1].join(" ")
1522: # data[:modes] is an array where each element
1523: # is an array with two elements, the first of which
1524: # is either :set or :reset, and the second symbol
1525: # is the mode letter. An optional third element
1526: # is present e.g. for channel modes that need
1527: # a parameter
1528: data[:modes] = []
1529: case data[:target]
1530: when User
1531: # User modes aren't currently handled internally,
1532: # but we still parse them and delegate to the client
1533: warning "Unhandled user mode message '#{serverstring}'"
1534: argv[1..-1].each { |arg|
1535: setting = arg[0].chr
1536: if "+-".include?(setting)
1537: setting = setting == "+" ? :set : :reset
1538: arg[1..-1].each_byte { |b|
1539: m = b.chr.intern
1540: data[:modes] << [setting, m]
1541: }
1542: else
1543: # Although typically User modes don't take an argument,
1544: # this is not true for all modes on all servers. Since
1545: # we have no knowledge of which modes take parameters
1546: # and which don't we just assign it to the last
1547: # mode. This is not going to do strange things often,
1548: # as usually User modes are only set one at a time
1549: warning "Unhandled user mode parameter #{arg} found"
1550: data[:modes].last << arg
1551: end
1552: }
1553: when Channel
1554: # array of indices in data[:modes] where parameters
1555: # are needed
1556: who_wants_params = []
1557:
1558: modes = argv[1..-1].dup
1559: debug modes
1560: getting_args = false
1561: while arg = modes.shift
1562: debug arg
1563: if getting_args
1564: # getting args for previously set modes
1565: idx = who_wants_params.shift
1566: if idx.nil?
1567: warning "Oops, problems parsing #{serverstring.inspect}"
1568: break
1569: end
1570: data[:modes][idx] << arg
1571: getting_args = false if who_wants_params.empty?
1572: else
1573: debug @server.supports[:chanmodes]
1574: setting = :set
1575: arg.each_byte do |c|
1576: m = c.chr.intern
1577: case m
1578: when :+
1579: setting = :set
1580: when :-
1581: setting = :reset
1582: else
1583: data[:modes] << [setting, m]
1584: case m
1585: when *@server.supports[:chanmodes][:typea]
1586: who_wants_params << data[:modes].length - 1
1587: when *@server.supports[:chanmodes][:typeb]
1588: who_wants_params << data[:modes].length - 1
1589: when *@server.supports[:chanmodes][:typec]
1590: if setting == :set
1591: who_wants_params << data[:modes].length - 1
1592: end
1593: when *@server.supports[:chanmodes][:typed]
1594: # Nothing to do
1595: when *@server.supports[:prefix][:modes]
1596: who_wants_params << data[:modes].length - 1
1597: else
1598: warning "Ignoring unknown mode #{m} in #{serverstring.inspect}"
1599: data[:modes].pop
1600: end
1601: end
1602: end
1603: getting_args = true unless who_wants_params.empty?
1604: end
1605: end
1606: unless who_wants_params.empty?
1607: warning "Unhandled malformed modeline #{data[:modestring]} (unexpected empty arguments)"
1608: return
1609: end
1610:
1611: data[:modes].each { |mode|
1612: set, key, val = mode
1613: if val
1614: data[:target].mode[key].send(set, val)
1615: else
1616: data[:target].mode[key].send(set)
1617: end
1618: }
1619: else
1620: warning "Ignoring #{data[:modestring]} for unrecognized target #{argv[0]} (#{data[:target].inspect})"
1621: end
1622: end