xrc 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1765652f98f9ded0ba8dc89c729babf653461914
4
+ data.tar.gz: d2192424e3c6737e616d2774d02fefa485ba029c
5
+ SHA512:
6
+ metadata.gz: 26d909993287c8bef933efc2b7f5a6121216240dc107e31c55410d7eefba4ee3c900a4bdf66476e62c952ef59ef47ac791d01d2730e7b597b06d507e8cc0cbd8
7
+ data.tar.gz: d1278831dabc08f37324389d1532bfd16c73599ded497b2118d3c1b3fc41d588dbdca9b511eaeda7042a79f90939c14ee8e9d4c3f168eed8a28a7958d9e78134
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in xrc.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ryo Nakamura
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Xrc
2
+ XMPP Ruby Client.
3
+
4
+ ## Usage
5
+ ```ruby
6
+ # Loads all classes & modules defined in this library.
7
+ require "xrc"
8
+
9
+ # Constructs a new Client class to connect to a XMPP server.
10
+ client = Xrc::Client.new(
11
+ jid: "foo@example.com", # required
12
+ nickname: "bot" # optional
13
+ password: "xxx", # optional
14
+ port: 5222, # optional, default: 5222
15
+ room_jid: "bar@example.com", # optional
16
+ )
17
+
18
+ # Responds to "ping" and returns "pong".
19
+ client.on_private_message do |message|
20
+ if message.body == "ping"
21
+ client.reply(body: "pong", to: message)
22
+ end
23
+ end
24
+
25
+ # Responds to "Thank you" and returns "You're welcome".
26
+ client.on_room_message do |message|
27
+ if message.body == "Thank you"
28
+ client.reply(body: "You're welcome", to: message)
29
+ end
30
+ end
31
+
32
+ # Connects to a XMPP server and waits for new messages.
33
+ client.connect
34
+ ```
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/lib/xrc.rb ADDED
@@ -0,0 +1,43 @@
1
+ require "active_support/core_ext/enumerable"
2
+ require "active_support/core_ext/object/try"
3
+ require "active_support/core_ext/string/indent"
4
+ require "base64"
5
+ require "openssl"
6
+ require "ostruct"
7
+ require "resolv"
8
+ require "rexml/document"
9
+ require "rexml/element"
10
+ require "rexml/parsers/sax2parser"
11
+ require "securerandom"
12
+ require "socket"
13
+
14
+ require "xrc/method_loggable"
15
+ require "xrc/client"
16
+ require "xrc/connection"
17
+ require "xrc/connection/connector"
18
+ require "xrc/connection/tsl_connector"
19
+ require "xrc/elements/auth"
20
+ require "xrc/elements/bind"
21
+ require "xrc/elements/message"
22
+ require "xrc/elements/presence"
23
+ require "xrc/elements/join"
24
+ require "xrc/elements/ping"
25
+ require "xrc/elements/roster"
26
+ require "xrc/elements/session"
27
+ require "xrc/elements/starttls"
28
+ require "xrc/elements/stream"
29
+ require "xrc/hosts_resolver"
30
+ require "xrc/message_builder"
31
+ require "xrc/messages/base"
32
+ require "xrc/messages/null"
33
+ require "xrc/messages/message"
34
+ require "xrc/messages/private_message"
35
+ require "xrc/messages/room_message"
36
+ require "xrc/messages/subject"
37
+ require "xrc/namespaces"
38
+ require "xrc/jid"
39
+ require "xrc/parser"
40
+ require "xrc/roster"
41
+ require "xrc/socket"
42
+ require "xrc/user"
43
+ require "xrc/version"
data/lib/xrc/client.rb ADDED
@@ -0,0 +1,318 @@
1
+ module Xrc
2
+ class Client
3
+ DEFAULT_PORT = 5222
4
+
5
+ PING_INTERVAL = 30
6
+
7
+ # @return [Xrc::Roster] Users information existing in the server
8
+ attr_reader :users
9
+
10
+ # @option options [String] :port Port number to connect server (default: 5222)
11
+ # @option options [String] :jid Jabber ID of your account (required)
12
+ # @option options [String] :nickname Pass nickname for the Room (optional)
13
+ # @option options [String] :password Password to connect server (optional)
14
+ # @option options [String] :room_jid Room Jabber ID to join in after authentication (optional)
15
+ # @example
16
+ # client = Xrc::Client.new(jid: "alice@example.com")
17
+ def initialize(options = {})
18
+ @options = options
19
+ end
20
+
21
+ # Connects to the JID's server and waits for message
22
+ # @return [nil] Returns nothing
23
+ def connect
24
+ connection.connect
25
+ nil
26
+ end
27
+
28
+ # Registers a callback called when client received a new private message (a.k.a. 1vs1 message)
29
+ # @yield Executes a given callback in the Client's context
30
+ # @yieldparam element [Xrc::Message] Represents a given message
31
+ # @return [Proc] Returns given block
32
+ # @example
33
+ # client.on_private_message do |message|
34
+ # puts "#{message.from}: #{message.body}"
35
+ # end
36
+ #
37
+ def on_private_message(&block)
38
+ @on_private_message_block = block
39
+ end
40
+
41
+ # Registers a callback called when client received a new room message
42
+ # @yield Executes a given callback in the Client's context
43
+ # @yieldparam element [Xrc::Message] Represents a given message
44
+ # @return [Proc] Returns given block
45
+ # @example
46
+ # client.on_room_message do |message|
47
+ # puts "#{message.from}: #{message.body}"
48
+ # end
49
+ #
50
+ def on_room_message(&block)
51
+ @on_room_message_block = block
52
+ end
53
+
54
+ # Registers a callback called when client received a new message with subject
55
+ # @yield Executes a given callback in the Client's context
56
+ # @yieldparam element [Xrc::Message] Represents a given message
57
+ # @return [Proc] Returns given block
58
+ # @example
59
+ # client.on_subject do |element|
60
+ # puts "Subject: #{element.subject}"
61
+ # end
62
+ #
63
+ def on_subject(&block)
64
+ @on_subject_block = block
65
+ end
66
+
67
+ # Registers a callback called when client received a new XML element from server
68
+ # @yield Executes a given callback in the Client's context
69
+ # @yieldparam element [REXML::Element] Represents a new XML element
70
+ # @return [Proc] Returns given block
71
+ # @example
72
+ # client.on_event do |element|
73
+ # puts "Received #{element}"
74
+ # end
75
+ #
76
+ def on_event(&block)
77
+ @on_event_block = block
78
+ end
79
+
80
+ # Replies to given message
81
+ # @option options [Xrc::Messages::Base] :to A message object given from server
82
+ # @option options [String] :body A text to be sent to server
83
+ # @return [REXML::Element] Returns an element sent to server
84
+ # @example
85
+ # client.reply(body: "Thanks", to: message)
86
+ def reply(options)
87
+ say(
88
+ body: options[:body],
89
+ from: options[:to].to,
90
+ to: options[:to].from,
91
+ type: options[:to].type,
92
+ )
93
+ end
94
+
95
+ # @return [String] Mention name of this account
96
+ # @example
97
+ # client.mention_name #=> "alice"
98
+ def mention_name
99
+ users[jid].try(:mention_name)
100
+ end
101
+
102
+ private
103
+
104
+ def on_event_block
105
+ @on_event_block ||= ->(element) {}
106
+ end
107
+
108
+ def on_private_message_block
109
+ @on_private_message_block ||= ->(element) {}
110
+ end
111
+
112
+ def on_room_message_block
113
+ @on_room_message_block ||= ->(element) {}
114
+ end
115
+
116
+ def on_subject_block
117
+ @on_subject_block ||= ->(element) {}
118
+ end
119
+
120
+ def on_received(element)
121
+ on_event_block.call(element)
122
+ case
123
+ when element.attribute("id") && has_reply_callbacks_to?(element.attribute("id").value)
124
+ on_replied(element)
125
+ when element.name == "message"
126
+ on_room_message_received(element)
127
+ when element.prefix == "stream" && element.name == "features"
128
+ on_features_received(element)
129
+ when element.name == "proceed" && element.namespace == Namespaces::TLS
130
+ on_tls_proceeded(element)
131
+ when element.name == "success" && element.namespace == Namespaces::SASL
132
+ on_authentication_succeeded(element)
133
+ when element.name == "failure" && element.namespace == Namespaces::SASL
134
+ on_authentication_failed(element)
135
+ end
136
+ end
137
+
138
+ def jid
139
+ @jid ||= Jid.new(@options[:jid])
140
+ end
141
+
142
+ def password
143
+ @options[:password]
144
+ end
145
+
146
+ def nickname
147
+ @options[:nickname]
148
+ end
149
+
150
+ def port
151
+ @options[:port] || DEFAULT_PORT
152
+ end
153
+
154
+ def room_jid
155
+ Jid.new("#{@options[:room_jid]}") if @options[:room_jid]
156
+ end
157
+
158
+ def connection
159
+ @connection ||= Connection.new(domain: jid.domain, port: port) do |element|
160
+ on_received(element)
161
+ end
162
+ end
163
+
164
+ def on_room_message_received(element)
165
+ case message = MessageBuilder.build(element)
166
+ when Messages::RoomMessage
167
+ on_room_message_block.call(message)
168
+ when Messages::PrivateMessage
169
+ on_private_message_block.call(message)
170
+ when Messages::Subject
171
+ on_subject_block.call(message)
172
+ end
173
+ end
174
+
175
+ def on_bound(element)
176
+ @jid = Jid.new(element.elements["/bind/jid/text()"].value)
177
+ establish_session
178
+ require_roster
179
+ end
180
+
181
+ def on_replied(element)
182
+ id = element.attribute("id").value
183
+ callback = reply_callbacks.delete(id)
184
+ callback.call(element)
185
+ end
186
+
187
+ def on_features_received(element)
188
+ element.each do |feature|
189
+ case
190
+ when feature.name == "bind" && feature.namespace == Namespaces::BIND
191
+ bind
192
+ when feature.name == "starttls" && feature.namespace == Namespaces::TLS
193
+ start_tls
194
+ when feature.name == "mechanisms" && feature.namespace == Namespaces::SASL
195
+ on_mechanisms_received(feature)
196
+ else
197
+ features[feature.name] = feature.namespace
198
+ end
199
+ end
200
+ end
201
+
202
+ def on_authentication_succeeded(element)
203
+ connection.open
204
+ end
205
+
206
+ def on_authentication_failed(element)
207
+ raise NotImplementedError
208
+ end
209
+
210
+ def on_tls_proceeded(element)
211
+ connection.encrypt
212
+ end
213
+
214
+ def on_mechanisms_received(element)
215
+ element.each_element("mechanism") do |mechanism|
216
+ mechanisms << mechanism.text
217
+ end
218
+ authenticate if password
219
+ end
220
+
221
+ def on_roster_received(element)
222
+ @users = Roster.new(element)
223
+ attend
224
+ on_connection_established
225
+ end
226
+
227
+ def on_connection_established
228
+ join if room_jid
229
+ start_ping_thread
230
+ end
231
+
232
+ def features
233
+ @features ||= {}
234
+ end
235
+
236
+ def mechanisms
237
+ @mechanisms ||= []
238
+ end
239
+
240
+ def reply_callbacks
241
+ @reply_callbacks ||= {}
242
+ end
243
+
244
+ def has_reply_callbacks_to?(id)
245
+ reply_callbacks.has_key?(id)
246
+ end
247
+
248
+ def post(element, &block)
249
+ if block
250
+ id = generate_id
251
+ element.add_attributes("id" => id)
252
+ reply_callbacks[id] = block
253
+ end
254
+ connection.write(element)
255
+ element
256
+ end
257
+
258
+ # See RFC1750 for Randomness Recommendations for Security
259
+ def generate_id
260
+ SecureRandom.hex(8)
261
+ end
262
+
263
+ def bind
264
+ post(Elements::Bind.new(resource: jid.resource), &method(:on_bound))
265
+ end
266
+
267
+ def establish_session
268
+ post(Elements::Session.new)
269
+ end
270
+
271
+ def require_roster
272
+ post(Elements::Roster.new, &method(:on_roster_received))
273
+ end
274
+
275
+ def attend
276
+ post(Elements::Presence.new)
277
+ end
278
+
279
+ def join
280
+ post(Elements::Join.new(from: jid.strip, to: "#{room_jid}/#{nickname}"))
281
+ end
282
+
283
+ def ping
284
+ post(Elements::Ping.new(from: jid.to_s, to: jid.domain))
285
+ end
286
+
287
+ def start_tls
288
+ post(Elements::Starttls.new)
289
+ end
290
+
291
+ def start
292
+ post(Elements::Stream.new(domain: jid.domain))
293
+ end
294
+
295
+ # Only supports PLAIN authentication
296
+ def authenticate
297
+ post(Elements::Auth.new(jid: jid, password: password))
298
+ end
299
+
300
+ def say(options)
301
+ post Elements::Message.new(
302
+ body: options[:body],
303
+ from: options[:from],
304
+ to: options[:to],
305
+ type: options[:type],
306
+ )
307
+ end
308
+
309
+ def start_ping_thread
310
+ Thread.new do
311
+ loop do
312
+ ping
313
+ sleep(PING_INTERVAL)
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end