PerfectlyNormal-Flexo 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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