Guest User

Untitled

a guest
Feb 19th, 2018
246
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.80 KB | None | 0 0
  1. =begin
  2.  
  3. = rice - Ruby Irc interfaCE
  4.  
  5. $Id: irc.rb,v 1.9 2001/06/13 10:22:24 akira Exp $
  6.  
  7. Copyright (c) 2001 akira yamada <akira@ruby-lang.org>
  8. You can redistribute it and/or modify it under the same term as Ruby.
  9.  
  10. =end
  11.  
  12. require 'socket'
  13. require 'thread'
  14. require 'monitor'
  15.  
  16. module RICE
  17. class Error < StandardError; end
  18. class InvalidMessage < Error; end
  19. class UnknownCommand < Error; end
  20.  
  21. =begin
  22.  
  23. == RICE::Connection
  24.  
  25. =end
  26.  
  27. class Connection
  28. class Error < StandardError; end
  29. class Closed < Error; end
  30.  
  31. =begin
  32.  
  33. --- RICE::Connection::new
  34.  
  35. =end
  36.  
  37. def initialize(server, port, eol = "\r\n")
  38. @conn = []
  39. @conn.extend(MonitorMixin)
  40.  
  41. self.server = server
  42. self.port = port
  43. self.eol = eol
  44.  
  45. @read_q = Queue.new
  46.  
  47. @read_th = Thread.new(@read_q, @eol) do |read_q, eol|
  48. read_thread(read_q, eol)
  49. end
  50.  
  51. @threads = {}
  52. @threads.extend(MonitorMixin)
  53.  
  54. @dispatcher = Thread.new(@read_q) do |read_q|
  55. loop do
  56. x = read_q.pop
  57.  
  58. ths = @threads.synchronize do
  59. @threads.keys
  60. end
  61. ths.each do |th|
  62. if th.status
  63. @threads[th].q.push(x)
  64. else
  65. @threads.delete(th)
  66. end
  67. end
  68. end # loop
  69.  
  70. end
  71.  
  72. @delay = 0.3
  73. end
  74. attr :delay, true
  75.  
  76. =begin
  77.  
  78. --- RICE::Connection#server=(server)
  79.  
  80. =end
  81.  
  82. def server=(server)
  83. raise RuntimeError,
  84. "Already connected to #{@server}:#{@port}" unless @conn.empty?
  85. @server = server
  86. end
  87.  
  88. =begin
  89.  
  90. --- RICE::Connection#port=(port)
  91.  
  92. =end
  93.  
  94. def port=(port)
  95. raise RuntimeError,
  96. "Already connected to #{@server}:#{@port}" unless @conn.empty?
  97. @port = port
  98. end
  99.  
  100. =begin
  101.  
  102. --- RICE::Connection#eol=(eol)
  103.  
  104. =end
  105.  
  106. def eol=(eol)
  107. raise RuntimeError,
  108. "Already connected to #{@server}:#{@port}" unless @conn.empty?
  109. @eol = eol
  110. end
  111.  
  112. =begin
  113.  
  114. --- RICE::Connection#start(max_retry = 3, retry_wait = 30)
  115.  
  116. =end
  117.  
  118. def start(max_retry = 3, retry_wait = 30)
  119. @client_th = Thread.current # caller thread
  120. if alive?
  121. #sleep retry_wait
  122. return nil
  123. end
  124.  
  125. if block_given?
  126. @main_th = Thread.new do
  127. yield
  128. end
  129. else
  130. @main_th = Thread.new do
  131. begin
  132. Thread.stop
  133. ensure
  134. @read_th.raise(Closed) if @read_th.status
  135. close(true)
  136. @client_th.raise(Closed)
  137. end
  138. end
  139. end
  140.  
  141. begin
  142. open_conn
  143. rescue SystemCallError
  144. max_retry -= 1
  145. if max_retry == 0
  146. raise
  147. end
  148. sleep retry_wait
  149. retry
  150. end
  151.  
  152. @main_th.join
  153. nil
  154. end
  155.  
  156. def open_conn
  157. @conn.synchronize do
  158. @conn[0] = TCPSocket.new(@server, @port)
  159. end
  160. @conn[0].extend(MonitorMixin)
  161.  
  162. @read_th.run
  163.  
  164. ths = @threads.synchronize do
  165. @threads.keys
  166. end
  167. ths.each do |th|
  168. th.run if th.status && th.stop?
  169. end
  170. end
  171. private :open_conn
  172.  
  173. =begin
  174.  
  175. --- RICE::Connection#regist(raise_on_close, *args) {...}
  176.  
  177. =end
  178.  
  179. USER_THREAD = Struct.new('User_Thread', :q, :raise_on_close)
  180. def regist(raise_on_close = false, *args)
  181. read_q = Queue.new
  182. th = Thread.new(read_q, self, *args) do |read_q, conn, *args|
  183. yield(read_q, conn, *args)
  184. end
  185. @threads.synchronize do
  186. @threads[th] = USER_THREAD.new(read_q, raise_on_close)
  187. end
  188. th
  189. end
  190.  
  191. =begin
  192.  
  193. --- RICE::Connection#unregist(thread)
  194.  
  195. =end
  196.  
  197. def unregist(thread)
  198. th = nil
  199. @threads.synchronize do
  200. th = @threads.delete(th)
  201. end
  202. th.exit
  203. th
  204. end
  205.  
  206. def read_thread(read_q, eol)
  207. begin
  208. read_q.clear
  209. Thread.stop
  210.  
  211. begin
  212. conn = @conn[0]
  213. while l = conn.gets(eol)
  214. # $stderr.print l.inspect if $DEBUG
  215. begin
  216. read_q.push(Message.parse(l))
  217. rescue UnknownCommand
  218. $stderr.print l.inspect if $DEBUG
  219. rescue InvalidMessage
  220. begin
  221. read_q.push(Message.parse(l.sub(/\s*#{eol}\z/o, eol)))
  222. rescue
  223. RAILS_DEFAULT_LOGGER.warn l.inspect
  224. $stderr.print l.inspect if $DEBUG
  225. end
  226. end
  227. end
  228.  
  229. rescue IOError#, SystemCallError
  230. $stderr.print "#{self.inspect}: read_th get error #{$!}" if $DEBUG
  231.  
  232. ensure
  233. raise Closed
  234. end
  235.  
  236. rescue Closed
  237. begin
  238. @main_th.run
  239. rescue Closed
  240. end
  241. retry
  242. end
  243. end
  244. private :read_thread
  245.  
  246. =begin
  247.  
  248. --- RICE::Connection#close(restart = false)
  249.  
  250. =end
  251.  
  252. def close(restart = false)
  253. begin
  254. unless restart
  255. @main_th.exit if @main_th.alive?
  256. @read_th.exit if @read_th.alive?
  257. end
  258.  
  259. conn = nil
  260. @conn.synchronize do
  261. conn = @conn.shift
  262. end
  263. conn.close if conn
  264.  
  265. @threads.synchronize do
  266. @threads.each_key do |th|
  267. if restart
  268. if @threads[th].raise_on_close
  269. if @threads[th].raise_on_close.kind_of?(Exception)
  270. th.raise(@threads[th].raise_on_close)
  271. else
  272. th.raise(Closed)
  273. end
  274. end
  275.  
  276. else
  277. th.exit
  278. end
  279. end
  280. end
  281.  
  282. end
  283. end
  284.  
  285. =begin
  286.  
  287. --- RICE::Connection#alive?
  288.  
  289. =end
  290.  
  291. def alive?
  292. @main_th && @main_th.alive?
  293. end
  294.  
  295. =begin
  296.  
  297. --- RICE::Connection#push(message)
  298.  
  299. =end
  300.  
  301. def push(message)
  302. if @conn[0]
  303. @conn[0].synchronize do
  304. sleep(@delay) if @delay
  305. @conn[0].print message.to_s
  306. end
  307. else
  308. nil
  309. end
  310. end
  311. alias << push
  312. end # Connection
  313.  
  314. =begin
  315.  
  316. == RICE::Message
  317.  
  318. =end
  319.  
  320. class Message
  321. module PATTERN
  322. # letter = %x41-5A / %x61-7A ; A-Z / a-z
  323. # digit = %x30-39 ; 0-9
  324. # hexdigit = digit / "A" / "B" / "C" / "D" / "E" / "F"
  325. # special = %x5B-60 / %x7B-7D
  326. # ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
  327. LETTER = 'A-Za-z'
  328. DIGIT = '\d'
  329. HEXDIGIT = "#{DIGIT}A-Fa-f"
  330. SPECIAL = '\x5B-\x60\x7B-\x7D'
  331.  
  332. # shortname = ( letter / digit ) *( letter / digit / "-" )
  333. # *( letter / digit )
  334. # ; as specified in RFC 1123 [HNAME]
  335. # hostname = shortname *( "." shortname )
  336. # SHORTNAME = "[#{LETTER}#{DIGIT}](?:[-#{LETTER}#{DIGIT}]*[#{LETTER}#{DIGIT}])?"
  337. # allow for - at end of hostname for / notation
  338. SHORTNAME = "[#{LETTER}#{DIGIT}](?:[-#{LETTER}#{DIGIT}]*)?"
  339. # HOSTNAME = "#{SHORTNAME}(?:\\.#{SHORTNAME})*"
  340. # allow for hidden hostnames with / notation
  341. HOSTNAME = "#{SHORTNAME}(?:[./]#{SHORTNAME})*"
  342.  
  343. # servername = hostname
  344. SERVERNAME = HOSTNAME
  345.  
  346. # nickname = ( letter / special ) *8( letter / digit / special / "-" )
  347. NICKNAME = "[#{LETTER}#{SPECIAL}][-#{LETTER}#{DIGIT}#{SPECIAL}]{0,25}"
  348.  
  349. # user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
  350. # ; any octet except NUL, CR, LF, " " and "@"
  351. USER = '[\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x3F\x41-\xFF]+'
  352.  
  353. # ip4addr = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit
  354. IP4ADDR = "[#{DIGIT}]{1,3}(?:\\.[#{DIGIT}]{1,3}){3}"
  355. # ip6addr = 1*hexdigit 7( ":" 1*hexdigit )
  356. # ip6addr =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr
  357. IP6ADDR = "(?:[#{HEXDIGIT}]+(?::[#{HEXDIGIT}]+){7}|0:0:0:0:0:(?:0|FFFF):#{IP4ADDR})"
  358. # hostaddr = ip4addr / ip6addr
  359. HOSTADDR = "(?:#{IP4ADDR}|#{IP6ADDR})"
  360.  
  361. # host = hostname / hostaddr
  362. HOST = "(?:#{HOSTNAME}|#{HOSTADDR})"
  363.  
  364. # prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
  365. PREFIX = "(?:#{NICKNAME}(?:(?:!#{USER})?@#{HOST})?|#{SERVERNAME})"
  366.  
  367. # nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
  368. # ; any octet except NUL, CR, LF, " " and ":"
  369. NOSPCRLFCL = '\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x39\x3B-\xFF'
  370.  
  371. # command = 1*letter / 3digit
  372. COMMAND = "(?:[#{LETTER}]+|[#{DIGIT}]{3})"
  373.  
  374. # SPACE = %x20 ; space character
  375. # middle = nospcrlfcl *( ":" / nospcrlfcl )
  376. # trailing = *( ":" / " " / nospcrlfcl )
  377. # params = *14( SPACE middle ) [ SPACE ":" trailing ]
  378. # =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
  379. MIDDLE = "[#{NOSPCRLFCL}][:#{NOSPCRLFCL}]*"
  380. TRAILING = "[: #{NOSPCRLFCL}]*"
  381. PARAMS = "(?:((?: #{MIDDLE}){0,14})(?: :(#{TRAILING}))?|((?: #{MIDDLE}){14})(?::?)?(#{TRAILING}))"
  382.  
  383. # crlf = %x0D %x0A ; "carriage return" "linefeed"
  384. # message = [ ":" prefix SPACE ] command [ params ] crlf
  385. CRLF = '\x0D\x0A'
  386. MESSAGE = "(?::(#{PREFIX}) )?(#{COMMAND})#{PARAMS}#{CRLF}"
  387.  
  388. CLIENT_PATTERN = /\A#{NICKNAME}(?:(?:!#{USER})?@#{HOST})\z/on
  389. MESSAGE_PATTERN = /^#{MESSAGE}$/on
  390. end # PATTERN
  391.  
  392. =begin
  393.  
  394. --- RICE::Message::parse(str)
  395.  
  396. =end
  397.  
  398. def self.parse(str)
  399. unless PATTERN::MESSAGE_PATTERN =~ str
  400. raise InvalidMessage, "Invalid message"
  401.  
  402. else
  403. prefix = $1
  404. command = $2
  405. if $3 && $3.size > 0
  406. middle = $3
  407. trailer = $4
  408. elsif $5 && $5.size > 0
  409. middle = $5
  410. trailer = $6
  411. elsif $4
  412. params = []
  413. trailer = $4
  414. elsif $6
  415. params = []
  416. trailer = $6
  417. else
  418. params = []
  419. end
  420. end
  421. params ||= middle.split(/ /)[1..-1]
  422. params << trailer if trailer
  423.  
  424. self.build(prefix, command, params)
  425. end
  426.  
  427. =begin
  428.  
  429. --- RICE::Message::build(prefix, command, params)
  430.  
  431. =end
  432.  
  433. def self.build(prefix, command, params)
  434. if Command::Commands.include?(command)
  435. Command::Commands[command].new(prefix, command, params)
  436. elsif Reply::Replies.include?(command)
  437. Reply::Replies[command].new(prefix, command, params)
  438. else
  439. raise UnknownCommand, "unknown command: #{command}"
  440. end
  441. end
  442.  
  443. =begin
  444.  
  445. --- RICE::Message#prefix
  446.  
  447. --- RICE::Message#command
  448.  
  449. --- RICE::Message#params
  450.  
  451. =end
  452.  
  453. def initialize(prefix, command, params)
  454. @prefix = prefix
  455. @command = command
  456. @params = params
  457. end
  458. attr_reader :prefix, :command, :params
  459.  
  460. =begin
  461.  
  462. --- RICE::Message::#to_s
  463.  
  464. =end
  465.  
  466. def to_s
  467. str = ''
  468. if @prefix
  469. str << ':'
  470. str << @prefix
  471. str << ' '
  472. end
  473.  
  474. str << @command
  475.  
  476. if @params
  477. f = false
  478. @params.each do |param|
  479. str << ' '
  480. if !f && (param.size == 0 || /[: ]/ =~ param)
  481. str << ':'
  482. str << param
  483. f = true
  484. else
  485. str << param
  486. end
  487. end
  488. end
  489.  
  490. str << "\x0D\x0A"
  491.  
  492. str
  493. end
  494.  
  495. =begin
  496.  
  497. --- RICE::Message::#to_a
  498.  
  499. =end
  500.  
  501. def to_a
  502. [@prefix, @command, @params]
  503. end
  504.  
  505. def inspect
  506. sprintf('#<%s:0x%x prefix:%s command:%s params:%s>',
  507. self.class, self.object_id, @prefix, @command, @params.inspect)
  508. end
  509.  
  510. end # Message
  511.  
  512. =begin
  513.  
  514. == RICE::Command
  515.  
  516. =end
  517.  
  518. module Command
  519. class Command < Message
  520. end # Command
  521.  
  522. Commands = {}
  523. %w(PASS NICK USER OPER MODE SERVICE QUIT SQUIT
  524. JOIN PART TOPIC NAMES LIST INVITE KICK
  525. PRIVMSG NOTICE MOTD LUSERS VERSION STATS LINKS
  526. TIME CONNECT TRACE ADMIN INFO SERVLIST SQUERY
  527. WHO WHOIS WHOWAS KILL PING PONG ERROR
  528. AWAY REHASH DIE RESTART SUMMON USERS WALLOPS USERHOST ISON
  529. ).each do |cmd|
  530. eval <<E
  531. class #{cmd} < Command
  532. end
  533. Commands['#{cmd}'] = #{cmd}
  534.  
  535. def #{cmd.downcase}(*params)
  536. #{cmd}.new(nil, '#{cmd}', params)
  537. end
  538. module_function :#{cmd.downcase}
  539. E
  540. end
  541.  
  542. # XXX:
  543. class PRIVMSG
  544. def to_s
  545. str = ''
  546. if @prefix
  547. str << ':'
  548. str << @prefix
  549. str << ' '
  550. end
  551.  
  552. str << @command
  553.  
  554. str << ' '
  555. str << @params[0]
  556.  
  557. str << ' :'
  558. str << @params[1..-1].join(' ')
  559.  
  560. str << "\x0D\x0A"
  561. str
  562. end
  563. end
  564. end # Command
  565.  
  566. =begin
  567.  
  568. == RICE::Reply
  569.  
  570. == RICE::CommandResponse
  571.  
  572. == RICE::ErrorReply
  573.  
  574. =end
  575.  
  576. module Reply
  577. class Reply < Message
  578. end
  579.  
  580. class CommandResponse < Reply
  581. end
  582.  
  583. class ErrorReply < Reply
  584. end
  585.  
  586. Replies = {}
  587. %w(001,RPL_WELCOME 002,RPL_YOURHOST 003,RPL_CREATED
  588. 004,RPL_MYINFO 005,RPL_BOUNCE
  589. 302,RPL_USERHOST 303,RPL_ISON 301,RPL_AWAY
  590. 305,RPL_UNAWAY 306,RPL_NOWAWAY 311,RPL_WHOISUSER
  591. 312,RPL_WHOISSERVER 313,RPL_WHOISOPERATOR
  592. 317,RPL_WHOISIDLE 318,RPL_ENDOFWHOIS
  593. 319,RPL_WHOISCHANNELS 314,RPL_WHOWASUSER
  594. 369,RPL_ENDOFWHOWAS 321,RPL_LISTSTART
  595. 322,RPL_LIST 323,RPL_LISTEND 325,RPL_UNIQOPIS
  596. 324,RPL_CHANNELMODEIS 331,RPL_NOTOPIC
  597. 332,RPL_TOPIC 341,RPL_INVITING 342,RPL_SUMMONING
  598. 346,RPL_INVITELIST 347,RPL_ENDOFINVITELIST
  599. 348,RPL_EXCEPTLIST 349,RPL_ENDOFEXCEPTLIST
  600. 351,RPL_VERSION 352,RPL_WHOREPLY 315,RPL_ENDOFWHO
  601. 353,RPL_NAMREPLY 366,RPL_ENDOFNAMES 364,RPL_LINKS
  602. 365,RPL_ENDOFLINKS 367,RPL_BANLIST 368,RPL_ENDOFBANLIST
  603. 371,RPL_INFO 374,RPL_ENDOFINFO 375,RPL_MOTDSTART
  604. 372,RPL_MOTD 376,RPL_ENDOFMOTD 381,RPL_YOUREOPER
  605. 382,RPL_REHASHING 383,RPL_YOURESERVICE 391,RPL_TIM
  606. 392,RPL_ 393,RPL_USERS 394,RPL_ENDOFUSERS 395,RPL_NOUSERS
  607. 200,RPL_TRACELINK 201,RPL_TRACECONNECTING
  608. 202,RPL_TRACEHANDSHAKE 203,RPL_TRACEUNKNOWN
  609. 204,RPL_TRACEOPERATOR 205,RPL_TRACEUSER 206,RPL_TRACESERVER
  610. 207,RPL_TRACESERVICE 208,RPL_TRACENEWTYPE 209,RPL_TRACECLASS
  611. 210,RPL_TRACERECONNECT 261,RPL_TRACELOG 262,RPL_TRACEEND
  612. 211,RPL_STATSLINKINFO 212,RPL_STATSCOMMANDS 219,RPL_ENDOFSTATS
  613. 242,RPL_STATSUPTIME 243,RPL_STATSOLINE 221,RPL_UMODEIS
  614. 234,RPL_SERVLIST 235,RPL_SERVLISTEND 251,RPL_LUSERCLIENT
  615. 252,RPL_LUSEROP 253,RPL_LUSERUNKNOWN 254,RPL_LUSERCHANNELS
  616. 255,RPL_LUSERME 256,RPL_ADMINME 257,RPL_ADMINLOC1
  617. 258,RPL_ADMINLOC2 259,RPL_ADMINEMAIL 263,RPL_TRYAGAIN
  618. 401,ERR_NOSUCHNICK 402,ERR_NOSUCHSERVER 403,ERR_NOSUCHCHANNEL
  619. 404,ERR_CANNOTSENDTOCHAN 405,ERR_TOOMANYCHANNELS
  620. 406,ERR_WASNOSUCHNICK 407,ERR_TOOMANYTARGETS
  621. 408,ERR_NOSUCHSERVICE 409,ERR_NOORIGIN 411,ERR_NORECIPIENT
  622. 412,ERR_NOTEXTTOSEND 413,ERR_NOTOPLEVEL 414,ERR_WILDTOPLEVEL
  623. 415,ERR_BADMASK 421,ERR_UNKNOWNCOMMAND 422,ERR_NOMOTD
  624. 423,ERR_NOADMININFO 424,ERR_FILEERROR 431,ERR_NONICKNAMEGIVEN
  625. 432,ERR_ERRONEUSNICKNAME 433,ERR_NICKNAMEINUSE
  626. 436,ERR_NICKCOLLISION 437,ERR_UNAVAILRESOURCE
  627. 441,ERR_USERNOTINCHANNEL 442,ERR_NOTONCHANNEL
  628. 443,ERR_USERONCHANNEL 444,ERR_NOLOGIN 445,ERR_SUMMONDISABLED
  629. 446,ERR_USERSDISABLED 451,ERR_NOTREGISTERED
  630. 461,ERR_NEEDMOREPARAMS 462,ERR_ALREADYREGISTRED
  631. 463,ERR_NOPERMFORHOST 464,ERR_PASSWDMISMATCH
  632. 465,ERR_YOUREBANNEDCREEP 466,ERR_YOUWILLBEBANNED
  633. 467,ERR_KEYSE 471,ERR_CHANNELISFULL 472,ERR_UNKNOWNMODE
  634. 473,ERR_INVITEONLYCHAN 474,ERR_BANNEDFROMCHAN
  635. 475,ERR_BADCHANNELKEY 476,ERR_BADCHANMASK 477,ERR_NOCHANMODES
  636. 478,ERR_BANLISTFULL 481,ERR_NOPRIVILEGES 482,ERR_CHANOPRIVSNEEDED
  637. 483,ERR_CANTKILLSERVER 484,ERR_RESTRICTED
  638. 485,ERR_UNIQOPPRIVSNEEDED 491,ERR_NOOPERHOST
  639. 501,ERR_UMODEUNKNOWNFLAG 502,ERR_USERSDONTMATCH
  640. 231,RPL_SERVICEINFO 232,RPL_ENDOFSERVICES
  641. 233,RPL_SERVICE 300,RPL_NONE 316,RPL_WHOISCHANOP
  642. 361,RPL_KILLDONE 362,RPL_CLOSING 363,RPL_CLOSEEND
  643. 373,RPL_INFOSTART 384,RPL_MYPORTIS 213,RPL_STATSCLINE
  644. 214,RPL_STATSNLINE 215,RPL_STATSILINE 216,RPL_STATSKLINE
  645. 217,RPL_STATSQLINE 218,RPL_STATSYLINE 240,RPL_STATSVLINE
  646. 241,RPL_STATSLLINE 244,RPL_STATSHLINE 244,RPL_STATSSLINE
  647. 246,RPL_STATSPING 247,RPL_STATSBLINE 250,RPL_STATSDLINE
  648. 492,ERR_NOSERVICEHOST
  649. ).each do |num_cmd|
  650. num, cmd = num_cmd.split(',', 2)
  651. eval <<E
  652. class #{cmd} < #{if num[0] == ?0 || num[0] == ?2 || num[0] == ?3
  653. 'CommandResponse'
  654. elsif num[0] == ?4 || num[0] == ?5
  655. 'ErrorReply'
  656. end}
  657. end
  658. Replies['#{num}'] = #{cmd}
  659.  
  660. def #{cmd.downcase}(*params)
  661. #{cmd}.new(nil, '#{cmd}', params)
  662. end
  663. module_function :#{cmd.downcase}
  664. E
  665. end
  666. end # Reply
  667. end # RICE
Add Comment
Please, Sign In to add comment