Advertisement
Guest User

Untitled

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