hector 1.0.0

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.
@@ -0,0 +1,28 @@
1
+ module Hector
2
+ module Commands
3
+ module Whois
4
+ def on_whois
5
+ nickname = request.args.first
6
+ if session = Session.find(nickname)
7
+ respond_to_whois_for(self.nickname, session)
8
+ else
9
+ raise NoSuchNickOrChannel, nickname
10
+ end
11
+ ensure
12
+ respond_with("318", self.nickname, nickname, "End of /WHOIS list.")
13
+ end
14
+
15
+ def respond_to_whois_for(destination, session)
16
+ respond_with("301", session.nickname, :text => session.away_message) if session.away?
17
+ respond_with("311", destination, session.nickname, session.whois)
18
+ respond_with("319", destination, session.nickname, :text => channels.map { |channel| channel.name }.join(" ")) unless channels.empty?
19
+ respond_with("312", destination, session.nickname, Hector.server_name, :text => "Hector")
20
+ respond_with("317", destination, session.nickname, session.seconds_idle, session.created_at, :text => "seconds idle, signon time")
21
+ end
22
+
23
+ def whois
24
+ "#{nickname} #{identity.username} #{Hector.server_name} * :#{realname}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ module Hector
2
+ module Concerns
3
+ module Authentication
4
+ def on_user
5
+ @username = request.args.first
6
+ @realname = request.text
7
+ authenticate
8
+ end
9
+
10
+ def on_pass
11
+ @password = request.text
12
+ authenticate
13
+ end
14
+
15
+ def on_nick
16
+ @nickname = request.text
17
+ authenticate
18
+ end
19
+
20
+ protected
21
+ def authenticate
22
+ set_identity
23
+ set_session
24
+ end
25
+
26
+ def set_identity
27
+ if @username && @password && !@identity
28
+ Identity.authenticate(@username, @password) do |identity|
29
+ if @identity = identity
30
+ set_session
31
+ else
32
+ error InvalidPassword
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def set_session
39
+ if @identity && @nickname
40
+ @session = UserSession.create(@nickname, self, @identity, @realname)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module Hector
2
+ module Concerns
3
+ module KeepAlive
4
+ def initialize_keep_alive
5
+ @received_pong = true
6
+ @heartbeat = Hector::Heartbeat.new { on_heartbeat }
7
+ end
8
+
9
+ def on_heartbeat
10
+ if @received_pong
11
+ @received_pong = false
12
+ respond_with(:ping, Hector.server_name)
13
+ else
14
+ @quit_message = "Ping timeout"
15
+ connection.close_connection(true)
16
+ end
17
+ end
18
+
19
+ def destroy_keep_alive
20
+ @heartbeat.stop
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ module Hector
2
+ module Concerns
3
+ module Presence
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ attr_reader :created_at, :updated_at
7
+ end
8
+ end
9
+
10
+ def channels
11
+ Channel.find_all_for_session(self)
12
+ end
13
+
14
+ def initialize_presence
15
+ @created_at = Time.now
16
+ @updated_at = Time.now
17
+ deliver_welcome_message
18
+ end
19
+
20
+ def destroy_presence
21
+ deliver_quit_message
22
+ leave_all_channels
23
+ end
24
+
25
+ def seconds_idle
26
+ Time.now - updated_at
27
+ end
28
+
29
+ def peer_sessions
30
+ [self, *channels.map { |channel| channel.sessions }.flatten].uniq
31
+ end
32
+
33
+ def touch_presence
34
+ @updated_at = Time.now
35
+ end
36
+
37
+ protected
38
+ def deliver_welcome_message
39
+ respond_with("001", nickname, :text => "Welcome to IRC")
40
+ respond_with("422", :text => "MOTD File is missing")
41
+ end
42
+
43
+ def deliver_quit_message
44
+ broadcast(:quit, :source => source, :text => quit_message, :except => self)
45
+ respond_with(:error, :text => "Closing Link: #{nickname}[hector] (#{quit_message})")
46
+ end
47
+
48
+ def leave_all_channels
49
+ channels.each do |channel|
50
+ channel.part(self)
51
+ end
52
+ end
53
+
54
+ def quit_message
55
+ @quit_message || "Connection closed"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,85 @@
1
+ module Hector
2
+ class Connection < EventMachine::Protocols::LineAndTextProtocol
3
+ include Concerns::Authentication
4
+
5
+ attr_reader :session, :request, :identity
6
+
7
+ def post_init
8
+ log(:info, "opened connection")
9
+ end
10
+
11
+ def receive_line(line)
12
+ @request = Request.new(line)
13
+ log(:debug, "received", @request.to_s.inspect) unless @request.sensitive?
14
+
15
+ if session
16
+ session.receive(request)
17
+ else
18
+ if respond_to?(request.event_name)
19
+ send(request.event_name)
20
+ else
21
+ close_connection(true)
22
+ end
23
+ end
24
+
25
+ rescue IrcError => e
26
+ handle_error(e)
27
+
28
+ rescue Exception => e
29
+ log(:error, [e, *e.backtrace].join("\n"))
30
+
31
+ ensure
32
+ @request = nil
33
+ end
34
+
35
+ def unbind
36
+ session.destroy if session
37
+ log(:info, "closing connection")
38
+ end
39
+
40
+ def respond_with(response, *args)
41
+ response = Response.new(response, *args) unless response.is_a?(Response)
42
+ send_data(response.to_s)
43
+ log(:debug, "sent", response.to_s.inspect)
44
+ end
45
+
46
+ def handle_error(error)
47
+ respond_with(error.response)
48
+ close_connection(true) if error.fatal?
49
+ end
50
+
51
+ def error(klass, *args)
52
+ handle_error(klass.new(*args))
53
+ end
54
+
55
+ def log(level, *args)
56
+ Hector.logger.send(level, [log_tag, *args].join(" "))
57
+ end
58
+
59
+ def address
60
+ peer_info[1]
61
+ end
62
+
63
+ def port
64
+ peer_info[0]
65
+ end
66
+
67
+ protected
68
+ def peer_info
69
+ @peer_info ||= Socket.unpack_sockaddr_in(get_peername)
70
+ end
71
+
72
+ def log_tag
73
+ "[#{address}:#{port}]".tap do |tag|
74
+ tag << " (#{session.nickname})" if session
75
+ end
76
+ end
77
+ end
78
+
79
+ class SSLConnection < Connection
80
+ def post_init
81
+ log(:info, "opened SSL connection")
82
+ start_tls
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,11 @@
1
+ module Hector
2
+ class << self
3
+ def defer(&block)
4
+ EM.defer(&block)
5
+ end
6
+
7
+ def next_tick(&block)
8
+ EM.next_tick(&block)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module Hector
2
+ class Error < ::StandardError; end
3
+ class LoadError < Error; end
4
+
5
+ class IrcError < Error
6
+ def response
7
+ Response.new(command, *options).tap do |response|
8
+ response.args.push(message) unless message == self.class.name
9
+ end
10
+ end
11
+ end
12
+
13
+ def self.IrcError(command, *options)
14
+ fatal = options.last.is_a?(Hash) && options.last.delete(:fatal)
15
+ Class.new(IrcError).tap do |klass|
16
+ klass.class_eval do
17
+ define_method(:command) { command.dup }
18
+ define_method(:options) { options.dup }
19
+ define_method(:fatal?) { fatal }
20
+ end
21
+ end
22
+ end
23
+
24
+ class NoSuchNickOrChannel < IrcError("401", :text => "No such nick/channel"); end
25
+ class NoSuchChannel < IrcError("403", :text => "No such channel"); end
26
+ class CannotSendToChannel < IrcError("404", :text => "Cannot send to channel"); end
27
+ class ErroneousNickname < IrcError("432", :text => "Erroneous nickname"); end
28
+ class NicknameInUse < IrcError("433", "*", :text => "Nickname is already in use"); end
29
+ class InvalidPassword < IrcError("464", :text => "Invalid password", :fatal => true); end
30
+ end
@@ -0,0 +1,25 @@
1
+ module Hector
2
+ class Heartbeat
3
+ def self.create_timer(interval, &block)
4
+ EventMachine::PeriodicTimer.new(interval, &block)
5
+ end
6
+
7
+ def initialize(interval = 60, &block)
8
+ @interval, @block = interval, block
9
+ start
10
+ end
11
+
12
+ def start
13
+ @timer ||= self.class.create_timer(@interval) { pulse }
14
+ end
15
+
16
+ def pulse
17
+ @block.call
18
+ end
19
+
20
+ def stop
21
+ @timer.cancel
22
+ @timer = nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Hector
2
+ class Identity
3
+ attr_accessor :username
4
+
5
+ class << self
6
+ attr_accessor :adapter
7
+
8
+ def authenticate(username, password)
9
+ adapter.authenticate(username, password) do |authenticated|
10
+ yield authenticated ? new(username) : nil
11
+ end
12
+ end
13
+ end
14
+
15
+ def initialize(username)
16
+ @username = username
17
+ end
18
+
19
+ def ==(identity)
20
+ Identity.adapter.normalize(username) == Identity.adapter.normalize(identity.username)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Hector
2
+ class NullLogger
3
+ def level=(l) end
4
+ def debug(*) end
5
+ def info(*) end
6
+ def warn(*) end
7
+ def error(*) end
8
+ def fatal(*) end
9
+ end
10
+
11
+ class << self
12
+ attr_accessor :logger
13
+ end
14
+
15
+ self.logger = NullLogger.new
16
+ end
@@ -0,0 +1,42 @@
1
+ module Hector
2
+ class Request
3
+ attr_reader :line, :command, :args, :text
4
+ alias_method :to_s, :line
5
+
6
+ def initialize(line)
7
+ @line = line
8
+ parse
9
+ end
10
+
11
+ def event_name
12
+ "on_#{command.downcase}"
13
+ end
14
+
15
+ def sensitive?
16
+ command.downcase == "pass"
17
+ end
18
+
19
+ protected
20
+ def parse
21
+ source = line.dup
22
+ @command = extract!(source, /^ *([^ ]+)/, "").upcase
23
+ @text = extract!(source, / :(.*)$/)
24
+ @args = source.strip.split(" ")
25
+
26
+ if @text
27
+ @args.push(@text)
28
+ else
29
+ @text = @args.last
30
+ end
31
+ end
32
+
33
+ def extract!(line, regex, default = nil)
34
+ result = nil
35
+ line.gsub!(regex) do |match|
36
+ result = $~[1]
37
+ ""
38
+ end
39
+ result || default
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ module Hector
2
+ class Response
3
+ attr_reader :command, :args, :source
4
+ attr_accessor :text
5
+
6
+ class << self
7
+ def apportion_text(args, *base_args)
8
+ base_response = Response.new(*base_args)
9
+ max_length = 510 - base_response.to_s.length
10
+
11
+ args.inject([args.shift.dup]) do |texts, arg|
12
+ if texts.last.length + arg.length + 1 >= max_length
13
+ texts << arg.dup
14
+ else
15
+ texts.last << " " << arg
16
+ end
17
+ texts
18
+ end.map do |text|
19
+ base_response.dup.tap do |response|
20
+ response.text = text
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def initialize(command, *args)
27
+ @command = command.to_s.upcase
28
+ @args = args
29
+
30
+ options = args.pop if args.last.is_a?(Hash)
31
+ @text = options[:text] if options
32
+ @source = options[:source] if options
33
+ end
34
+
35
+ def event_name
36
+ "received_#{command.downcase}"
37
+ end
38
+
39
+ def to_s
40
+ [].tap do |line|
41
+ line.push(":#{source}") if source
42
+ line.push(command)
43
+ line.concat(args)
44
+ line.push(":#{text}") if text
45
+ end.join(" ")[0, 510] + "\r\n"
46
+ end
47
+ end
48
+ end