PerfectlyNormal-Flexo 0.3.9

Sign up to get free protection for your applications and to get access to all the features.
data/lib/daemonize.rb ADDED
@@ -0,0 +1,59 @@
1
+ # Shamelessly downloaded and included in Flexo
2
+ # from http://grub.ath.cx/daemonize/
3
+ # Not my own work at all.
4
+ module Daemonize
5
+ VERSION = "0.1.2"
6
+
7
+ # Try to fork if at all possible retrying every 5 sec if the
8
+ # maximum process limit for the system has been reached
9
+ def safefork
10
+ tryagain = true
11
+
12
+ while tryagain
13
+ tryagain = false
14
+ begin
15
+ if pid = fork
16
+ return pid
17
+ end
18
+ rescue Errno::EWOULDBLOCK
19
+ sleep 5
20
+ tryagain = true
21
+ end
22
+ end
23
+ end
24
+
25
+ # This method causes the current running process to become a daemon
26
+ # If closefd is true, all existing file descriptors are closed
27
+ def daemonize(oldmode=0, closefd=false)
28
+ srand # Split rand streams between spawning and daemonized process
29
+ safefork and exit # Fork and exit from the parent
30
+
31
+ # Detach from the controlling terminal
32
+ unless sess_id = Process.setsid
33
+ raise 'Cannot detach from controlled terminal'
34
+ end
35
+
36
+ # Prevent the possibility of acquiring a controlling terminal
37
+ if oldmode.zero?
38
+ trap 'SIGHUP', 'IGNORE'
39
+ exit if pid = safefork
40
+ end
41
+
42
+ Dir.chdir "/" # Release old working directory
43
+ File.umask 0000 # Insure sensible umask
44
+
45
+ if closefd
46
+ # Make sure all file descriptors are closed
47
+ ObjectSpace.each_object(IO) do |io|
48
+ unless [STDIN, STDOUT, STDERR].include?(io)
49
+ io.close rescue nil
50
+ end
51
+ end
52
+ end
53
+
54
+ STDIN.reopen "/dev/null" # Free file descriptors and
55
+ STDOUT.reopen File.expand_path("~/flexo-err.log"), "a+" # point them somewhere sensible
56
+ STDERR.reopen STDOUT # STDOUT/STDERR should go to a logfile
57
+ return oldmode ? sess_id : 0 # Return value is mostly irrelevant
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ # dirty haxx
2
+ $:.push 'lib'
3
+
4
+ require 'daemonize'
5
+ require 'flexo/manager'
6
+
7
+ module Flexo
8
+ # This is the 'main' class of Flexo.
9
+ # The main file just creates an instance of this class, and then
10
+ # everything goes automatically. Additionally, it contains tons of
11
+ # convenience functions, so plugins don't neccessarily have to do everything
12
+ # so complicated.
13
+ class Client
14
+ include Daemonize
15
+
16
+ def initialize
17
+ @nickname = nil
18
+
19
+ begin
20
+ @manager = Flexo::Manager.instance
21
+ @manager.setup
22
+ rescue InvalidConfigurationException => e
23
+ $stderr.puts "Invalid configuration from Flexo::Client"
24
+ exit 1
25
+ end
26
+
27
+ daemonize
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,79 @@
1
+ require 'yaml'
2
+
3
+ module Flexo
4
+ # Configuration-related functions.
5
+ # Able to read settings from a file, look them up, change them, and save
6
+ # them back to the file while the bot is running.
7
+ class Config
8
+ attr_reader :configpath
9
+ attr_reader :configfile
10
+
11
+ alias path configpath
12
+ alias file configfile
13
+
14
+ # Reads the configuration file into an array
15
+ def initialize
16
+ @configpath = File.expand_path("~/.flexo")
17
+ @configfile = "#{@configpath}/config.yaml"
18
+ @config = {}
19
+ reload(:first)
20
+ end
21
+
22
+ # Very basic validity-test. Just checks if we have *anything*
23
+ # stored in the configuration.
24
+ def valid?
25
+ return !@config.empty?
26
+ end
27
+
28
+ # Lookup-function. Lets us use Config as an array,
29
+ # for convenience. so instead of Config.lookup('key'),
30
+ # we can use Config['key']
31
+ def [](key)
32
+ if @config[key]
33
+ return @config[key]
34
+ end
35
+
36
+ return nil
37
+ end
38
+
39
+ # Same as [], lets you use this class as an array for saving
40
+ # values as well. Plugins also have access to this class, and
41
+ # can store their own permanent values here.
42
+ #
43
+ # Flexo would preferably use a "namespace", and plugins use their own
44
+ # namespace, using keys like core.nicks instead of just making a mess.
45
+ # This would also prevent collisions
46
+ def []=(key, value)
47
+ @config[key] = value
48
+ return @config[key]
49
+ end
50
+
51
+ # Reloads configuration from file, resetting changes made since the
52
+ # last write.
53
+ def reload(first = false)
54
+ begin
55
+ Dir["#{@configpath}/*.yaml"].each do |file|
56
+ tmp = YAML.load_file(file)
57
+ next if tmp.class != Hash && tmp.class != Array
58
+ @config.merge!(tmp)
59
+ end
60
+ rescue
61
+ if first == :first
62
+ Flexo::Logger.error "No configuration found. "
63
+ Flexo::Logger.error "Try mkflexorc for an automated tool to help "
64
+ Flexo::Logger.error "you create a configuration for Flexo.\n"
65
+ exit
66
+ else
67
+ Flexo::Logger.warn "Configuration has disappeared! Aborting reload."
68
+ end
69
+ end
70
+ end
71
+
72
+ # Saves the current configuration to a file.
73
+ def write
74
+ File.open(@configfile, 'w') do |f|
75
+ f.puts @config.to_yaml
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ module Flexo
2
+ # A collection of various constants
3
+ module Constants
4
+ LOG_DEBUG2 = 4
5
+ LOG_DEBUG = 3
6
+ LOG_INFO = 2
7
+ LOG_WARN = 1
8
+ LOG_ERROR = 0
9
+
10
+ CHANPREFIX = ['#', '&', '+', '!']
11
+
12
+ FLEXO_VERSION = '0.3.9'
13
+ FLEXO_RELEASE = 'Bendless Love'
14
+ end
15
+ end
data/lib/flexo/data.rb ADDED
@@ -0,0 +1,104 @@
1
+ module Flexo
2
+ # This is a handy objecty representation
3
+ # of an IRC message. Each line is broken into
4
+ # tiny little pieces, made available to the rest
5
+ # of Flexo through accessors (only readers).
6
+ class Data
7
+ attr_reader :raw
8
+ attr_reader :message
9
+ attr_reader :origin
10
+ attr_reader :params
11
+ attr_reader :string
12
+ attr_reader :hostmask
13
+ attr_reader :numeric
14
+
15
+ # Calls parse() and fills up all the variables
16
+ # with sensible data
17
+ def initialize(raw)
18
+ @manager = Flexo::Manager.instance
19
+ data = parse(raw)
20
+
21
+ if data == nil
22
+ @manager.logger.error("Unable to parse\n#{raw}")
23
+ return
24
+ end
25
+
26
+ @raw = raw
27
+ @message = data[:message]
28
+ @origin = data[:origin]
29
+ @params = data[:params]
30
+ @string = data[:string]
31
+ @hostmask = parse_hostmask(@origin) if @origin
32
+ @numeric = @message =~ /^\d+$/ ? true : false
33
+ end
34
+
35
+ def numeric?
36
+ @numeric
37
+ end
38
+
39
+ def to_s
40
+ @raw
41
+ end
42
+
43
+ def inspect # :nodoc:
44
+ "#<#{self.class}:#{(@raw.size > 40 ? @raw[0..40] + '...' : @raw).inspect}>"
45
+ end
46
+
47
+ # Does the actual parsing and splitting and trickery
48
+ # with the received line
49
+ def parse(line)
50
+ m = line.lstrip.chomp.match(/^(?::(\S+) )?(\S+)(?: ([^:].*?[^:]))?(?: :(.*?))?$/)
51
+ {:origin => m[1], :message => m[2], :params => m[3] ? m[3].split(' ') : [], :string => m[4] || ''} if m
52
+ end
53
+
54
+ # This parses the hostmask, creating an instance of
55
+ # Flexo::Data::Hostmask.
56
+ def parse_hostmask(mask)
57
+ m = mask.match(/^([^!\s]+)(?:(?:!([^@\s]+))?@(\S+))?$/)
58
+
59
+ if m
60
+ host = Hostmask.new(m[1], m[2], m[3])
61
+ return host
62
+ end
63
+
64
+ return nil
65
+ end
66
+ end
67
+
68
+ # For added fun, we have a small, separate class
69
+ # for hostmasks. And, being very creative, we call it...
70
+ # Data::Hostmask!
71
+ class Data::Hostmask
72
+ attr_reader :nickname
73
+ attr_reader :hostname
74
+ attr_reader :username
75
+ attr_reader :hostmask
76
+
77
+ alias :nick :nickname
78
+ alias :from :nickname
79
+ alias :by :nickname
80
+
81
+ alias :host :hostname
82
+ alias :ip :hostname
83
+
84
+ alias :user :username
85
+ alias :ident :username
86
+
87
+ alias :mask :hostmask
88
+
89
+ # The only function in this class.
90
+ # When instanciated, this populates some instance variables
91
+ # with sensible values, and then allow us to access those using
92
+ # attr_readers
93
+ def initialize(nickname, username, hostname)
94
+ @nickname = nickname
95
+ @username = username
96
+ @hostname = hostname
97
+ @hostmask = "#{@nickname}!#{@username}@#{@hostname}".sub(/!?@?$/, '')
98
+ end
99
+
100
+ def inspect
101
+ "#<#{self.class}:#{@hostmask}>"
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,134 @@
1
+ require 'flexo/event'
2
+ require 'flexo/events/privmsg'
3
+ require 'flexo/events/notice'
4
+ require 'flexo/events/reply'
5
+ require 'flexo/events/unknown'
6
+ require 'flexo/events/mode'
7
+ require 'flexo/events/ping'
8
+ require 'flexo/events/pong'
9
+ require 'flexo/events/join'
10
+ require 'flexo/events/part'
11
+ require 'flexo/events/quit'
12
+ require 'flexo/events/kick'
13
+ require 'flexo/events/nick'
14
+ require 'flexo/events/topic'
15
+
16
+ module Flexo
17
+ # Dispatcher receives data from Flexo::Server, analyzes it, and creates
18
+ # relevant events (see Flexo::Event), which is then broadcast
19
+ # to all classes and plugins wanting to use it for something.
20
+ #
21
+ # This class also manages subscriptions for events, so all plugins
22
+ # that want to listen for an event, can use subscribe and unsubscribe
23
+ # to manage things.
24
+ class Dispatcher
25
+ # Save a reference to Flexo::Manager, set up the dispatching-queue,
26
+ # and get ready for working.
27
+ def initialize
28
+ @manager = Flexo::Manager.instance
29
+ @queue = Queue.new
30
+ @handlers = {}
31
+ @triggers = []
32
+
33
+ @manager.thread do
34
+ while event = @queue.shift
35
+ next unless @handlers.key?(event.class)
36
+ @handlers[event.class].each { |h|
37
+ @manager.logger.debug2("Running handler #{h} for #{event.class}")
38
+ h.call(event)
39
+ }
40
+ end
41
+ end
42
+ end
43
+
44
+ # Does the handling of incoming lines.
45
+ # First, we use Flexo::Data to get
46
+ # anything meaningful out of the line received.
47
+ # Then, we create a subclass of Flexo::Event, which
48
+ # is pushed onto the dispatching-queue for further processing.
49
+ def receive(line)
50
+ @manager.logger.debug2("Received \"#{line.chomp}\"")
51
+ data = Data.new(line)
52
+ event = case data.message
53
+ when /^\d+$/ then Flexo::Events::ReplyEvent.new @manager, data
54
+ when 'JOIN' then Flexo::Events::JoinEvent.new @manager, data
55
+ when 'KICK' then Flexo::Events::KickEvent.new @manager, data
56
+ when 'MODE' then Flexo::Events::ModeEvent.new @manager, data
57
+ when 'NICK' then Flexo::Events::NickEvent.new @manager, data
58
+ when 'NOTICE' then Flexo::Events::NoticeEvent.new @manager, data
59
+ when 'PART' then Flexo::Events::PartEvent.new @manager, data
60
+ when 'PING' then Flexo::Events::PingEvent.new @manager, data
61
+ when 'PONG' then Flexo::Events::PongEvent.new @manager, data
62
+ when 'PRIVMSG' then Flexo::Events::PrivmsgEvent.new @manager, data
63
+ when 'QUIT' then Flexo::Events::QuitEvent.new @manager, data
64
+ when 'TOPIC' then Flexo::Events::TopicEvent.new @manager, data
65
+ else Flexo::Events::UnknownEvent.new @manager, data
66
+ end
67
+
68
+ @queue << event
69
+ end
70
+
71
+ # Registers a callback function for a specific event.
72
+ def subscribe(event, &block)
73
+ if event.is_a?(Symbol) || event.kind_of?(String)
74
+ if (e = event.to_s) =~ /^(?:RPL|ERR)_/i
75
+ event = Flexo::Events::ReplyEvent[e.upcase]
76
+ else
77
+ match = e.match(/^(.+?)(?:_?event)?$/i)
78
+ event = Flexo::Events.const_get("#{match[1].capitalize}Event")
79
+ end
80
+ end
81
+
82
+ if event.is_a? Flexo::Events::ReplyEvent::Numeric
83
+ numeric = event.numeric
84
+ event = Flexo::Events::ReplyEvent
85
+ else
86
+ numeric = false
87
+ end
88
+
89
+ handler = Handler.new(numeric, &block)
90
+ @manager.mutex do
91
+ @handlers[event] ||= []
92
+ @handlers[event] << handler
93
+ end
94
+
95
+ return handler
96
+ end
97
+
98
+ # Removes an event handler.
99
+ # Known to be broken.
100
+ # DO NOT USE.
101
+ # See bug #28
102
+ def unsubscribe(handler)
103
+ @manager.logger.debug("Going to remove handler #{handler}")
104
+ events = []
105
+ @manager.mutex do
106
+ @handlers.each { |e,handlers|
107
+ if handlers.delete(handler)
108
+ events << e
109
+ @manager.logger.debug("Adding #{e} to removal queue")
110
+ end
111
+ }
112
+ events.each { |e|
113
+ @manager.logger.debug("Removing #{e}")
114
+ @handlers.delete(e)
115
+ }
116
+ end
117
+ end
118
+
119
+ # Easy way to add a trigger.
120
+ # See Flexo::Trigger for documentation
121
+ def add_trigger(pattern, &block)
122
+ trigger = Flexo::Trigger.new(pattern, &block)
123
+ @manager.mutex { @triggers << trigger }
124
+ trigger
125
+ end
126
+
127
+ # Easy way to remove a trigger.
128
+ # Must be passed a Flexo::Trigger object
129
+ def remove_trigger(trigger)
130
+ trigger.unsubscribe
131
+ @manager.mutex { @triggers.delete(trigger) }
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,29 @@
1
+ module Flexo
2
+ # Our base-class for errors and exceptions
3
+ class Error < StandardError
4
+ end
5
+
6
+ # Raised when the configuration is invalid, and there's no way to recover
7
+ class InvalidConfigurationException < Error
8
+ end
9
+
10
+ # Raised when a plugin is missing a required dependency
11
+ class PluginDependencyError < Error
12
+ end
13
+
14
+ # Default error for plugins
15
+ class PluginError < Error
16
+ end
17
+
18
+ # Exception raised when a plugin is missing a name
19
+ class PluginNameError < PluginError
20
+ end
21
+
22
+ # Raised when there's a name conflict
23
+ class PluginConflictError < PluginError
24
+ end
25
+
26
+ # Trying to load, but could not be found (404)
27
+ class PluginNotFoundError < PluginError
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Flexo
2
+ # Base class for all events generated by Flexo.
3
+ # Nothing of interest happening here, actually.
4
+ # Most of the fun stuff is taken care of by the subclasses
5
+ class Event
6
+ attr_reader :data
7
+ attr_reader :nickname
8
+
9
+ alias nick nickname
10
+ alias from nickname
11
+ alias by nickname
12
+ alias who nickname
13
+
14
+ # Create the event, setting some variables
15
+ def initialize(manager, data)
16
+ @manager = manager
17
+ @data = data
18
+ @manager.logger.debug("Creating instance of #{self.class}")
19
+ end
20
+
21
+ # Checks the nickname in the hostmask
22
+ # to see if you're the one that made this event trigger,
23
+ # or if it was someone else.
24
+ def me?
25
+ end
26
+
27
+ alias is_me? me?
28
+ alias from_me? me?
29
+ alias by_me? me?
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Flexo
2
+ module Events
3
+ class JoinEvent < Event
4
+ attr_reader :channel
5
+ attr_reader :nick
6
+
7
+ def initialize(*args)
8
+ super
9
+ @channel = @data.params[0] || @data.string
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Flexo
2
+ module Events
3
+ class KickEvent < Event
4
+ attr_reader :channel
5
+ attr_reader :kicker
6
+ attr_reader :victim
7
+ attr_reader :reason
8
+
9
+ alias kicked_by kicker
10
+
11
+ def initialize(*args)
12
+ super
13
+ @kicker = @data.hostmask.nick
14
+ @channel = @data.params[0]
15
+ @victim = @data.params[1]
16
+ @reason = @data.string
17
+ end
18
+
19
+ def kicked_me?
20
+ @victim == @manager.nickname
21
+ end
22
+
23
+ def rejoin
24
+ sleep rand*5
25
+ @manager.sender.join(@channel)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module Flexo
2
+ module Events
3
+ class ModeEvent < Event
4
+ attr_reader :modes
5
+
6
+ def initialize(*args)
7
+ super
8
+ @channelmode = Flexo::Constants::CHANPREFIX.include?(@data.params[0][0])
9
+ @modes = @data.params[1..-1].join(' ')
10
+ end
11
+
12
+ def usermode?
13
+ !@channelmode
14
+ end
15
+
16
+ def channelmode?
17
+ @channelmode
18
+ end
19
+
20
+ alias chanmode? channelmode?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ module Flexo
2
+ module Events
3
+ class NickEvent < Event
4
+ attr_reader :old
5
+ attr_reader :new
6
+
7
+ def initialize(*args)
8
+ super
9
+ @old = @nickname
10
+ @new = @data.params[0] || @data.string
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module Flexo
2
+ module Events
3
+ # A little special event. This one is identical to Flexo::Events::PrivmsgEvent,
4
+ # so see that one for documentation.
5
+ class NoticeEvent < PrivmsgEvent
6
+ # We default to not replying to a channel for these.
7
+ def reply(target, to_channel=false)
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Flexo
2
+ module Events
3
+ class PartEvent < Event
4
+ attr_reader :channel
5
+ attr_reader :reason
6
+
7
+ def initialize(*args)
8
+ super
9
+ @channel = @data.params[0]
10
+ @reason = @data.string
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ module Flexo
2
+ module Events
3
+ class PingEvent < Event
4
+ attr_reader :server
5
+
6
+ def initialize(*args)
7
+ super
8
+ @server = @data.params[0] || @data.string
9
+ end
10
+
11
+ def pong
12
+ @manager.sender.pong(@server)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Flexo
2
+ module Events
3
+ class PongEvent < Event
4
+ attr_reader :server
5
+ attr_reader :origin
6
+
7
+ def initialize(*args)
8
+ super
9
+ @server = @data.params[0]
10
+ @origin = @data.params[1] || @data.string
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,46 @@
1
+ module Flexo
2
+ module Events
3
+ # Private messages, which, in fact, isn't neccessarily private.
4
+ # When the target of a privmsg is a channel, it's displayed to the
5
+ # entire channel. Which isn't very private at all.
6
+ class PrivmsgEvent < Event
7
+ attr_reader :sender
8
+ attr_reader :target
9
+ attr_reader :text
10
+ alias from sender
11
+
12
+ def initialize(*args)
13
+ super
14
+ @sender = @data.hostmask.nick if @data.hostmask
15
+ @target = @data.params[0]
16
+ @ctcp = (m = @data.string.match(/^\x01(.+?)\x01$/)) ? true : false
17
+ @text = @ctcp ? m[1] : @data.string
18
+ end
19
+
20
+ # Returns true if this was a CTCP-message, and false if not.
21
+ def ctcp?
22
+ return @ctcp
23
+ end
24
+
25
+ # Were you the target of the message?
26
+ def to_me?
27
+ return @target == @manager.nickname # FIXME: Should this be stored here?
28
+ end
29
+
30
+ # Sent to a channel, or was it actually a private message?
31
+ def to_channel?
32
+ return Flexo::Constants::CHANPREFIX.include?(@target[0..0])
33
+ end
34
+
35
+ # Sends a quick reply to the message.
36
+ # If to_channel is false, it's sent to the person who sent the
37
+ # message, whether he sent it to a channel, or private.
38
+ # If it was sent to you, this will send a direct reply
39
+ # no matter what to_channel is set to.
40
+ def reply(text, to_channel=true)
41
+ target = (to_channel && to_channel?) ? @target : @sender
42
+ @manager.sender.privmsg(target, text)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ module Flexo
2
+ module Events
3
+ class QuitEvent < Event
4
+ attr_reader :reason
5
+
6
+ def initialize(*args)
7
+ super
8
+ @reason = @data.string
9
+ end
10
+ end
11
+ end
12
+ end