nelumba 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.travis.yml +9 -0
- data/Gemfile +20 -0
- data/README.md +242 -0
- data/Rakefile +7 -0
- data/assets/lotus_logo_purple.png +0 -0
- data/assets/lotus_logo_purple.svg +262 -0
- data/lib/nelumba.rb +47 -0
- data/lib/nelumba/activity.rb +250 -0
- data/lib/nelumba/application.rb +11 -0
- data/lib/nelumba/article.rb +11 -0
- data/lib/nelumba/atom/account.rb +50 -0
- data/lib/nelumba/atom/address.rb +56 -0
- data/lib/nelumba/atom/author.rb +176 -0
- data/lib/nelumba/atom/category.rb +41 -0
- data/lib/nelumba/atom/comment.rb +96 -0
- data/lib/nelumba/atom/entry.rb +216 -0
- data/lib/nelumba/atom/feed.rb +198 -0
- data/lib/nelumba/atom/generator.rb +40 -0
- data/lib/nelumba/atom/link.rb +79 -0
- data/lib/nelumba/atom/name.rb +57 -0
- data/lib/nelumba/atom/organization.rb +62 -0
- data/lib/nelumba/atom/person.rb +179 -0
- data/lib/nelumba/atom/portable_contacts.rb +117 -0
- data/lib/nelumba/atom/source.rb +179 -0
- data/lib/nelumba/atom/thread.rb +60 -0
- data/lib/nelumba/audio.rb +39 -0
- data/lib/nelumba/badge.rb +11 -0
- data/lib/nelumba/binary.rb +52 -0
- data/lib/nelumba/bookmark.rb +30 -0
- data/lib/nelumba/category.rb +49 -0
- data/lib/nelumba/collection.rb +34 -0
- data/lib/nelumba/comment.rb +47 -0
- data/lib/nelumba/crypto.rb +144 -0
- data/lib/nelumba/device.rb +11 -0
- data/lib/nelumba/discover.rb +362 -0
- data/lib/nelumba/event.rb +57 -0
- data/lib/nelumba/feed.rb +173 -0
- data/lib/nelumba/file.rb +43 -0
- data/lib/nelumba/generator.rb +53 -0
- data/lib/nelumba/group.rb +11 -0
- data/lib/nelumba/identity.rb +63 -0
- data/lib/nelumba/image.rb +30 -0
- data/lib/nelumba/link.rb +56 -0
- data/lib/nelumba/note.rb +34 -0
- data/lib/nelumba/notification.rb +229 -0
- data/lib/nelumba/object.rb +251 -0
- data/lib/nelumba/person.rb +306 -0
- data/lib/nelumba/place.rb +34 -0
- data/lib/nelumba/product.rb +30 -0
- data/lib/nelumba/publisher.rb +44 -0
- data/lib/nelumba/question.rb +30 -0
- data/lib/nelumba/review.rb +30 -0
- data/lib/nelumba/service.rb +11 -0
- data/lib/nelumba/subscription.rb +117 -0
- data/lib/nelumba/version.rb +3 -0
- data/lib/nelumba/video.rb +43 -0
- data/nelumba.gemspec +28 -0
- data/spec/activity_spec.rb +116 -0
- data/spec/application_spec.rb +136 -0
- data/spec/article_spec.rb +136 -0
- data/spec/atom/comment_spec.rb +455 -0
- data/spec/atom/feed_spec.rb +684 -0
- data/spec/audio_spec.rb +164 -0
- data/spec/badge_spec.rb +136 -0
- data/spec/binary_spec.rb +218 -0
- data/spec/bookmark.rb +150 -0
- data/spec/collection_spec.rb +152 -0
- data/spec/comment_spec.rb +128 -0
- data/spec/crypto_spec.rb +126 -0
- data/spec/device_spec.rb +136 -0
- data/spec/event_spec.rb +239 -0
- data/spec/feed_spec.rb +252 -0
- data/spec/file_spec.rb +190 -0
- data/spec/group_spec.rb +136 -0
- data/spec/helper.rb +10 -0
- data/spec/identity_spec.rb +67 -0
- data/spec/image_spec.rb +150 -0
- data/spec/link_spec.rb +30 -0
- data/spec/note_spec.rb +163 -0
- data/spec/notification_spec.rb +89 -0
- data/spec/person_spec.rb +244 -0
- data/spec/place_spec.rb +162 -0
- data/spec/product_spec.rb +150 -0
- data/spec/question_spec.rb +156 -0
- data/spec/review_spec.rb +149 -0
- data/spec/service_spec.rb +136 -0
- data/spec/video_spec.rb +164 -0
- data/test/example_feed.atom +393 -0
- data/test/example_feed_empty_author.atom +336 -0
- data/test/example_feed_false_connected.atom +359 -0
- data/test/example_feed_link_without_href.atom +134 -0
- data/test/example_page.html +4 -0
- data/test/mime_type_bug_feed.atom +874 -0
- metadata +288 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module Nelumba
|
2
|
+
class Comment
|
3
|
+
include Nelumba::Object
|
4
|
+
|
5
|
+
# Holds a collection of Nelumba::Activity's that this comment is in reply to.
|
6
|
+
attr_reader :in_reply_to
|
7
|
+
|
8
|
+
# Create a new Comment activity object.
|
9
|
+
#
|
10
|
+
# options:
|
11
|
+
# :content => The body of the comment in HTML.
|
12
|
+
# :author => A Nelumba::Person that created the object.
|
13
|
+
# :display_name => A natural-language, human-readable and plain-text name
|
14
|
+
# for the comment.
|
15
|
+
# :summary => Natural-language summarization of the comment.
|
16
|
+
# :url => The canonical url of this comment.
|
17
|
+
# :uid => The unique id that identifies this comment.
|
18
|
+
# :image =>
|
19
|
+
# :published => The Time when this comment was originally published.
|
20
|
+
# :updated => The Time when this comment was last modified.
|
21
|
+
def initialize(options = {}, &blk)
|
22
|
+
init(options, &blk)
|
23
|
+
end
|
24
|
+
|
25
|
+
def init(options = {}, &blk)
|
26
|
+
@in_reply_to = options[:in_reply_to] || []
|
27
|
+
|
28
|
+
super options
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a Hash representing this comment.
|
32
|
+
def to_hash
|
33
|
+
{
|
34
|
+
:in_reply_to => @in_reply_to
|
35
|
+
}.merge(super)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a Hash representing this comment with JSON ActivityStreams
|
39
|
+
# conventions.
|
40
|
+
def to_json_hash
|
41
|
+
{
|
42
|
+
:objectType => "comment",
|
43
|
+
:in_reply_to => @in_reply_to
|
44
|
+
}.merge(super)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Nelumba
|
2
|
+
module Crypto
|
3
|
+
require 'openssl'
|
4
|
+
require 'rsa'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
KeyPair = Struct.new(:public_key, :private_key)
|
8
|
+
|
9
|
+
# Generate a new RSA keypair with the given bitlength.
|
10
|
+
def self.new_keypair(bits = 2048)
|
11
|
+
keypair = KeyPair.new
|
12
|
+
|
13
|
+
key = RSA::KeyPair.generate(bits)
|
14
|
+
|
15
|
+
public_key = key.public_key
|
16
|
+
m = public_key.modulus
|
17
|
+
e = public_key.exponent
|
18
|
+
|
19
|
+
modulus = ""
|
20
|
+
until m == 0 do
|
21
|
+
modulus << [m % 256].pack("C")
|
22
|
+
m >>= 8
|
23
|
+
end
|
24
|
+
modulus.reverse!
|
25
|
+
|
26
|
+
exponent = ""
|
27
|
+
until e == 0 do
|
28
|
+
exponent << [e % 256].pack("C")
|
29
|
+
e >>= 8
|
30
|
+
end
|
31
|
+
exponent.reverse!
|
32
|
+
|
33
|
+
keypair.public_key = "RSA.#{Base64::urlsafe_encode64(modulus)}.#{Base64::urlsafe_encode64(exponent)}"
|
34
|
+
|
35
|
+
tmp_private_key = key.private_key
|
36
|
+
m = tmp_private_key.modulus
|
37
|
+
e = tmp_private_key.exponent
|
38
|
+
|
39
|
+
modulus = ""
|
40
|
+
until m == 0 do
|
41
|
+
modulus << [m % 256].pack("C")
|
42
|
+
m >>= 8
|
43
|
+
end
|
44
|
+
modulus.reverse!
|
45
|
+
|
46
|
+
exponent = ""
|
47
|
+
until e == 0 do
|
48
|
+
exponent << [e % 256].pack("C")
|
49
|
+
e >>= 8
|
50
|
+
end
|
51
|
+
exponent.reverse!
|
52
|
+
|
53
|
+
keypair.private_key = "RSA.#{Base64::urlsafe_encode64(modulus)}.#{Base64::urlsafe_encode64(exponent)}"
|
54
|
+
|
55
|
+
keypair
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates an EMSA signature for the given plaintext and key.
|
59
|
+
def self.emsa_sign(text, private_key)
|
60
|
+
private_key = generate_key(private_key) unless private_key.is_a? RSA::Key
|
61
|
+
signature = self.emsa_signature(text, private_key)
|
62
|
+
self.decrypt(private_key, signature)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Verifies an existing EMSA signature.
|
66
|
+
def self.emsa_verify(text, signature, public_key)
|
67
|
+
# RSA encryption is needed to compare the signatures
|
68
|
+
public_key = generate_key(public_key) unless public_key.is_a? RSA::Key
|
69
|
+
|
70
|
+
# Get signature to check
|
71
|
+
emsa = self.emsa_signature(text, public_key)
|
72
|
+
|
73
|
+
# Get signature in payload
|
74
|
+
emsa_signature = self.encrypt(public_key, signature)
|
75
|
+
|
76
|
+
# RSA gem drops leading 0s since it does math upon an Integer
|
77
|
+
# As a workaround, I check for what I expect the second byte to be (\x01)
|
78
|
+
# This workaround will also handle seeing a \x00 first if the RSA gem is
|
79
|
+
# fixed.
|
80
|
+
if emsa_signature.getbyte(0) == 1
|
81
|
+
emsa_signature = "\x00#{emsa_signature}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Does the signature match?
|
85
|
+
# Return the result.
|
86
|
+
emsa_signature == emsa
|
87
|
+
end
|
88
|
+
|
89
|
+
# Decrypts the given data with the given private key.
|
90
|
+
def self.decrypt(private_key, data)
|
91
|
+
private_key = generate_key(private_key) unless private_key.is_a? RSA::Key
|
92
|
+
keypair = generate_keypair(nil, private_key)
|
93
|
+
keypair.decrypt(data)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Encrypts the given data with the given public key.
|
97
|
+
def self.encrypt(public_key, data)
|
98
|
+
public_key = generate_key(public_key) unless public_key.is_a? RSA::Key
|
99
|
+
keypair = generate_keypair(public_key, nil)
|
100
|
+
keypair.encrypt(data)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# :nodoc:
|
106
|
+
def self.emsa_signature(text, key)
|
107
|
+
modulus_byte_count = key.modulus.size
|
108
|
+
|
109
|
+
plaintext = Digest::SHA2.new(256).digest(text)
|
110
|
+
|
111
|
+
prefix = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"
|
112
|
+
padding_count = modulus_byte_count - prefix.bytes.count - plaintext.bytes.count - 3
|
113
|
+
|
114
|
+
padding = ""
|
115
|
+
padding_count.times do
|
116
|
+
padding = padding + "\xff"
|
117
|
+
end
|
118
|
+
|
119
|
+
"\x00\x01#{padding}\x00#{prefix}#{plaintext}".force_encoding('binary')
|
120
|
+
end
|
121
|
+
|
122
|
+
# :nodoc:
|
123
|
+
def self.generate_key(key_string)
|
124
|
+
return nil unless key_string
|
125
|
+
|
126
|
+
key_string.match /^RSA\.(.*?)\.(.*)$/
|
127
|
+
|
128
|
+
modulus = decode_key($1)
|
129
|
+
exponent = decode_key($2)
|
130
|
+
|
131
|
+
RSA::Key.new(modulus, exponent)
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.generate_keypair(public_key, private_key)
|
135
|
+
RSA::KeyPair.new(private_key, public_key)
|
136
|
+
end
|
137
|
+
|
138
|
+
# :nodoc:
|
139
|
+
def self.decode_key(encoded_key_part)
|
140
|
+
modulus = Base64::urlsafe_decode64(encoded_key_part)
|
141
|
+
modulus.bytes.inject(0) {|num, byte| (num << 8) | byte }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,362 @@
|
|
1
|
+
module Nelumba
|
2
|
+
module Discover
|
3
|
+
require 'nelumba/atom/feed'
|
4
|
+
require 'net/http'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
# The order to respect atom links
|
8
|
+
MIME_ORDER = ['application/atom+xml',
|
9
|
+
'application/rss+xml',
|
10
|
+
'application/xml']
|
11
|
+
|
12
|
+
# Will yield an OStatus::Identity for the given fully qualified name
|
13
|
+
# (i.e. "user@domain.tld")
|
14
|
+
def self.identity(name)
|
15
|
+
xrd = nil
|
16
|
+
|
17
|
+
if name.match /^https?:\/\//
|
18
|
+
url = name
|
19
|
+
type = 'text/html'
|
20
|
+
response = Nelumba::Discover.pull_url(url, type)
|
21
|
+
|
22
|
+
# Look at HTTP link headers
|
23
|
+
if response["Link"]
|
24
|
+
link = response["Link"]
|
25
|
+
|
26
|
+
new_url = link[/^<([^>]+)>/,1]
|
27
|
+
rel = link[/;\s*rel\s*=\s*"([^"]+)"/,1]
|
28
|
+
type = link[/;\s*type\s*=\s*"([^"]+)"/,1]
|
29
|
+
|
30
|
+
if new_url.start_with? "/"
|
31
|
+
domain = url[/^(http[s]?:\/\/[^\/]+)\//,1]
|
32
|
+
new_url = "#{domain}#{new_url}"
|
33
|
+
end
|
34
|
+
|
35
|
+
if rel == "lrdd"
|
36
|
+
Nelumba::Discover.identity_from_xml(new_url)
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
elsif name.match /@/
|
42
|
+
Nelumba::Discover.identity_from_webfinger(name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Retrieves a Nelumba::Identity from the given webfinger account.
|
47
|
+
def self.identity_from_webfinger(acct)
|
48
|
+
# We allow a port, unusually. Some exaxmples:
|
49
|
+
#
|
50
|
+
# acct: 'acct:wilkie@example.org:9292'
|
51
|
+
# or: 'acct:wilkie@example.org'
|
52
|
+
# or: 'wilkie@example.org'
|
53
|
+
|
54
|
+
# Remove acct: prefix if it exists
|
55
|
+
acct.gsub!(/^acct\:/, "")
|
56
|
+
|
57
|
+
# Get domain and port
|
58
|
+
matches = acct.match /([^@]+)@([^:]+)(:\d+)?$/
|
59
|
+
username = matches[1]
|
60
|
+
domain = matches[2]
|
61
|
+
port = matches[3] || "" # will include the ':'
|
62
|
+
|
63
|
+
accept = ['application/xml+xrd',
|
64
|
+
'application/xml',
|
65
|
+
'text/html']
|
66
|
+
|
67
|
+
# Pull .well-known/host-meta
|
68
|
+
scheme = 'https'
|
69
|
+
url = "#{scheme}://#{domain}#{port}/.well-known/host-meta"
|
70
|
+
host_meta = Nelumba::Discover.pull_url(url, accept)
|
71
|
+
|
72
|
+
if host_meta.nil?
|
73
|
+
# TODO: Should we do this? probably not. ugh. but we must.
|
74
|
+
scheme = 'http'
|
75
|
+
url = "#{scheme}://#{domain}#{port}/.well-known/host-meta"
|
76
|
+
host_meta = Nelumba::Discover.pull_url(url, accept)
|
77
|
+
end
|
78
|
+
|
79
|
+
return nil if host_meta.nil?
|
80
|
+
|
81
|
+
# Read xrd template location
|
82
|
+
host_meta = host_meta.body
|
83
|
+
host_meta = Nokogiri::XML(host_meta)
|
84
|
+
links = host_meta.xpath("/xmlns:XRD/xmlns:Link")
|
85
|
+
link = links.select{|link| link.attr('rel') == 'lrdd' }.first
|
86
|
+
lrdd_template = link.attr('template') || link.attr('href')
|
87
|
+
|
88
|
+
xrd_url = lrdd_template.gsub(/{uri}/, "acct:#{username}")
|
89
|
+
|
90
|
+
xrd = Nelumba::Discover.pull_url(xrd_url, accept)
|
91
|
+
return nil if xrd.nil?
|
92
|
+
|
93
|
+
xrd = xrd.body
|
94
|
+
xrd = Nokogiri::XML(xrd)
|
95
|
+
|
96
|
+
unless xrd
|
97
|
+
# TODO: Error
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# magic-envelope public key
|
102
|
+
public_key = find_link(xrd, 'magic-public-key') || ""
|
103
|
+
public_key = public_key.split(",")[1] || ""
|
104
|
+
|
105
|
+
# ostatus notification endpoint
|
106
|
+
salmon_url = find_link(xrd, 'salmon')
|
107
|
+
|
108
|
+
# pump.io authentication endpoint
|
109
|
+
dialback_url = find_link(xrd, 'dialback')
|
110
|
+
|
111
|
+
# pump.io activity endpoints
|
112
|
+
activity_inbox_endpoint = find_link(xrd, 'activity-inbox')
|
113
|
+
activity_outbox_endpoint = find_link(xrd, 'activity-outbox')
|
114
|
+
|
115
|
+
# profile page
|
116
|
+
profile_page = find_link(xrd, 'http://webfinger.net/rel/profile-page')
|
117
|
+
|
118
|
+
Identity.new(:public_key => public_key,
|
119
|
+
:profile_page => profile_page,
|
120
|
+
:salmon_endpoint => salmon_url,
|
121
|
+
:dialback_endpoint => dialback_url,
|
122
|
+
:activity_inbox_endpoint => activity_inbox_endpoint,
|
123
|
+
:activity_outbox_endpoint => activity_outbox_endpoint)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Retrieves a Nelumba::Identity from the given xml. You specify the xml
|
127
|
+
# as a url, which will be retrieved and then parsed.
|
128
|
+
def self.identity_from_xml(url, content_type = nil)
|
129
|
+
content_type ||= ['application/xml+xrd',
|
130
|
+
'application/xml']
|
131
|
+
|
132
|
+
xml = Nelumba::Discover.pull_url(url, content_type)
|
133
|
+
return nil if xml.nil?
|
134
|
+
|
135
|
+
Nelumba::Discover.identity_from_xml_string(xml)
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.identity_from_xml_string(xml)
|
139
|
+
xrd = Nokogiri::XML(xml)
|
140
|
+
unless xrd
|
141
|
+
# TODO: Error
|
142
|
+
return nil
|
143
|
+
end
|
144
|
+
|
145
|
+
# magic-envelope public key
|
146
|
+
public_key = find_link(xrd, 'magic-public-key')
|
147
|
+
public_key = public_key.split(",")[1] || ""
|
148
|
+
|
149
|
+
# ostatus notification endpoint
|
150
|
+
salmon_url = find_link(xrd, 'salmon')
|
151
|
+
|
152
|
+
# pump.io authentication endpoint
|
153
|
+
dialback_url = find_link(xrd, 'dialback')
|
154
|
+
|
155
|
+
# pump.io activity endpoints
|
156
|
+
activity_inbox_endpoint = find_link(xrd, 'activity-inbox')
|
157
|
+
activity_outbox_endpoint = find_link(xrd, 'activity-outbox')
|
158
|
+
|
159
|
+
# profile page
|
160
|
+
profile_page = find_link(xrd, 'http://webfinger.net/rel/profile-page')
|
161
|
+
|
162
|
+
Identity.new(:public_key => public_key,
|
163
|
+
:profile_page => profile_page,
|
164
|
+
:salmon_endpoint => salmon_url,
|
165
|
+
:dialback_endpoint => dialback_url,
|
166
|
+
:activity_inbox_endpoint => activity_inbox_endpoint,
|
167
|
+
:activity_outbox_endpoint => activity_outbox_endpoint)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Will yield an Nelumba::Person for the given person.
|
171
|
+
#
|
172
|
+
# identity: Can be a String containing a fully qualified name (i.e.
|
173
|
+
# "user@domain.tld") or a previously resolved Nelumba::Identity.
|
174
|
+
def self.person(identity)
|
175
|
+
if identity.is_a? String
|
176
|
+
identity = Nelumba::Discover.identity(identity)
|
177
|
+
end
|
178
|
+
|
179
|
+
return nil if identity.nil? || identity.profile_page.nil?
|
180
|
+
|
181
|
+
# Discover Person information
|
182
|
+
|
183
|
+
# Pull profile page
|
184
|
+
# Look for a feed to pull
|
185
|
+
feed = Nelumba::Discover.feed(identity.profile_page)
|
186
|
+
feed.authors.first
|
187
|
+
end
|
188
|
+
|
189
|
+
# Will yield a Nelumba::Feed object representing the feed at the given url
|
190
|
+
# or identity.
|
191
|
+
#
|
192
|
+
# Usage:
|
193
|
+
# feed = Nelumba::Discover.feed("https://rstat.us/users/wilkieii/feed")
|
194
|
+
#
|
195
|
+
# i = Nelumba::Discover.identity("wilkieii@rstat.us")
|
196
|
+
# feed = Nelumba::Discover.feed(i)
|
197
|
+
def self.feed(url_or_identity, content_type = nil)
|
198
|
+
if url_or_identity =~ /^(?:acct:)?[^@]+@[^@]+\.[^@]+$/
|
199
|
+
url_or_identity = Nelumba::Discover.identity(url_or_identity)
|
200
|
+
end
|
201
|
+
|
202
|
+
if url_or_identity.is_a? Nelumba::Identity
|
203
|
+
return Nelumba::Discover.feed(url_or_identity.profile_page)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Atom is default type to attempt to retrieve
|
207
|
+
content_type ||= ["application/atom+xml", "text/html"]
|
208
|
+
accept = content_type
|
209
|
+
|
210
|
+
url = url_or_identity
|
211
|
+
|
212
|
+
if url =~ /^http[s]?:\/\//
|
213
|
+
# Url is an internet resource
|
214
|
+
response = Nelumba::Discover.pull_url(url, accept)
|
215
|
+
|
216
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
217
|
+
|
218
|
+
content_type = response.content_type
|
219
|
+
str = response.body
|
220
|
+
else
|
221
|
+
str = open(url).read
|
222
|
+
end
|
223
|
+
|
224
|
+
case content_type
|
225
|
+
when 'application/atom+xml', 'application/rss+xml', 'application/xml',
|
226
|
+
'xml', 'atom', 'rss', 'atom+xml', 'rss+xml'
|
227
|
+
xml_str = str
|
228
|
+
|
229
|
+
self.feed_from_string(xml_str, content_type)
|
230
|
+
when 'text/html'
|
231
|
+
html_str = str
|
232
|
+
|
233
|
+
# Discover the feed
|
234
|
+
doc = Nokogiri::HTML::Document.parse(html_str)
|
235
|
+
links = doc.xpath("//link[@rel='alternate']").map {|el|
|
236
|
+
{:type => el.attributes['type'].to_s,
|
237
|
+
:href => el.attributes['href'].to_s}
|
238
|
+
}.select{|e|
|
239
|
+
MIME_ORDER.include? e[:type]
|
240
|
+
}.sort {|a, b|
|
241
|
+
MIME_ORDER.index(a[:type]) <=>
|
242
|
+
MIME_ORDER.index(b[:type])
|
243
|
+
}
|
244
|
+
|
245
|
+
return nil if links.empty?
|
246
|
+
|
247
|
+
href = links.first[:href]
|
248
|
+
if href.start_with? "/"
|
249
|
+
# Append domain from what we know
|
250
|
+
uri = URI::parse(url) rescue URI.new
|
251
|
+
href = "#{uri.scheme}://#{uri.host}#{uri.port == 80 ? "" : ":#{uri.port}"}#{href}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# Resolve relative links
|
255
|
+
link = URI::parse(href) rescue URI.new
|
256
|
+
|
257
|
+
unless link.scheme
|
258
|
+
link.scheme = URI::parse(url).scheme
|
259
|
+
end
|
260
|
+
|
261
|
+
unless link.host
|
262
|
+
link.host = URI::parse(url).host rescue nil
|
263
|
+
end
|
264
|
+
|
265
|
+
unless link.absolute?
|
266
|
+
link.path = File::dirname(URI::parse(url).path) \
|
267
|
+
+ '/' + link.path rescue nil
|
268
|
+
end
|
269
|
+
|
270
|
+
url = link.to_s
|
271
|
+
Nelumba::Discover.feed(url, links.first[:type])
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Yield a Nelumba::Feed from the given string content.
|
276
|
+
def self.feed_from_string(string, content_type = nil)
|
277
|
+
# Atom is default type to attempt to retrieve
|
278
|
+
content_type ||= "application/atom+xml"
|
279
|
+
|
280
|
+
case content_type
|
281
|
+
when 'application/atom+xml', 'application/rss+xml', 'application/xml'
|
282
|
+
Nelumba::Atom::Feed.new(XML::Reader.string(string)).to_canonical
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.activity(url)
|
287
|
+
self.activity_from_url(url)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Yield a Nelumba::Activity from the given url.
|
291
|
+
def self.activity_from_url(url, content_type = nil)
|
292
|
+
# Atom is default type to attempt to retrieve
|
293
|
+
content_type ||= "application/atom+xml"
|
294
|
+
|
295
|
+
response = Nelumba::Discover.pull_url(url, content_type)
|
296
|
+
|
297
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
298
|
+
|
299
|
+
content_type = response.content_type
|
300
|
+
|
301
|
+
case content_type
|
302
|
+
when 'application/atom+xml', 'application/rss+xml', 'application/xml'
|
303
|
+
xml_str = response.body
|
304
|
+
self.entry_from_string(xml_str, response.content_type)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Yield a Nelumba::Activity from the given string content.
|
309
|
+
def self.activity_from_string(string, content_type = "application/atom+xml")
|
310
|
+
content_type ||= "application/atom+xml"
|
311
|
+
|
312
|
+
case content_type
|
313
|
+
when 'application/atom+xml', 'application/rss+xml', 'application/xml'
|
314
|
+
Nelumba::Atom::Entry.new(XML::Reader.string(string)).to_canonical
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
private
|
319
|
+
|
320
|
+
# :nodoc:
|
321
|
+
def self.pull_url(url, accept = nil, limit = 10)
|
322
|
+
# Atom is default type to attempt to retrieve
|
323
|
+
accept ||= ["application/atom+xml",
|
324
|
+
"application/xml"]
|
325
|
+
|
326
|
+
if accept.is_a? String
|
327
|
+
accept = [accept]
|
328
|
+
end
|
329
|
+
|
330
|
+
uri = URI(url)
|
331
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
332
|
+
request['Accept'] = accept.join(',')
|
333
|
+
request['User-Agent'] = 'nelumba'
|
334
|
+
|
335
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
336
|
+
if uri.scheme == 'https'
|
337
|
+
http.use_ssl = true
|
338
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
339
|
+
end
|
340
|
+
|
341
|
+
response = http.request(request)
|
342
|
+
|
343
|
+
if response.is_a?(Net::HTTPRedirection) && limit > 0
|
344
|
+
location = response['location']
|
345
|
+
Nelumba::Discover.pull_url(location, accept, limit - 1)
|
346
|
+
else
|
347
|
+
response
|
348
|
+
end
|
349
|
+
|
350
|
+
rescue OpenSSL::SSL::SSLError
|
351
|
+
return nil
|
352
|
+
end
|
353
|
+
|
354
|
+
# :nodoc:
|
355
|
+
def self.find_link(xrd, rel)
|
356
|
+
links = xrd.xpath("/xmlns:XRD/xmlns:Link")
|
357
|
+
link = links.select{|link| link.attr('rel').downcase == rel }.first
|
358
|
+
return nil if link.nil?
|
359
|
+
link.attr('template') || link.attr('href')
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|