ahoy 0.0.2 → 0.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.
- 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
|