nelumba 0.0.13
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/.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
|