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