Pulse 1.1.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,27 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Channel
5
+ attr_accessor :name, :users
6
+
7
+ def initialize name, client, users = []
8
+ @name, @client, @users = name, client, users
9
+
10
+ users.each { |user| user.channel = self }
11
+ end
12
+
13
+ def say message
14
+ @client.say self, message
15
+ end
16
+
17
+ def user name; @users.find { |user| user.name == name } end
18
+ def user? name; not user(name).nil? end
19
+
20
+ def to_s; @name end
21
+ def to_yaml opts = {}; @name.to_yaml opts end
22
+
23
+ def inspect
24
+ %{#<#{self.class.name} @name=#{@name.inspect} @users=#{@users.inspect}}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+
3
+ require 'pulse/handling'
4
+
5
+ module Pulse
6
+ class Client
7
+ include Handling
8
+
9
+ attr_accessor :scripts
10
+
11
+ def initialize options
12
+ @scripts = []
13
+ @channels = {}
14
+ @settings = Settings.new options
15
+ @callbacks = {}
16
+ @connection = Connection.new self, @settings
17
+ @conversations = {}
18
+
19
+ load_scripts
20
+ end
21
+
22
+ def connect
23
+ trap 2 do
24
+ unload_scripts
25
+ transmit :QUIT, "I was interrupted"
26
+ end
27
+ @connection.establish
28
+ end
29
+
30
+ def got_command command
31
+ name = :"got_#{command.name.downcase}"
32
+ puts "<< #{command}"
33
+
34
+ if respond_to? name
35
+ __send__ name, command
36
+ end
37
+ end
38
+
39
+ def connection_established connection
40
+ puts "Connection has been established."
41
+
42
+ transmit :NICK, @settings.nickname
43
+ transmit :USER, @settings.username, ?*, ?*, @settings.realname
44
+ end
45
+
46
+ def connection_terminated connection
47
+ puts "The connection was terminated."
48
+ end
49
+
50
+ def say recipient, line
51
+ transmit :PRIVMSG, recipient.to_s, line
52
+ end
53
+
54
+ def load_scripts
55
+ @settings.scripts.each do |path|
56
+ @scripts.<< Script.new path, self
57
+ end
58
+ end
59
+
60
+ def unload_scripts
61
+ @scripts.each &:unload!
62
+ end
63
+
64
+ def each_user name
65
+ @channels.values.select { |c| c.user? name }.each do |channel|
66
+ yield channel.user name
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def emit name, *args
73
+ @callbacks[name].each do |callback|
74
+ callback.call *args
75
+ end if @callbacks[name]
76
+
77
+
78
+ begin
79
+ @scripts.each do |script|
80
+ if script.respond_to? name
81
+ script.__send__ name, *args
82
+ end
83
+ end
84
+ rescue
85
+ puts "Script error #$!"
86
+ end
87
+ end
88
+
89
+ def catch name, &block
90
+ (@callbacks[name] ||= []) << block
91
+ end
92
+
93
+ def transmit name, *args
94
+ @connection.transmit name, *args
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ostruct'
4
+
5
+ module Pulse
6
+ class Command
7
+ attr_accessor :name, :params, :prefix
8
+
9
+ Pattern = /^(?:[:@]([^\s]+) )?([^\s]+)(?: ((?:[^:\s][^\s]* ?)*))?(?: ?:(.*))?$/
10
+
11
+ def self.parse data
12
+ match = data.strip.match Pattern
13
+ prefix, name, args, extra = match.captures
14
+ params = extra ? args.split << extra : args.split
15
+
16
+ new(name, params).tap do |this|
17
+ this.prefix = prefix
18
+ end
19
+ end
20
+
21
+ def initialize name, params = []
22
+ @name, @params = name, params
23
+ end
24
+
25
+ def [] index; @params[index] end
26
+
27
+ def sender
28
+ return @sender if @sender
29
+
30
+ if prefix =~ /^(\S+)!(\S+)@(\S+)$/
31
+ @sender = OpenStruct.new nickname: $1, username: $2, hostname: $3
32
+ else
33
+ @sender = prefix
34
+ end
35
+ end
36
+
37
+ def to_s
38
+ String.new.tap do |line|
39
+ line << "#{prefix} " if prefix
40
+ line << name.to_s
41
+
42
+ params.each_with_index do |param, index|
43
+ line << ' '
44
+ line << ?: if index == params.length - 1 and param =~ /[ :]/
45
+ line << param.to_s
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Connection
5
+ def initialize delegate, settings
6
+ @delegate = delegate
7
+ @settings = settings
8
+ @queue = Queue.new
9
+ end
10
+
11
+ def establish
12
+ TCPSocket.open @settings.hostname, @settings.port do |socket|
13
+ Thread.start socket, &@queue.method(:process)
14
+ @delegate.connection_established self
15
+
16
+ until socket.eof?
17
+ @delegate.got_command Command.parse socket.gets
18
+ end
19
+ end
20
+
21
+ @delegate.connection_terminated self
22
+ end
23
+
24
+ def transmit name, *arguments
25
+ Command.new(name, arguments).tap { |this| @queue << this }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Conversation
5
+ attr_accessor :id, :recipient
6
+
7
+ def initialize recipient, client = nil
8
+ @id, @recipient, @client = random_id, recipient, client
9
+ end
10
+
11
+ def say message
12
+ @client.say @recipient.name, message
13
+ end
14
+
15
+ def name; @recipient.name end
16
+
17
+ def inspect
18
+ %{#<#{self.class.name} @recipient=#{@recipient}>}
19
+ end
20
+
21
+ private
22
+ def random_id
23
+ chars = 'abcdefghijklmnopqrstu0123456789'
24
+
25
+ String.new.tap do |result|
26
+ 6.times { result << chars[rand chars.length] }
27
+ end.to_sym
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ class String
4
+ alias_method :starts_with?, :start_with?
5
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Client
5
+ module Handling
6
+
7
+ protected
8
+ # End of MOTD
9
+ def got_end_of_motd command
10
+ emit :connection_ready, @connection
11
+ end
12
+
13
+ # The /NAMES list
14
+ def got_353 command
15
+ users = command[3].split.map &User.method(:new)
16
+
17
+ if channel = @channels[command[2]]
18
+ users.each &channel.users.method(:<<)
19
+ users.each { |user| user.channel = channel }
20
+ else
21
+ @channels[command[2]] ||= Channel.new(command[2], self, users)
22
+ end
23
+ p users, command[3]
24
+ end
25
+
26
+ # The IRCd is checking whether or not we're still alive
27
+ # PING :1285409133
28
+ # PONG :1285409133
29
+ def got_ping command
30
+ transmit :PONG, command[0]
31
+ end
32
+
33
+ # Someone has changed their nickname
34
+ # mk!mk@maero.dk NICK mk_
35
+ def got_nick command
36
+ each_user command.sender.nickname do |user|
37
+ emit :rename, user, command[0]
38
+ user.name = command[0]
39
+ end
40
+ end
41
+
42
+ # Someone has sent a message, it can be both a private message and
43
+ # a channel message
44
+ # mk!mk@maero.dk PRIVMSG #maero :tp: kod
45
+ def got_privmsg command
46
+ name, message = command.params
47
+
48
+ if channel = @channels[name]
49
+ user = channel.user command.sender.nickname
50
+ user.synchronize command.sender
51
+ emit :message, user, channel, message
52
+ else
53
+ user = User.new command.sender.nickname
54
+
55
+ (@conversations[command.sender.nickname] ||= Conversation.new user, self).tap do |conversation|
56
+ user.channel = conversation
57
+ emit :conversation, user, conversation, message
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ # Someone has entered a channel.
64
+ def got_join command
65
+ if channel = @channels[command[0]]
66
+ user = User.new command.sender.nickname
67
+ user.channel = channel
68
+ user.synchronize command.sender
69
+ channel.users << user
70
+ emit :user_joined, user, channel
71
+ end
72
+ end
73
+
74
+ # Someone has left a channel.
75
+ def got_part command
76
+ if channel = @channels[command[0]]
77
+ user = channel.user command.sender.nickname
78
+ channel.users.delete user
79
+ emit :user_left, user, channel
80
+ end
81
+ end
82
+
83
+ alias_method :got_422, :got_end_of_motd
84
+ alias_method :got_376, :got_end_of_motd
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Queue
5
+ def initialize
6
+ @queue = []
7
+ end
8
+
9
+ def process socket
10
+ @socket ||= socket
11
+ @thread = Thread.current
12
+
13
+ while @thread.alive?
14
+ if command = @queue.shift
15
+ puts ">> #{command}"
16
+ @socket.write "#{command}\n"
17
+ else
18
+ Thread.stop
19
+ end
20
+ end
21
+ end
22
+
23
+ def << command
24
+ @queue << command
25
+ @thread.run if @thread
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Script
5
+ class Cache
6
+ def initialize script
7
+ @script = script
8
+ @containers = {}
9
+ end
10
+
11
+ def save
12
+ create_directory
13
+
14
+ File.open path, ?w do |file|
15
+ YAML.dump @containers, file
16
+ end
17
+
18
+ puts "Dumping cache for script #{@script.inspect} to #{path} …"
19
+ end
20
+
21
+ def load
22
+ if yaml = YAML.load_file(path)
23
+ @containers = yaml
24
+
25
+ puts "Imported cache from #{path} …"
26
+ end
27
+ rescue
28
+
29
+ puts "The cache is corrupt. Removing."
30
+ File.unlink path
31
+ end
32
+
33
+ def containers; @containers.keys end
34
+ def clear sure = false; @containers.clear if sure end
35
+
36
+ def [] key; @containers[key] ||= {} end
37
+ def []= key, value; @containers[key] ||= value end
38
+
39
+ private
40
+
41
+ def create_directory
42
+ Dir.mkdir File.dirname path unless File.directory? File.dirname path
43
+ end
44
+
45
+ def path
46
+ "#{File.dirname File.expand_path $0}/cache/#{@script.name}.yml"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Script < Module
5
+
6
+ attr_accessor :name, :author, :version
7
+
8
+ def initialize path, client
9
+ @path, @client = path, client
10
+ @loaded = false
11
+
12
+ evaluate_script
13
+ cache.load if has_cache?
14
+ __send__ :loaded if respond_to? :loaded
15
+ end
16
+
17
+ def Script name, version = [1, 0, 0], author = nil, &block
18
+ @name, @version, @author = name, version, author
19
+ instance_eval &block
20
+ end
21
+
22
+ def loaded?; @loaded == true end
23
+
24
+ def unload!
25
+ cache.save if @cache
26
+ @client.scripts.delete self
27
+ __send__ :unloaded if respond_to? :unloaded
28
+ end
29
+
30
+ def to_yaml opts = {}; @name.to_yaml opts end
31
+
32
+ def inspect
33
+ %{#<#{self.class.name} @name=#{@name.inspect} @version=#{@version.inspect} @author=#{@author.inspect}}
34
+ end
35
+
36
+ def has_cache?
37
+ File.exists? "#{File.expand_path File.dirname $0}/cache/#@name.yml"
38
+ end
39
+
40
+ private
41
+
42
+ def evaluate_script
43
+ @loaded = true if module_eval File.read(@path), File.basename(@path), 0
44
+ rescue
45
+ puts "Parse error: #{$!.message}"
46
+ end
47
+
48
+ def cache; @cache ||= Cache.new self end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class Settings < Hash
5
+ class Error < StandardError; end
6
+
7
+ attr_accessor :nickname, :username, :realname, :hostname, :password
8
+
9
+ def initialize options = {}
10
+ @options = options
11
+
12
+ @nickname = options[:nickname] or raise Error, 'No nickname given'
13
+ @username = options[:username] || @nickname
14
+ @realname = options[:realname] || @username
15
+ @hostname = options[:hostname] or raise Error, 'No hostname given'
16
+
17
+ @script_path = options[:path] || File.expand_path(File.dirname $0) + '/scripts'
18
+ end
19
+
20
+ def port; @options[:port] or secure ? 6697 : 6667 end
21
+ def secure?; @options[:secure] == true end
22
+ def password?; not @options[:password].nil? end
23
+
24
+ def scripts
25
+ Dir.glob "#@script_path/*.rb"
26
+ end
27
+
28
+ def method_missing name, *args; @options[name] end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ module Pulse
4
+ class User
5
+ attr_accessor :name, :user, :host, :channel
6
+
7
+ def initialize name
8
+ @name = name.sub /^\W/, ''
9
+ end
10
+
11
+ def synchronize sender
12
+ @name, @user, @host = sender.nickname, sender.username, sender.hostname
13
+ end
14
+
15
+ def to_s; @name end
16
+ def to_yaml opts = {}; @name.to_yaml opts end
17
+
18
+ def inspect
19
+ %{#<#{self.class.name} @name=#{@name.inspect} @channel=#{@channel.name.inspect}>}
20
+ end
21
+ end
22
+ end
data/library/pulse.rb ADDED
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+ require 'socket'
5
+ require 'pulse/user'
6
+ require 'pulse/queue'
7
+ require 'pulse/client'
8
+ require 'pulse/script'
9
+ require 'pulse/channel'
10
+ require 'pulse/command'
11
+ require 'pulse/settings'
12
+ require 'pulse/connection'
13
+ require 'pulse/enhancements'
14
+ require 'pulse/conversation'
15
+ require 'pulse/script/cache'
16
+
17
+ Thread.abort_on_exception = true
18
+
19
+ module Pulse
20
+ class << Version = [1,1]
21
+ def to_s; join ?. end
22
+ end
23
+
24
+ class << self
25
+ def connect options, &block
26
+ puts "=> Pulse #{Pulse::Version}"
27
+
28
+ Client.new(options).tap do |client|
29
+ client.instance_eval &block
30
+ end.connect
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Pulse
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 1
8
+ - 0
9
+ version: 1.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Mikkel Kroman
13
+ autorequire:
14
+ bindir: executables
15
+ cert_chain: []
16
+
17
+ date: 2010-11-25 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email: mk@maero.dk
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - library/pulse/client.rb
31
+ - library/pulse/handling.rb
32
+ - library/pulse/connection.rb
33
+ - library/pulse/script.rb
34
+ - library/pulse/queue.rb
35
+ - library/pulse/command.rb
36
+ - library/pulse/settings.rb
37
+ - library/pulse/script/cache.rb
38
+ - library/pulse/user.rb
39
+ - library/pulse/conversation.rb
40
+ - library/pulse/channel.rb
41
+ - library/pulse/enhancements.rb
42
+ - library/pulse.rb
43
+ has_rdoc: true
44
+ homepage:
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - library
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 1
59
+ - 9
60
+ - 1
61
+ version: 1.9.1
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.7
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Extensible IRC library
77
+ test_files: []
78
+