ahoy 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ahoy.rb +2 -6
- data/lib/ahoy/broadcast.rb +22 -0
- data/lib/ahoy/chat.rb +148 -35
- data/lib/ahoy/contact.rb +102 -24
- data/lib/ahoy/contact_list.rb +54 -30
- data/lib/ahoy/errors.rb +1 -0
- data/lib/ahoy/user.rb +81 -13
- data/readme.rdoc +1 -9
- metadata +38 -17
- data/lib/ahoy/xmpp4r_hack.rb +0 -41
data/lib/ahoy.rb
CHANGED
@@ -8,11 +8,7 @@ end
|
|
8
8
|
module Ahoy
|
9
9
|
SERVICE_TYPE = "_presence._tcp"
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.add?(reply)
|
16
|
-
reply.flags.to_i & DNSSD::Flags::Add > 0
|
11
|
+
class << self
|
12
|
+
attr_accessor :use_markdown # default for Chat#use_markdown
|
17
13
|
end
|
18
14
|
end
|
data/lib/ahoy/broadcast.rb
CHANGED
@@ -1,13 +1,31 @@
|
|
1
1
|
require File.expand_path("#{File.dirname(__FILE__)}/../ahoy")
|
2
2
|
|
3
3
|
module Ahoy
|
4
|
+
|
5
|
+
# Ahoy::Broadcast provides a simple interface to send a message to all online
|
6
|
+
# users. Example:
|
7
|
+
# user = Ahoy::User.new("Dr. Nick")
|
8
|
+
# cast = Ahoy::Broadcast.new(user)
|
9
|
+
#
|
10
|
+
# cast.send("Hi, everybody!")
|
11
|
+
# cast.close
|
12
|
+
#
|
4
13
|
class Broadcast
|
14
|
+
|
15
|
+
# :call-seq: Broadcast.new(user) -> broadcast
|
16
|
+
#
|
17
|
+
# Create a new Ahoy::Broadcast.
|
18
|
+
#
|
5
19
|
def initialize(user)
|
6
20
|
user.sign_in
|
7
21
|
sleep 1
|
8
22
|
@chats = user.contacts.map {|contact| user.chat(contact)}
|
9
23
|
end
|
10
24
|
|
25
|
+
# :call-seq: broadcast.send(string) -> array
|
26
|
+
#
|
27
|
+
# Send string to all online contacts.
|
28
|
+
#
|
11
29
|
def send(message)
|
12
30
|
@chats.each do |chat|
|
13
31
|
begin
|
@@ -18,6 +36,10 @@ module Ahoy
|
|
18
36
|
end
|
19
37
|
end
|
20
38
|
|
39
|
+
# :call-seq: broadcast.close -> close
|
40
|
+
#
|
41
|
+
# End all chats.
|
42
|
+
#
|
21
43
|
def close
|
22
44
|
@chats.each {|chat| chat.close}
|
23
45
|
end
|
data/lib/ahoy/chat.rb
CHANGED
@@ -1,44 +1,68 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'xmpp4r'
|
3
|
-
require File.expand_path("#{File.dirname(__FILE__)}/xmpp4r_hack")
|
4
3
|
|
5
4
|
module Ahoy
|
5
|
+
|
6
|
+
# Ahoy::Chat models a conversation between the user and one of their contacts.
|
7
|
+
# It can be thought of as representing an iChat chat window, or treated more
|
8
|
+
# like a Ruby socket.
|
9
|
+
#
|
6
10
|
class Chat
|
7
|
-
attr_reader :
|
8
|
-
attr_accessor :client
|
9
|
-
protected :client, :client=
|
11
|
+
attr_reader :contact_name
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
# :call-seq: Chat.new(contact_name) -> chat
|
14
|
+
#
|
15
|
+
# Create a new Ahoy::Chat.
|
16
|
+
#
|
17
|
+
def initialize(contact_name)
|
18
|
+
@contact_name = contact_name
|
14
19
|
@client = nil
|
20
|
+
self.use_markdown = Ahoy.use_markdown
|
21
|
+
end
|
22
|
+
|
23
|
+
# :call-seq: chat.connect(target, port) -> chat
|
24
|
+
# chat.connect(socket) -> chat
|
25
|
+
#
|
26
|
+
# Connect to target on port, or use the connection provided by socket.
|
27
|
+
#
|
28
|
+
def connect(host, port=nil)
|
29
|
+
if host.respond_to?(:read) && host.respond_to?(:write)
|
30
|
+
connect_with(host)
|
31
|
+
else
|
32
|
+
begin
|
33
|
+
client.connect(host, port)
|
34
|
+
rescue Errno::ECONNREFUSED
|
35
|
+
raise Ahoy::ContactOfflineError.new("Contact Offline")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self
|
15
39
|
end
|
16
40
|
|
17
|
-
#
|
41
|
+
# :call-seq: chat.connected? -> bool
|
18
42
|
#
|
19
|
-
def
|
20
|
-
|
21
|
-
connect
|
43
|
+
def connected?
|
44
|
+
client.is_connected?
|
22
45
|
end
|
23
46
|
|
24
|
-
#
|
47
|
+
# :call-seq: chat.send(string) -> message
|
48
|
+
#
|
49
|
+
# Send string to contact. May raise Ahoy::ContactOfflineError.
|
25
50
|
#
|
26
|
-
def send(
|
27
|
-
|
51
|
+
def send(text)
|
52
|
+
raise Ahoy::NotConnectedError.new("Not Connected") unless connected?
|
28
53
|
|
29
|
-
message = Jabber::Message.new(
|
54
|
+
message = Jabber::Message.new(contact_name, text)
|
30
55
|
message.type = :chat
|
31
|
-
|
32
|
-
|
33
|
-
rescue IOError
|
34
|
-
connect
|
35
|
-
retry
|
36
|
-
end
|
56
|
+
markdown(message) if markdown?
|
57
|
+
client.send(message)
|
37
58
|
message
|
38
59
|
end
|
39
60
|
|
61
|
+
# :call-seq: chat.on_reply {|string| block }
|
62
|
+
#
|
63
|
+
# Set up block as a callback for when a message is received.
|
64
|
+
#
|
40
65
|
def on_reply(&block)
|
41
|
-
start unless client
|
42
66
|
client.delete_message_callback("on_reply")
|
43
67
|
|
44
68
|
client.add_message_callback(0, "on_reply") do |message|
|
@@ -46,8 +70,12 @@ module Ahoy
|
|
46
70
|
end
|
47
71
|
end
|
48
72
|
|
49
|
-
|
50
|
-
|
73
|
+
# :call-seq: chat.receive -> string
|
74
|
+
#
|
75
|
+
# Block until a message is received, then return the message body as a
|
76
|
+
# string.
|
77
|
+
#
|
78
|
+
def receive(*ignore_args)
|
51
79
|
thread = Thread.current
|
52
80
|
reply = nil
|
53
81
|
|
@@ -62,23 +90,108 @@ module Ahoy
|
|
62
90
|
client.delete_message_callback("receive")
|
63
91
|
reply
|
64
92
|
end
|
93
|
+
alias read receive
|
94
|
+
alias sysread receive
|
95
|
+
alias gets receive
|
96
|
+
alias readline receive
|
65
97
|
|
98
|
+
# :call-seq: chat.close -> nil
|
99
|
+
#
|
100
|
+
# End the chat.
|
101
|
+
#
|
66
102
|
def close
|
67
103
|
client.close
|
68
|
-
|
104
|
+
@client = nil
|
69
105
|
end
|
70
106
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
107
|
+
# :call-seq: chat.use_markdown = bool -> bool
|
108
|
+
#
|
109
|
+
# Set true to send a html copy of messages, by interpreting the message text
|
110
|
+
# as markdown.
|
111
|
+
#
|
112
|
+
def use_markdown=(value)
|
113
|
+
@use_markdown = value
|
114
|
+
if value && !markdown_processor
|
115
|
+
%W{rdiscount kramdown maruku bluecloth}.each do |lib|
|
116
|
+
begin
|
117
|
+
require lib
|
118
|
+
break
|
119
|
+
rescue LoadError
|
120
|
+
end
|
121
|
+
end
|
81
122
|
end
|
123
|
+
value
|
124
|
+
end
|
125
|
+
|
126
|
+
# :call-seq: chat.markdown? -> bool
|
127
|
+
#
|
128
|
+
# Are messages sent to this chat being interpreted as markdown?
|
129
|
+
#
|
130
|
+
def markdown?
|
131
|
+
@use_markdown && markdown_processor
|
132
|
+
end
|
133
|
+
alias use_markdown markdown?
|
134
|
+
|
135
|
+
# :call-seq: chat << string -> chat
|
136
|
+
#
|
137
|
+
# See #send
|
138
|
+
#
|
139
|
+
def <<(string)
|
140
|
+
send(string)
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
# :call-seq: chat.puts(string) -> nil
|
145
|
+
#
|
146
|
+
# See #send
|
147
|
+
#
|
148
|
+
def puts(string)
|
149
|
+
send(string)
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
# :call-seq: chat.write -> integer
|
154
|
+
#
|
155
|
+
# See #send
|
156
|
+
#
|
157
|
+
def write(string)
|
158
|
+
send(string)
|
159
|
+
string.length
|
160
|
+
end
|
161
|
+
alias syswrite write
|
162
|
+
|
163
|
+
private
|
164
|
+
def connect_with(socket)
|
165
|
+
client.instance_variable_set(:@socket, socket)
|
166
|
+
client.start
|
167
|
+
client.accept_features
|
168
|
+
client.instance_variable_set(:@keepaliveThread, Thread.new do
|
169
|
+
Thread.current.abort_on_exception = true
|
170
|
+
client.__send__(:keepalive_loop)
|
171
|
+
end)
|
172
|
+
end
|
173
|
+
|
174
|
+
def client
|
175
|
+
return @client if @client
|
176
|
+
@client = Jabber::Client.new(nil)
|
177
|
+
@client.features_timeout = 0.001
|
178
|
+
@client
|
179
|
+
end
|
180
|
+
|
181
|
+
def markdown(message)
|
182
|
+
html = REXML::Element.new("html")
|
183
|
+
html.add_attribute("xmlns", "http://www.w3.org/1999/xhtml")
|
184
|
+
body = html.add_element("body")
|
185
|
+
markdown = markdown_processor.new(message.body)
|
186
|
+
body.add_element(REXML::Document.new(markdown.to_html))
|
187
|
+
message.add_element(html)
|
188
|
+
end
|
189
|
+
|
190
|
+
def markdown_processor
|
191
|
+
return RDiscount if defined?(RDiscount)
|
192
|
+
return Kramdown::Document if defined?(Kramdown::Document)
|
193
|
+
return Maruku if defined?(Maruku)
|
194
|
+
return BlueCloth if defined?(BlueCloth)
|
82
195
|
end
|
83
196
|
|
84
197
|
end
|
data/lib/ahoy/contact.rb
CHANGED
@@ -2,56 +2,134 @@ require 'rubygems'
|
|
2
2
|
require 'dnssd'
|
3
3
|
|
4
4
|
module Ahoy
|
5
|
+
|
6
|
+
# Ahoy::Contact represents another user or system, available to recieve
|
7
|
+
# messages, or who may send them to our user.
|
8
|
+
#
|
5
9
|
class Contact
|
6
|
-
attr_reader :name, :domain
|
10
|
+
attr_reader :name, :domain
|
7
11
|
attr_accessor :online
|
12
|
+
alias online? online
|
8
13
|
|
9
|
-
|
14
|
+
# :call-seq: Contact.new(name, domain="local.") -> contact
|
15
|
+
#
|
16
|
+
# Create a new Ahoy::Contact. name should be in name@location format.
|
17
|
+
#
|
18
|
+
def initialize(name, domain="local.")
|
10
19
|
@name = name
|
11
20
|
@domain = domain
|
12
21
|
@target = nil
|
13
|
-
@ip = nil
|
14
22
|
@port = nil
|
15
|
-
@
|
23
|
+
@interface_addresses = {}
|
16
24
|
@online = true
|
17
25
|
end
|
18
26
|
|
27
|
+
# :call-seq: contact.fullname -> string
|
28
|
+
#
|
29
|
+
# Returns the contact's full name in name@location.service.domain format
|
30
|
+
#
|
19
31
|
def fullname
|
20
32
|
[name, Ahoy::SERVICE_TYPE, domain].join(".")
|
21
33
|
end
|
22
34
|
|
23
|
-
|
24
|
-
|
35
|
+
# :call-seq: contact.target -> string
|
36
|
+
#
|
37
|
+
# Return the contact's target attribute. Pass true as the argument to use
|
38
|
+
# the cached value rather than looking it up.
|
39
|
+
#
|
40
|
+
def target(use_cache=nil)
|
41
|
+
resolve(use_cache)
|
42
|
+
@target
|
25
43
|
end
|
26
44
|
|
27
|
-
|
28
|
-
|
45
|
+
# :call-seq: contact.port -> string
|
46
|
+
#
|
47
|
+
# Return the contact's port attribute. Pass true as the argument to use
|
48
|
+
# the cached value rather than looking it up.
|
49
|
+
#
|
50
|
+
def port(use_cache=nil)
|
51
|
+
resolve(use_cache)
|
52
|
+
@port
|
29
53
|
end
|
30
54
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
55
|
+
# :call-seq: contact.interfaces -> array
|
56
|
+
#
|
57
|
+
# Return the contact's interfaces. Pass true as the argument to use the
|
58
|
+
# cached value rather than looking it up.
|
59
|
+
#
|
60
|
+
def interfaces(use_cache=nil)
|
61
|
+
resolve(use_cache)
|
62
|
+
@interface_addresses.keys
|
63
|
+
end
|
64
|
+
|
65
|
+
# Internal use only.
|
66
|
+
#
|
67
|
+
def add_interface(name) # :nodoc:
|
68
|
+
@interface_addresses[name] = [] unless @interface_addresses.key?(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
# :call-seq: contact.ip_addresses(interface=nil)
|
72
|
+
#
|
73
|
+
# Returns all of contact's IP addresses, or if an interface is supplied as
|
74
|
+
# an argument, just the IP addresses for that interface.
|
75
|
+
#
|
76
|
+
# Pass true as the second argument to prevent a lookup of interfaces, pass
|
77
|
+
# true as the third argument to prevent a lookup of IP addresses, and
|
78
|
+
# instead use the cached value.
|
79
|
+
#
|
80
|
+
def ip_addresses(interface=nil, resolve_cache=nil, use_cache=nil)
|
81
|
+
getaddrinfo(resolve_cache) unless use_cache
|
82
|
+
if interface
|
83
|
+
@interface_addresses[interface]
|
84
|
+
else
|
85
|
+
@interface_addresses.values.flatten
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# :call-seq: contact == other_contact -> bool
|
90
|
+
#
|
91
|
+
# Equality. Two contacts are equal if they have the same fullname (and
|
92
|
+
# therefore name, location, service, and domain).
|
93
|
+
#
|
94
|
+
def ==(other)
|
95
|
+
other.is_a?(self.class) && other.fullname == fullname
|
96
|
+
end
|
97
|
+
|
98
|
+
# :call-seq: contact.resolve -> contact
|
99
|
+
#
|
100
|
+
# Determine and set the contact's target, port, and interfaces.
|
101
|
+
#
|
102
|
+
def resolve(use_cache=nil)
|
103
|
+
if use_cache && @target && @port && @interface_addresses.keys.any?
|
104
|
+
return self
|
105
|
+
end
|
106
|
+
@interface_addresses.clear
|
107
|
+
DNSSD::Service.new.resolve(name, Ahoy::SERVICE_TYPE, domain) do |resolved|
|
37
108
|
@target = resolved.target
|
38
109
|
@port = resolved.port
|
39
|
-
@interface =
|
40
|
-
|
110
|
+
@interface_addresses[resolved.interface] = []
|
111
|
+
break unless resolved.flags.more_coming?
|
41
112
|
end
|
42
|
-
Thread.stop unless service.stopped?
|
43
113
|
self
|
44
114
|
end
|
45
115
|
|
46
|
-
|
116
|
+
# :call-seq: contact.getaddrinfo(interface=nil) -> self
|
117
|
+
#
|
118
|
+
# Determine and set the contact's IP addresses. If an interface is passed,
|
119
|
+
# only lookup the IP addresses for that interface.
|
120
|
+
#
|
121
|
+
# Pass true as the second argument to prevent a resolve.
|
122
|
+
#
|
123
|
+
def getaddrinfo(interface=nil, resolve_cache=nil)
|
124
|
+
unless interface
|
125
|
+
interfaces(resolve_cache).each {|inter| getaddrinfo(inter, true)}
|
126
|
+
return self
|
127
|
+
end
|
47
128
|
service = DNSSD::Service.new
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
@ip = addressed.address
|
52
|
-
main.run
|
129
|
+
service.getaddrinfo(target(resolve_cache), 0, 0, interface) do |addressed|
|
130
|
+
@interface_addresses[addressed.interface].push(addressed.address)
|
131
|
+
break unless addressed.flags.more_coming?
|
53
132
|
end
|
54
|
-
Thread.stop unless service.stopped?
|
55
133
|
self
|
56
134
|
end
|
57
135
|
|
data/lib/ahoy/contact_list.rb
CHANGED
@@ -2,14 +2,23 @@ require 'thread'
|
|
2
2
|
require 'weakref'
|
3
3
|
|
4
4
|
module Ahoy
|
5
|
+
|
6
|
+
# Ahoy::ContactList is a self-populating collection of Contacts, and provides
|
7
|
+
# methods to retrieve and iterate over its contents.
|
8
|
+
#
|
5
9
|
class ContactList
|
6
10
|
include Enumerable
|
7
11
|
|
8
|
-
attr_reader :list, :weak_list, :lock, :
|
12
|
+
attr_reader :list, :weak_list, :lock, :user_name
|
9
13
|
private :list, :weak_list, :lock
|
10
14
|
|
11
|
-
|
12
|
-
|
15
|
+
# :call-seq: ContactList.new(user_name=nil) -> contact_list
|
16
|
+
#
|
17
|
+
# Create a new Ahoy::ContactList. Provide a username as the argument to
|
18
|
+
# avoid adding our user to the list.
|
19
|
+
#
|
20
|
+
def initialize(user_name=nil)
|
21
|
+
@user_name = user_name
|
13
22
|
@list = []
|
14
23
|
@weak_list = []
|
15
24
|
@lock = Mutex.new
|
@@ -17,38 +26,52 @@ module Ahoy
|
|
17
26
|
start_browse
|
18
27
|
end
|
19
28
|
|
29
|
+
# :call-seq: contact_list.each {|contact| block } -> contact_list
|
30
|
+
#
|
31
|
+
# Calls block once for each contact in the contact list.
|
32
|
+
#
|
20
33
|
def each(&block)
|
21
|
-
lock.synchronize
|
22
|
-
|
23
|
-
|
34
|
+
lock.synchronize {list.each(&block)}
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# :call-seq: contact_list[name] -> contact or nil
|
39
|
+
#
|
40
|
+
# Returns the first contact who's fullname or name matches name.
|
41
|
+
#
|
42
|
+
# The case equality operator (===) is used in the comparison, so strings or
|
43
|
+
# regexps can be used as the argument.
|
44
|
+
#
|
45
|
+
def [](name)
|
46
|
+
find {|c| name === c.fullname || name === c.name}
|
47
|
+
end
|
48
|
+
alias find_by_name []
|
49
|
+
|
50
|
+
# :call-seq: contact_list.find_by_ip(string) -> contact or nil
|
51
|
+
#
|
52
|
+
# Returns the first contact with the ip address matching string.
|
53
|
+
#
|
54
|
+
def find_by_ip(ip)
|
55
|
+
find {|contact| contact.ip_addresses.include?(ip)}
|
24
56
|
end
|
25
57
|
|
26
58
|
private
|
27
59
|
def start_browse
|
28
60
|
DNSSD.browse(Ahoy::SERVICE_TYPE) do |browsed|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
61
|
+
if browsed.flags.add? && browsed.name != user_name
|
62
|
+
existing = self[browsed.fullname]
|
63
|
+
contact = existing || find_in_weak_list(browsed.fullname) ||
|
64
|
+
Ahoy::Contact.new(browsed.name, browsed.domain)
|
65
|
+
contact.online = true
|
66
|
+
contact.add_interface(browsed.interface)
|
67
|
+
lock.synchronize {list.push(contact)} unless existing
|
34
68
|
else
|
35
69
|
remove(browsed.fullname)
|
36
70
|
end
|
37
71
|
end
|
38
72
|
end
|
39
73
|
|
40
|
-
def add(contact)
|
41
|
-
lock.synchronize do
|
42
|
-
unless list.find {|in_list| contact == in_list}
|
43
|
-
contact ||= find_in_weak_list(contact)
|
44
|
-
contact.online = true
|
45
|
-
list.push(contact)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
74
|
def remove(fullname)
|
51
|
-
fullname = fullname.fullname if fullname.respond_to?(:fullname)
|
52
75
|
lock.synchronize do
|
53
76
|
contact = list.find {|c| c.fullname == fullname}
|
54
77
|
if contact
|
@@ -60,16 +83,17 @@ module Ahoy
|
|
60
83
|
end
|
61
84
|
end
|
62
85
|
|
63
|
-
def find_in_weak_list(
|
64
|
-
existing_contact = nil
|
86
|
+
def find_in_weak_list(fullname)
|
65
87
|
Thread.exclusive do
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
88
|
+
begin
|
89
|
+
GC.disable
|
90
|
+
weak_list.reject! {|ref| !ref.weakref_alive?}
|
91
|
+
refrence = weak_list.find {|ref| fullname == ref.fullname}
|
92
|
+
refrence.__getobj__ if refrence
|
93
|
+
ensure
|
94
|
+
GC.enable
|
95
|
+
end
|
71
96
|
end
|
72
|
-
existing_contact
|
73
97
|
end
|
74
98
|
|
75
99
|
end
|
data/lib/ahoy/errors.rb
CHANGED
data/lib/ahoy/user.rb
CHANGED
@@ -1,28 +1,65 @@
|
|
1
|
+
require 'socket'
|
1
2
|
require 'rubygems'
|
2
3
|
require 'dnssd'
|
3
4
|
|
4
5
|
module Ahoy
|
6
|
+
|
7
|
+
# Ahoy::User represents us, or the current system, and is the entry point for
|
8
|
+
# using the Ahoy library.
|
9
|
+
#
|
10
|
+
# Send a message to a specific example:
|
11
|
+
# user = Ahoy::User.new("Ford")
|
12
|
+
# user.sign_in
|
13
|
+
#
|
14
|
+
# chat = user.chat(user.contacts[/Arthur/])
|
15
|
+
# chat.send("Don't panic")
|
16
|
+
#
|
17
|
+
# Simple echo server:
|
18
|
+
# user = Ahoy::User.new("echo")
|
19
|
+
# user.sign_in
|
20
|
+
#
|
21
|
+
# user.on_chat do |chat|
|
22
|
+
# chat.on_reply do |reply|
|
23
|
+
# chat.send(reply)
|
24
|
+
# end
|
25
|
+
# end.join
|
26
|
+
#
|
27
|
+
#
|
5
28
|
class User
|
6
|
-
attr_reader :short_name, :location, :domain, :contacts
|
7
|
-
attr_accessor :port, :flags, :interface
|
29
|
+
attr_reader :display_name, :short_name, :location, :domain, :contacts
|
30
|
+
attr_accessor :port, :flags, :interface
|
8
31
|
|
9
|
-
|
10
|
-
|
11
|
-
|
32
|
+
# :call-seq: User.new(name, location="nowhere", domain="local.") -> user
|
33
|
+
#
|
34
|
+
# Create a new Ahoy::User.
|
35
|
+
#
|
36
|
+
# Location should be set to the bonjour/zeroconf hostname.
|
37
|
+
#
|
38
|
+
def initialize(display_name, location="nowhere", domain="local.")
|
39
|
+
@display_name = display_name
|
40
|
+
@short_name = display_name.downcase.gsub(/ /, "-").gsub(/[^a-z0-9-]/, "")
|
41
|
+
@location = location.downcase.gsub(/ /, "-").gsub(/[^a-z0-9-]/, "")
|
12
42
|
@domain = domain
|
13
43
|
|
14
|
-
@contacts = Ahoy::ContactList.new(
|
15
|
-
@contact = nil
|
44
|
+
@contacts = Ahoy::ContactList.new(name)
|
16
45
|
|
17
46
|
@port = 5562
|
18
47
|
@flags = 0
|
19
48
|
@interface = DNSSD::InterfaceAny
|
20
49
|
end
|
21
50
|
|
51
|
+
# :call-seq: user.name -> string
|
52
|
+
#
|
53
|
+
# The user's name, in name@location format.
|
54
|
+
#
|
22
55
|
def name
|
23
56
|
"#{short_name}@#{location}"
|
24
57
|
end
|
25
58
|
|
59
|
+
# :call-seq: user.sign_in(status="avail", msg=nil) -> user
|
60
|
+
#
|
61
|
+
# Register user as 'on-line' and available to send/receive messages.
|
62
|
+
#
|
26
63
|
def sign_in(status="avail", msg=nil)
|
27
64
|
@registrar = DNSSD.register(
|
28
65
|
name,
|
@@ -32,15 +69,42 @@ module Ahoy
|
|
32
69
|
txt_record(status, msg),
|
33
70
|
flags.to_i,
|
34
71
|
interface)
|
72
|
+
self
|
35
73
|
end
|
36
74
|
|
37
|
-
|
38
|
-
|
39
|
-
|
75
|
+
# :call-seq: user.chat(contact) -> chat
|
76
|
+
#
|
77
|
+
# Initiate a new chat session with contact.
|
78
|
+
#
|
79
|
+
def chat(contact)
|
80
|
+
chat = Ahoy::Chat.new(contact.name)
|
81
|
+
chat.connect(contact.target, contact.port(true))
|
40
82
|
end
|
41
83
|
|
42
|
-
|
43
|
-
|
84
|
+
# :call-seq: user.listen -> chat
|
85
|
+
#
|
86
|
+
# Listen for an incoming chat. This method will block until a chat is
|
87
|
+
# recieved.
|
88
|
+
#
|
89
|
+
def listen
|
90
|
+
socket = server.accept
|
91
|
+
domain, port, hostname, ip = socket.peeraddr
|
92
|
+
Ahoy::Chat.new(name, contacts.find_by_ip(ip).name).connect(socket)
|
93
|
+
end
|
94
|
+
|
95
|
+
# :call-seq: user.on_chat {|chat| block } -> thread
|
96
|
+
#
|
97
|
+
# Set up block as a callback for when a chat is initiated by a contact.
|
98
|
+
#
|
99
|
+
# This method does not block, but does return a thread, which can be joined
|
100
|
+
# if you wish to block.
|
101
|
+
#
|
102
|
+
def on_chat(&block)
|
103
|
+
Thread.new do
|
104
|
+
while chat = listen
|
105
|
+
block.call(chat)
|
106
|
+
end
|
107
|
+
end
|
44
108
|
end
|
45
109
|
|
46
110
|
private
|
@@ -50,7 +114,11 @@ module Ahoy
|
|
50
114
|
"port.p2pj" => port,
|
51
115
|
"status" => status,
|
52
116
|
"msg" => msg,
|
53
|
-
"1st" =>
|
117
|
+
"1st" => display_name)
|
118
|
+
end
|
119
|
+
|
120
|
+
def server
|
121
|
+
@server ||= TCPServer.new("0.0.0.0", port)
|
54
122
|
end
|
55
123
|
|
56
124
|
end
|
data/readme.rdoc
CHANGED
@@ -9,8 +9,6 @@ The Bonjour chat protocol is pretty much XMPP with the presence and server parts
|
|
9
9
|
|
10
10
|
Ahoy isn't much more than a wrapper with a nice API around the dnssd and xmpp4r gems.
|
11
11
|
|
12
|
-
The API is based around the idea that this is an instant messaging protocol, there is a user, that user has a list of contacts, you can start a chat with a contact and send messages the contact though that chat session. This may be refined, but is unlikely to change in it's outlook.
|
13
|
-
|
14
12
|
Example:
|
15
13
|
|
16
14
|
require 'rubygems'
|
@@ -52,13 +50,7 @@ Along with the blocking Chat#receive method to receive replies, there is an on_r
|
|
52
50
|
sleep 1
|
53
51
|
end
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
user = Ahoy::User.new("mat")
|
58
|
-
user.interface = "en1" # on Mac OS X en1 is usually Wi-Fi, en0 is ethernet
|
59
|
-
user.sign_in
|
60
|
-
|
61
|
-
...
|
53
|
+
Messages can be formatted using markdown, simply set Chat#use_markdown (or Ahoy.use_markdown for the global default). Ahoy will use any of the following markdown processors, in order of preference: rdiscount, kramdown, maruku, bluecloth.
|
62
54
|
|
63
55
|
The current use case is to send a message to a team of developers when a deploy script is run, there is a simplified interface for this:
|
64
56
|
|
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ahoy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- Mat Sadler
|
@@ -9,29 +15,39 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date:
|
18
|
+
date: 2011-05-07 00:00:00 +01:00
|
13
19
|
default_executable:
|
14
20
|
dependencies:
|
15
21
|
- !ruby/object:Gem::Dependency
|
16
22
|
name: dnssd
|
17
|
-
|
18
|
-
|
19
|
-
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
20
26
|
requirements:
|
21
|
-
- -
|
27
|
+
- - ~>
|
22
28
|
- !ruby/object:Gem::Version
|
23
|
-
|
24
|
-
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 0
|
33
|
+
version: "2.0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
25
36
|
- !ruby/object:Gem::Dependency
|
26
37
|
name: xmpp4r
|
27
|
-
|
28
|
-
|
29
|
-
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
30
41
|
requirements:
|
31
42
|
- - "="
|
32
43
|
- !ruby/object:Gem::Version
|
44
|
+
hash: 1
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
- 5
|
33
48
|
version: "0.5"
|
34
|
-
|
49
|
+
type: :runtime
|
50
|
+
version_requirements: *id002
|
35
51
|
description: Serverless Messaging using DNSDS/mDNS, XMPP, and Ruby
|
36
52
|
email: mat@sourcetagsandcodes.com
|
37
53
|
executables: []
|
@@ -47,11 +63,10 @@ files:
|
|
47
63
|
- lib/ahoy/contact_list.rb
|
48
64
|
- lib/ahoy/errors.rb
|
49
65
|
- lib/ahoy/user.rb
|
50
|
-
- lib/ahoy/xmpp4r_hack.rb
|
51
66
|
- lib/ahoy.rb
|
52
67
|
- readme.rdoc
|
53
68
|
has_rdoc: true
|
54
|
-
homepage: http://
|
69
|
+
homepage: http://github.com/matsadler/ahoy
|
55
70
|
licenses: []
|
56
71
|
|
57
72
|
post_install_message:
|
@@ -61,21 +76,27 @@ rdoc_options:
|
|
61
76
|
require_paths:
|
62
77
|
- lib
|
63
78
|
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
64
80
|
requirements:
|
65
81
|
- - ">="
|
66
82
|
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
67
86
|
version: "0"
|
68
|
-
version:
|
69
87
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
70
89
|
requirements:
|
71
90
|
- - ">="
|
72
91
|
- !ruby/object:Gem::Version
|
92
|
+
hash: 3
|
93
|
+
segments:
|
94
|
+
- 0
|
73
95
|
version: "0"
|
74
|
-
version:
|
75
96
|
requirements: []
|
76
97
|
|
77
98
|
rubyforge_project:
|
78
|
-
rubygems_version: 1.3.
|
99
|
+
rubygems_version: 1.3.7
|
79
100
|
signing_key:
|
80
101
|
specification_version: 3
|
81
102
|
summary: Bonjour Chat for Ruby
|
data/lib/ahoy/xmpp4r_hack.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'xmpp4r'
|
3
|
-
|
4
|
-
module Jabber
|
5
|
-
class Connection
|
6
|
-
def connect(host, port, local_host=nil, local_port=nil)
|
7
|
-
@host = host
|
8
|
-
@port = port
|
9
|
-
# Reset is_tls?, so that it works when reconnecting
|
10
|
-
@tls = false
|
11
|
-
|
12
|
-
Jabber::debuglog("CONNECTING:\n#{@host}:#{@port}, local #{local_host}:#{local_port}")
|
13
|
-
@socket = TCPSocket.new(@host, @port, local_host, local_port)
|
14
|
-
|
15
|
-
# We want to use the old and deprecated SSL protocol (usually on port 5223)
|
16
|
-
if @use_ssl
|
17
|
-
ssl = OpenSSL::SSL::SSLSocket.new(@socket)
|
18
|
-
ssl.connect # start SSL session
|
19
|
-
ssl.sync_close = true
|
20
|
-
Jabber::debuglog("SSL connection established.")
|
21
|
-
@socket = ssl
|
22
|
-
end
|
23
|
-
|
24
|
-
start
|
25
|
-
|
26
|
-
accept_features
|
27
|
-
|
28
|
-
@keepaliveThread = Thread.new do
|
29
|
-
Thread.current.abort_on_exception = true
|
30
|
-
keepalive_loop
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
class Client
|
36
|
-
def connect(host, port, local_host=nil, local_port=nil)
|
37
|
-
super
|
38
|
-
self
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|