Guest User

Untitled

a guest
Mar 11th, 2018
90
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.79 KB | None | 0 0
  1. #--
  2. # Copyright 2007 by Stefan Rusterholz.
  3. # All rights reserved.
  4. # See LICENSE.txt for permissions.
  5. #++
  6.  
  7.  
  8.  
  9. require 'butler'
  10. require 'butler/irc/client'
  11. require 'butler/debuglog'
  12. require 'configuration'
  13. require 'log'
  14. require 'butler/plugins'
  15. require 'butler/remote/connection'
  16. require 'butler/remote/message'
  17. require 'butler/remote/server'
  18. require 'butler/remote/user'
  19. require 'butler/services'
  20. require 'butler/session'
  21. require 'ruby/string/arguments'
  22. require 'ruby/string/post_arguments'
  23. require 'scheduler'
  24. require 'set'
  25. require 'thread'
  26. require 'timeout'
  27.  
  28.  
  29.  
  30. class Butler
  31. class Bot < IRC::Client
  32. include Log::Comfort
  33.  
  34. # The access framework for this bot. An instance of Access.
  35. attr_reader :access
  36.  
  37. # This bot instances configuration. An instance of Configuration.
  38. attr_reader :config
  39.  
  40. # The logging device, per default nil
  41. attr_reader :logger
  42.  
  43. # An OpenStruct with all important paths of this bot instance.
  44. attr_reader :path
  45.  
  46. # Services butler offers, instance of Butler::Plugins
  47. attr_reader :plugins
  48.  
  49. # Remote connections server, instance of Butler::Remote::Server
  50. attr_reader :remote
  51.  
  52. # Scheduler to hook up timed events, instance of Scheduler
  53. # For plugins and services, please look up the class methods for
  54. # Butler::Plugin and Butler::Service, they are to be prefered.
  55. attr_reader :scheduler
  56.  
  57. # Services butler offers, instance of Butler::Services
  58. attr_reader :services
  59.  
  60. def initialize(path, name, opts={})
  61. path ||= Butler.path
  62. @irc = nil # early inspects
  63. @name = name
  64. @base = "#{path.bots}/#{name}"
  65. @path = OpenStruct.new(
  66. :access => @base+'/access',
  67. :base => @base,
  68. :config => @base+'/config',
  69. :lib => @base+'/lib',
  70. :log => @base+'/log',
  71. :plugins => @base+'/plugins',
  72. :services => @base+'/services',
  73. :strings => @base+'/strings'
  74. )
  75. $LOAD_PATH.unshift(@path.lib)
  76. require 'butlerinit' if File.exist?(@path.lib+"/butlerinit.rb")
  77. @logger = $stderr
  78. @remote = Remote::Server.new(self)
  79. @config = Configuration.new(@base+'/config')
  80. @scheduler = Scheduler.new
  81. @access = Access.new(
  82. Access::YAMLBase.new(Access::User::Base, @base+'/access/user'),
  83. Access::YAMLBase.new(Access::Role::Base, @base+'/access/role'),
  84. Access::YAMLBase.new(Access::Privilege::Base, @base+'/access/privilege')
  85. #:channel => Access::YAMLBase.new("#{TestDir}/channel", Access::Location)
  86. )
  87. @on_disconnect = nil
  88. @on_reconnect = nil
  89. @reconnect = [
  90. opts.delete(:reconnect_delay) || 60,
  91. opts.delete(:reconnect_tries) || -1
  92. ]
  93.  
  94. super(@config["connections/main/server"], {
  95. :host => @config["connections/main/host"],
  96. :port => @config["connections/main/port"],
  97. }.merge(opts))
  98.  
  99. # { lang => { trigger => SortedSet[ *commands ] } }
  100. @commands = {}
  101. @services = Services.new(self, @path.services)
  102. @services.load_all
  103. @plugins = Plugins.new(self, @path.plugins)
  104.  
  105. if $DEBUG then
  106. @irc.extend DebugLog
  107. @irc.raw_log = File.open(@path.log+'/debug.log', 'wb')
  108. @irc.raw_log.sync = true
  109. end
  110.  
  111. subscribe(:PRIVMSG, -10, &method(:invoke_commands))
  112. subscribe(:NOTICE, -10, &method(:invoke_commands))
  113. end
  114.  
  115. def connected?
  116. @irc.connected?
  117. end
  118.  
  119. def invoke_commands(listener, message)
  120. return unless ((sequence = invocation(message.text)) || message.realm == :private)
  121. message.invocation = sequence || ""
  122. message.language = "en"
  123. trigger = message.arguments.first
  124. access = message.from && message.from.access.method(:authorized?)
  125. return unless access and trigger
  126.  
  127. trigger = trigger.downcase
  128. commands = []
  129. if @commands[message.language] && @commands[message.language][trigger] then
  130. commands.concat(@commands[message.language][trigger].to_a)
  131. end
  132. if message.language != "en" && @commands["en"] && @commands["en"][trigger] then
  133. commands.concat(@commands["en"][trigger].to_a)
  134. end
  135.  
  136. commands.each { |command|
  137. if args = (command.invoked_by?(message)) then
  138. if !command.authorization || access.call(command.authorization) then
  139. Thread.new {
  140. begin
  141. command.call(message, *args)
  142. rescue Exception => e
  143. exception(e)
  144. end
  145. }
  146. break if command.abort_invocations?
  147. else
  148. info("#{message.from} (#{message.from.access.oid}) had no authorization for 'plugin/#{command.plugin.base}'")
  149. end
  150. end
  151. }
  152. end
  153.  
  154. def add_command(command)
  155. @commands[command.language] ||= {}
  156. @commands[command.language][command.trigger] ||= SortedSet.new
  157. @commands[command.language][command.trigger].add(command)
  158. end
  159.  
  160. def delete_command(command)
  161. @commands[command.language][command.trigger].delete(command)
  162. if @commands[command.language][command.trigger].empty? then
  163. @commands[command.language].delete(command.trigger)
  164. @commands.delete(command.language) if @commands[command.language].empty?
  165. end
  166. end
  167.  
  168. # returns the rest of the sequence if it was an invocation, nil else
  169. def invocation(sequence)
  170. (@myself && sequence[/^(?:!?#{@myself.nick}[:;,])\s+/i]) || sequence[/^#{@config['invocation']}/i]
  171. puts @config['invocation']
  172. end
  173.  
  174. def login
  175. @access.default_user = @access["default_user"]
  176. @access.default_user.login
  177.  
  178. nick = @config["connections/main/nick"]
  179. pass = @config["connections/main/password"]
  180. begin
  181. super(
  182. nick,
  183. @config["connections/main/user"],
  184. @config["connections/main/real"],
  185. @config["connections/main/serverpass"]
  186. )
  187. rescue Errno::ECONNREFUSED
  188. if @reconnect[1].zero? then
  189. warn("Connection was refused")
  190. return false
  191. else
  192. warn("Connection was refused, trying again in #{@reconnect.first} seconds")
  193. @reconnect[1] -= 1
  194. sleep(@reconnect[0])
  195. retry
  196. end
  197. end
  198. info("Logged in")
  199. if pass && !@parser.same_nick?(@myself.nick, nick) then
  200. if wait_for(:NOTICE, 60, :prepare => proc { @irc.ghost(nick, pass) } ) { |m|
  201. m.from && m.from.nick =~ /nickserv/i && m.text =~ /has been killed/
  202. } then
  203. @irc.nick(nick)
  204. else
  205. warn("Could not ghost the user already occupying my nick '#{nick}'")
  206. end
  207. end
  208. @irc.identify(pass) if pass
  209. join(*@config["connections/main/channels"])
  210. plugins_dispatch(:on_login, "main")
  211. true
  212. end
  213.  
  214. def on_disconnect(reason)
  215. info("Disconnected due to #{reason}")
  216. return if reason == :quit
  217. plugins_dispatch(:on_disconnect, reason)
  218. unless @reconnect[1].zero? then
  219. @reconnect[1] -= 1
  220. sleep(@reconnect[0])
  221. login
  222. end
  223. end
  224.  
  225. def quit(reason=nil, *args)
  226. timeout(3) {
  227. plugins_dispatch(:on_quit, reason, *args).join
  228. }
  229. ensure
  230. super(reason)
  231. end
  232.  
  233. def plugins_dispatch(event, *args)
  234. Thread.new { # avoid total lockup due to an improperly written on_disconnect
  235. begin
  236. @plugins.instances.each { |plugin|
  237. plugin.send(event, *args)
  238. }
  239. rescue Exception => e
  240. exception(e)
  241. end
  242. }
  243. end
  244.  
  245. def output_to_logfiles
  246. # Errors still go to $stderr, $stdin handles puts as "info" level $stderr prints
  247. @logger = Log.file(@path.log+'/error.log')
  248. $stdout = Log.forward(@logger, :warn)
  249. $stderr = Log.forward(@logger, :info)
  250. end
  251.  
  252. def inspect # :nodoc:
  253. "#<%s:0x%08x %s (in %s) irc=%s>" % [
  254. self.class,
  255. object_id << 1,
  256. @name,
  257. @base,
  258. @irc.inspect
  259. ]
  260. end
  261. end # Bot
  262.  
  263. class IRC::UserList
  264. attr_reader :client
  265. end
  266.  
  267. class IRC::User
  268. attr_accessor :access
  269. attr_reader :session
  270.  
  271. def authorized?(*args)
  272. @access.authorized?(*args)
  273. end
  274.  
  275. alias butler_initialize initialize unless method_defined? :butler_initialize
  276. def initialize(users, *args, &block)
  277. butler_initialize(users, *args, &block)
  278. @access = users.client.access.default_user
  279. @session = Session.new
  280. end
  281. end # IRC::User
  282.  
  283. class IRC::Message
  284. # the language of this message, as determined by butler
  285. attr_accessor :language
  286.  
  287. # the invocation-sequence (e.g. "butler, "), nil if none was used (e.g. in
  288. # private messages)
  289. attr_accessor :invocation
  290.  
  291. alias butler_initialize initialize unless method_defined? :butler_initialize
  292. def initialize(*args, &block)
  293. butler_initialize(*args, &block)
  294. @language = nil
  295. @invocation = nil
  296. @arguments = nil
  297. @formatted_arguments = nil
  298. @post_arguments = nil
  299. end
  300.  
  301. # parses a given string into argument-tokens. a token is either a word or a quoted string. escapes are respected.
  302. # e.g. 'Hallo "this is token2" "and this \"token\" is token3"'
  303. # would be parsed into: ["hallo", "this is token2", "and this \"token\" is token3"]
  304. def arguments
  305. # only messages with text can be tokenized to parameters
  306. raise NoMethodError, "Message has no text, can't generate arguments" unless text
  307. # cached
  308. return @arguments if @arguments
  309. # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
  310. args = text[@invocation.length..-1].mirc_stripped.scan(/"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
  311. # sanitize the last argument in case it's not correctly quoted
  312. args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
  313. # unescape data and remove quotes
  314. @arguments = args.map { |arg|
  315. case arg[0,1]
  316. when "'": arg[1..-2].gsub(/\\(['\\])/, '\1')
  317. when '"': arg[1..-2].gsub(/\\(["\\])/, '\1')
  318. else arg.gsub(/\\(["'\\ ])/, '\1')
  319. end
  320. }
  321. end
  322.  
  323. # like #arguments, but doesn't strip color information
  324. def formatted_arguments
  325. # only messages with text can be tokenized to parameters
  326. raise NoMethodError, "Message has no text, can't generate arguments" unless text
  327. # cached
  328. return @arguments if @arguments
  329. # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
  330. args = text[@invocation.length..-1].scan(/"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
  331. # sanitize the last argument in case it's not correctly quoted
  332. args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
  333. # unescape data and remove quotes
  334. @formatted_arguments = args.map { |arg|
  335. case arg[0,1]
  336. when "'": arg[1..-2].gsub(/\\(['\\])/, '\1')
  337. when '"': arg[1..-2].gsub(/\\(["\\])/, '\1')
  338. else arg.gsub(/\\(["'\\ ])/, '\1')
  339. end
  340. }
  341. end
  342.  
  343. # Returns the rest of message text after argument
  344. # == Synopsis
  345. # "foo bar baz".post_arguments[0] # => "foo bar baz"
  346. # "foo bar baz".post_arguments[1] # => "bar baz"
  347. # "foo bar baz".post_arguments[2] # => "baz"
  348. def post_arguments
  349. # only messages with text can be tokenized to parameters
  350. raise NoMethodError, "Message has no text, can't generate arguments" unless text
  351. # cached
  352. return @post_arguments if @post_arguments
  353. # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
  354. args = text[@invocation.length..-1].scan(/\s+|"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
  355. # sanitize arguments
  356. args.shift if args.first =~ /^\s+$/
  357. args.pop if args.last =~ /^\s+$/
  358. args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
  359. # unescape data and remove quotes
  360. args.map! { |arg|
  361. case arg[0,1]
  362. when "'": arg.gsub(/\\(['\\])/, '\1')
  363. when '"': arg.gsub(/\\(["\\])/, '\1')
  364. else arg.gsub(/\\(["'\\ ])/, '\1')
  365. end
  366. }
  367. @post_arguments = []
  368. (0...args.length).step(2) { |i| @post_arguments << args[i..-1].join('') }
  369. @post_arguments
  370. end
  371. end # IRC::Message
  372. end # Butler
Add Comment
Please, Sign In to add comment