Advertisement
Guest User

Untitled

a guest
Jul 30th, 2017
66
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.98 KB | None | 0 0
  1. require 'socket'
  2. require 'logger'
  3. require 'thread'
  4. require 'numeric'
  5. require 'handler'
  6. require 'handler/default_numeric_handler'
  7. require 'handler/default_ping_handler'
  8.  
  9. module Flux
  10. #
  11. # Flux::Client is the client interface which connects to the IRC server through
  12. # a socket and sends/receives messages. It can be used to send users or channels
  13. # messages, notices or any other information. Flux::Client.connect is an alias
  14. # and may also be used... it also has a better name.
  15. #
  16. class Client
  17. @@messages = {}
  18.  
  19. #
  20. # Associate certain messages with classes. This method calls `merge!' on the
  21. # @@messages class variable, so it's not complex. Special messages are
  22. # :notice_auth, :numeric, and :unknown. So that means if the server sends a
  23. # NOTICE_AUTH or a NUMERIC or UNKNOWN literal, they won't get handled.
  24. # Everything else is handled, so PRIVMSG is associated with :privmsg, and so
  25. # on. Pass a hash of key/value pairs to this method (:privmsg => Privmsg).
  26. #
  27. def self.register_messages(messages = {})
  28. @@messages.merge!(messages)
  29. end
  30.  
  31. require 'messages'
  32.  
  33. attr_reader :address, :port, :debug, :mutex, :logger, :buffer, :threads
  34. attr_accessor :handlers
  35. alias_method :debug? , :debug
  36.  
  37. #
  38. # Initialize a new client object. `address' is the server address, for example
  39. # a string like 'irc.freenode.net', and `attr' is a hash of pre-defined
  40. # attributes. The predefined attributes are:
  41. #
  42. # :port => A fixnum port number
  43. # :debug => A boolean whether to enable debug output
  44. # :logdev => Log device, such as STDOUT for the Logger instance
  45. # :nick => Nickname to register as
  46. # :user => Username
  47. # :real => Supposedly your real name
  48. # :pass => The server password (PASS command)
  49. #
  50. # This method also has an alias, Flux::Client.connect. Alternatively, when
  51. # initializing a client object, you can pass a block which passes the client
  52. # object as the block parameter and assumes Flux::Client#process! and
  53. # Flux::Client#disconnect! at the end of the block.
  54. #
  55. #
  56. # <b>Flux::Client#threads</b>
  57. #
  58. #
  59. # Flux::Client#threads is an array of threads that the client spawns.
  60. # Flux::Client.new does some tricky stuff to this array; It adds a method
  61. # to the metaclass called `purge!' which removes all dead threads.
  62. #
  63. # This note is kept here to avoid any confusion or debugging hassles to
  64. # people extending or modifying the client library.
  65. #
  66. def initialize(address, attr = {})
  67. @mutex = Mutex.new
  68. @address = address
  69. @port = (attr[:port] || 6667).to_i
  70. @debug = attr[:debug] ? true : false
  71. @logger = Logger.new(attr[:logdev] || STDOUT)
  72. @buffer = []
  73. @handlers = []
  74. @threads = []
  75. @threads.instance_variable_set('@logger', @logger)
  76. @threads.instance_variable_set('@client', self)
  77.  
  78. # Default handlers.
  79. @handlers << Handler::DefaultNumericHandler.new
  80. @handlers << Handler::DefaultPingHandler.new
  81.  
  82. # Let's abort when a thread fails.
  83. Thread.abort_on_exception = true
  84.  
  85. class << @threads
  86. def purge!
  87. dead = find_all { |thread| !thread.alive? }
  88.  
  89. if @client.debug?
  90. @logger.debug("Purging threads array: #{dead.size} thread(s) removed, #{size - dead.size} still alive")
  91. end
  92.  
  93. unless dead.empty?
  94. for thread in dead
  95. delete(thread)
  96. end
  97.  
  98. return self
  99. end
  100. end
  101.  
  102. alias_method :purge, :purge!
  103. end
  104.  
  105. @logger.debug("Attempting to connect to #@address:#@port") if @debug
  106. @socket = TCPSocket.new(@address, @port)
  107. @logger.debug("You are now connected to #@address:#@port") if @debug
  108.  
  109. # Automatic login?
  110. login(attr[:nick], attr[:user] || attr[:nick], attr[:real] || attr[:nick]) if attr[:nick]
  111.  
  112. if block_given?
  113. @logger.debug('Initializing client object via block form') if @debug
  114. yield self
  115. process!
  116. disconnect!
  117. end
  118. end
  119.  
  120. #
  121. # Disconnect from the server. It does nothing if you're not connected in the
  122. # first place. Flux::Client#disconnect! is an alias.
  123. #
  124. def disconnect
  125. if connected?
  126. @socket.close unless @socket.closed?
  127. @logger.debug('Disconnected') if @debug
  128. else
  129. @logger.debug('Disconnect attempted, but not connected') if @debug
  130. end
  131. end
  132.  
  133. #
  134. # This is a convenience method for checking the connection status between the
  135. # client and the server. It'll return true if the client is still connected
  136. # and false if it isn't.
  137. #
  138. def connected?
  139. @socket ? true : false
  140. end
  141.  
  142. #
  143. # Write data to the socket.
  144. #
  145. def write(msg)
  146. @logger.debug("OUTGOING: #{msg.strip}") if @debug
  147. @socket.write("#{msg}\r\n")
  148. end
  149.  
  150. #
  151. # Login to the IRC server.
  152. #
  153. def login(nick, user = nick, real = nick, pass = nil)
  154. nick(nick)
  155. user(user, real)
  156. pass(pass) if pass
  157. end
  158.  
  159. #
  160. # Send NICK command.
  161. #
  162. def nick(nickname = nil)
  163. nickname ? _write("NICK #{nickname}") : write('NICK')
  164. end
  165.  
  166. #
  167. # Send USER command.
  168. #
  169. def user(username, realname)
  170. _write("USER #{username} #{username} #{username} :#{realname}")
  171. end
  172.  
  173. #
  174. # Send PASS command.
  175. #
  176. def pass(password)
  177. _write("PASS #{password}")
  178. end
  179.  
  180. #
  181. # Send PRIVMSG command.
  182. #
  183. def privmsg(target, text)
  184. for line in text.split("\n")
  185. next if line.strip.empty?
  186. write("PRIVMSG #{target} :#{line}")
  187. end
  188. end
  189.  
  190. #
  191. # Send NOTICE command.
  192. #
  193. def notice(target, text)
  194. write("NOTICE #{target} :#{text}")
  195. end
  196.  
  197. #
  198. # Send PING command.
  199. #
  200. def ping(data)
  201. write("PING :#{data}")
  202. end
  203.  
  204. #
  205. # Send PONG command.
  206. #
  207. def pong(data)
  208. write("PONG :#{data}")
  209. end
  210.  
  211. #
  212. # Process incoming data infinitely and sort them into their corresponding message
  213. # objects such that a PRIVMSG is a PrivateMessage object, a NOTICE is a Notice
  214. # object, and so on. Flux::Client#main is an alias.
  215. #
  216. def process!
  217. @logger.debug('Entering main processing loop') if @debug
  218.  
  219. loop do
  220. rw, wr, err = select([@socket], [@socket], [@socket])
  221. read unless rw.empty?
  222. write unless wr.empty?
  223. @logger.error("Socket error") if @debug and !err.empty?
  224. end
  225. end
  226.  
  227. class << self
  228. alias_method :connect, :new
  229. end
  230.  
  231. alias_method :disconnect!, :disconnect
  232. alias_method :main, :process!
  233.  
  234. #
  235. # See if the connection has been registered by the server. This means it's safe
  236. # to send messages.i
  237. #
  238. # This method by default will return false, but is redefined by the handler
  239. # Flux::Handler::DefaultNumericHandler when it receives an RPL_WELCOME.
  240. #
  241. def registered?
  242. false
  243. end
  244.  
  245. #
  246. # This method is invoked when the socket is writable or can be
  247. # invoked when you want to write data to the socket.
  248. #
  249. def write(data= nil)
  250. @buffer << data if data
  251.  
  252. @mutex.synchronize do
  253. if registered?
  254. for line in @buffer
  255. _write(line)
  256. end
  257.  
  258. @buffer.clear
  259. end
  260. end
  261. end
  262.  
  263. private
  264.  
  265. #
  266. # This method is invoked when the socket is readable, and it grabs
  267. # a line of data from the server and processes it in its own thread.
  268. #
  269. def read
  270. data = @socket.gets.strip
  271. @logger.debug("INCOMING << #{data}") if @debug
  272. invoke_handlers(parse_message(data))
  273. end
  274.  
  275. #
  276. # This method is like Flux::Client#write, except it bypasses any
  277. # buffer queueing and writes straight to the socket.
  278. #
  279. def _write(data)
  280. @logger.debug("OUTGOING >> #{data}") if @debug
  281. @socket.write("#{data}\r\n")
  282. end
  283.  
  284. #
  285. # Parse a line into its corresponding Flux::Message object.
  286. #
  287. def parse_message(raw)
  288. origin, line = raw.match(/^(?::(\S ) )?(. )$/).to_a[1..-1]
  289.  
  290. case line
  291. # parse NOTICE AUTH messages
  292. when /^NOTICE AUTH :(. )$/
  293. command = :notice_auth
  294. message = @@messages[:notice_auth].new(self, raw, command, [], $1)
  295. when /^(\d ) (\S )(?: (.*?))(?: :(. ))?$/
  296. command = :numeric
  297. arguments = $3 ? $3.split(' ') : []
  298. message = @@messages[:numeric].new(self, raw, command, arguments, $4)
  299. message.numeric = $1.to_i
  300. message.my_nick = $2
  301. when /^(\S )(?: (.*?))(?: ?:(. ))?$/
  302. command = $1.downcase.to_sym
  303. arguments = $2 ? $2.split(' ') : []
  304. message = @@messages[command].new(self, raw, command, arguments, $3)
  305. else
  306. command = :unknown
  307. message = @@messages[:unknown].new(self, raw, command, [], nil)
  308. end
  309.  
  310. @logger.debug("Parsed (#{message.class.name.split('::')[-1]}): #{raw.inspect}") if @debug
  311. message.origin = OpenStruct.new
  312.  
  313. if parsed = parse_origin(origin)
  314. message.origin.nick = parsed[0]
  315. message.origin.user = parsed[1]
  316. message.origin.host = parsed[2]
  317. else
  318. message.origin.server = origin
  319. end
  320.  
  321. message.customize
  322. message
  323. end
  324.  
  325. #
  326. # Parse an origin and return an array of [nick, user, host] if the format is
  327. # valid. Otherwise return nil.
  328. #
  329. def parse_origin(origin)
  330. [$1, $2, $3] if origin =~ /^([^!] )(?:!([^@] )(?:@(. )))?$/
  331. end
  332.  
  333. #
  334. # Determine if a given name is a channel name.
  335. #
  336. def channel?(name)
  337. chantypes = @isupport ? isupport[:chantypes] : '#'
  338. if chantypes =~ Regexp.new(Regexp.escape(name[0..0])) then true else false end
  339. end
  340.  
  341. #
  342. # Invoke handlers which correspond to a given message's signal.
  343. #
  344. def invoke_handlers(message)
  345. if handlers = @handlers.find_all { |h| h.signals.include?(message.command) }
  346. command = case message.command
  347. when :notice_auth then 'NOTICE AUTH message'
  348. when :numeric then "numeric message (#{Reply[message.numeric]}: #{message.numeric})"
  349. when :unknown then 'unknown message'
  350. else message.command.to_s.upcase
  351. end
  352.  
  353. @logger.debug("Invoking #{handlers.size} handler(s) for #{command}") if @debug
  354.  
  355. for handler in handlers
  356. @threads << thread = Thread.new { handler.call(self, message) }
  357. thread.join unless handler.threaded?
  358. end
  359. end
  360.  
  361. @threads.purge!
  362. end
  363. end
  364. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement