ostatus 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,7 +35,13 @@ module OStatus
35
35
  @entry[:object]
36
36
  else
37
37
  obj = @entry.activity_verb
38
- obj[SCHEMA_ROOT.size..-1].intern unless obj.nil?
38
+ if obj.nil?
39
+ nil
40
+ elsif obj.start_with?(SCHEMA_ROOT)
41
+ obj[SCHEMA_ROOT.size..-1].intern unless obj.nil?
42
+ else
43
+ obj
44
+ end
39
45
  end
40
46
  end
41
47
 
@@ -48,7 +54,13 @@ module OStatus
48
54
  @entry[:object_type]
49
55
  else
50
56
  obj = @entry.activity_object_type
51
- obj[SCHEMA_ROOT.size..-1].intern unless obj.nil?
57
+ if obj.nil?
58
+ nil
59
+ elsif obj.start_with?(SCHEMA_ROOT)
60
+ obj[SCHEMA_ROOT.size..-1].intern unless obj.nil?
61
+ else
62
+ obj
63
+ end
52
64
  end
53
65
  end
54
66
 
@@ -14,6 +14,8 @@ module OStatus
14
14
  element :email
15
15
  element :uri
16
16
 
17
+ elements :links, :class => Atom::Link
18
+
17
19
  add_extension_namespace :poco, POCO_NS
18
20
  element 'poco:id'
19
21
  element 'poco:displayName'
data/lib/ostatus/entry.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require_relative 'activity'
2
2
  require_relative 'author'
3
+ require_relative 'thread'
3
4
 
4
5
  module OStatus
6
+ THREAD_NS = 'http://purl.org/syndication/thread/1.0'
5
7
 
6
8
  # Holds information about an individual entry in the Feed.
7
9
  class Entry < Atom::Entry
@@ -9,10 +11,18 @@ module OStatus
9
11
 
10
12
  add_extension_namespace :activity, ACTIVITY_NS
11
13
  element 'activity:object-type'
12
- element 'activity:object'
14
+ element 'activity:object', :class => OStatus::Author
13
15
  element 'activity:verb'
14
16
  element 'activity:target'
15
17
 
18
+ add_extension_namespace :thr, THREAD_NS
19
+ element 'thr:in-reply-to', :class => OStatus::Thread
20
+
21
+ # This is for backwards compatibility with some implementations of Activity
22
+ # Streams. It should not be used, and in fact is obscured as it is not a
23
+ # method in OStatus::Activity.
24
+ element 'activity:actor', :class => OStatus::Author
25
+
16
26
  namespace Atom::NAMESPACE
17
27
  element :title, :id, :summary
18
28
  element :updated, :published, :class => DateTime, :content_only => true
@@ -35,7 +45,6 @@ module OStatus
35
45
  self.activity_verb = OStatus::Activity::SCHEMA_ROOT + value.activity_verb.to_s
36
46
  end
37
47
  self.activity_target = value.activity_target if value.target
38
- activity_object_type = "HEY"
39
48
  end
40
49
 
41
50
  def url
data/lib/ostatus/feed.rb CHANGED
@@ -25,6 +25,11 @@ module OStatus
25
25
 
26
26
  attr_reader :url
27
27
 
28
+ # Store in reverse order so that the -1 from .index "not found"
29
+ # will sort properly
30
+ MIME_ORDER = ['application/atom+xml', 'application/rss+xml',
31
+ 'application/xml'].reverse
32
+
28
33
  def initialize(str, url, access_token, options)
29
34
  @str = str
30
35
  @url = url
@@ -32,6 +37,34 @@ module OStatus
32
37
  @options = options
33
38
 
34
39
  if str
40
+
41
+ if str =~ /<html/
42
+ doc = LibXML::XML::HTMLParser.string(str).parse
43
+ links = doc.find(
44
+ "//*[contains(concat(' ',normalize-space(@rel),' '), 'alternate')]"
45
+ ).map {|el|
46
+ {:type => el.attributes['type'].to_s,
47
+ :href => el.attributes['href'].to_s}
48
+ }.sort {|a, b|
49
+ MIME_ORDER.index(b[:type]) <=> MIME_ORDER.index(a[:type])
50
+ }
51
+
52
+ # Resolve relative links
53
+ link = URI::parse(links.first[:href]) rescue URI.new
54
+
55
+ unless link.host
56
+ link.host = URI::parse(@url).host rescue nil
57
+ end
58
+
59
+ unless link.absolute?
60
+ link.path = File::dirname(URI::parse(@url).path) \
61
+ + '/' + link.path rescue nil
62
+ end
63
+
64
+ @url = link.to_s
65
+ @str = str = open(@url).read
66
+ end
67
+
35
68
  super(XML::Reader.string(str))
36
69
  else
37
70
  super(options)
@@ -0,0 +1,227 @@
1
+ require 'xml'
2
+ require 'atom'
3
+ require 'digest/sha2'
4
+ require 'rsa'
5
+
6
+ module OStatus
7
+ class Salmon
8
+ attr_accessor :entry
9
+
10
+ # Create a Salmon instance for a particular OStatus::Entry
11
+ def initialize entry, signature = nil, plaintext = nil
12
+ @entry = entry
13
+ @signature = signature
14
+ @plaintext = plaintext
15
+ end
16
+
17
+ # Creates an entry for following a particular Author.
18
+ def Salmon.from_follow(user_author, followed_author)
19
+ entry = OStatus::Entry.new(
20
+ :author => user_author,
21
+ :title => "Now following #{followed_author.name}",
22
+ :content => Atom::Content::Html.new("Now following #{followed_author.name}")
23
+ )
24
+
25
+ entry.activity_verb = :follow
26
+ entry.activity_object = followed_author
27
+
28
+ OStatus::Salmon.new(entry)
29
+ end
30
+
31
+ # Creates an entry for unfollowing a particular Author.
32
+ def Salmon.from_unfollow(user_author, followed_author)
33
+ entry = OStatus::Entry.new(
34
+ :author => user_author,
35
+ :title => "Stopped following #{followed_author.name}",
36
+ :content => Atom::Content::Html.new("Stopped following #{followed_author.name}")
37
+ )
38
+
39
+ entry.activity_verb = "http://ostatus.org/schema/1.0/unfollow"
40
+ entry.activity_object = followed_author
41
+
42
+ OStatus::Salmon.new(entry)
43
+ end
44
+
45
+ # Will pull a OStatus::Entry from a magic envelope described by the xml.
46
+ def Salmon.from_xml source
47
+ if source.is_a?(String)
48
+ source = XML::Document.string(source,
49
+ :options => XML::Parser::Options::NOENT)
50
+ end
51
+
52
+ # Retrieve the envelope
53
+ envelope = source.find('/me:env',
54
+ 'me:http://salmon-protocol.org/ns/magic-env').first
55
+
56
+ if envelope.nil?
57
+ return nil
58
+ end
59
+
60
+ data = envelope.find('me:data',
61
+ 'me:http://salmon-protocol.org/ns/magic-env').first
62
+ if data.nil?
63
+ return nil
64
+ end
65
+
66
+ data_type = data.attributes["type"]
67
+ if data_type.nil?
68
+ data_type = 'application/atom+xml'
69
+ armored_data_type = ''
70
+ else
71
+ armored_data_type = Base64::urlsafe_encode64(data_type)
72
+ end
73
+
74
+ encoding = envelope.find('me:encoding',
75
+ 'me:http://salmon-protocol.org/ns/magic-env').first
76
+
77
+ algorithm = envelope.find(
78
+ 'me:alg',
79
+ 'me:http://salmon-protocol.org/ns/magic-env').first
80
+
81
+ signature = source.find('me:sig',
82
+ 'me:http://salmon-protocol.org/ns/magic-env').first
83
+
84
+ # Parse fields
85
+
86
+ if signature.nil?
87
+ # Well, if we cannot verify, we don't accept
88
+ return nil
89
+ else
90
+ # XXX: Handle key_id attribute
91
+ signature = signature.content
92
+ signature = Base64::urlsafe_decode64(signature)
93
+ end
94
+
95
+ if encoding.nil?
96
+ # When the encoding is omitted, use base64url
97
+ # Cite: Magic Envelope Draft Spec Section 3.3
98
+ armored_encoding = ''
99
+ encoding = 'base64url'
100
+ else
101
+ armored_encoding = Base64::urlsafe_encode64(encoding.content)
102
+ encoding = encoding.content.downcase
103
+ end
104
+
105
+ if algorithm.nil?
106
+ # When algorithm is omitted, use 'RSA-SHA256'
107
+ # Cite: Magic Envelope Draft Spec Section 3.3
108
+ armored_algorithm = ''
109
+ algorithm = 'rsa-sha256'
110
+ else
111
+ armored_algorithm = Base64::urlsafe_encode64(algorithm.content)
112
+ algorithm = algorithm.content.downcase
113
+ end
114
+
115
+ # Retrieve and decode data payload
116
+
117
+ data = data.content
118
+ armored_data = data
119
+
120
+ case encoding
121
+ when 'base64url'
122
+ data = Base64::urlsafe_decode64(data)
123
+ else
124
+ # Unsupported data encoding
125
+ return nil
126
+ end
127
+
128
+ # Signature plaintext
129
+ plaintext = "#{armored_data}.#{armored_data_type}.#{armored_encoding}.#{armored_algorithm}"
130
+
131
+ # Interpret data payload
132
+ payload = XML::Reader.string(data)
133
+ Salmon.new OStatus::Entry.new(payload), signature, plaintext
134
+ end
135
+
136
+ # Generate the xml for this Salmon notice and sign with the given private
137
+ # key.
138
+ def to_xml key
139
+ # Generate magic envelope
140
+ magic_envelope = XML::Document.new
141
+
142
+ magic_envelope.root = XML::Node.new 'env'
143
+
144
+ me_ns = XML::Namespace.new(magic_envelope.root,
145
+ 'me', 'http://salmon-protocol.org/ns/magic-env')
146
+
147
+ magic_envelope.root.namespaces.namespace = me_ns
148
+
149
+ # Armored Data <me:data>
150
+ data = @entry.to_xml
151
+ @plaintext = data
152
+ data_armored = Base64::urlsafe_encode64(data)
153
+ elem = XML::Node.new 'data', data_armored, me_ns
154
+ elem.attributes['type'] = 'application/atom+xml'
155
+ data_type_armored = 'YXBwbGljYXRpb24vYXRvbSt4bWw='
156
+ magic_envelope.root << elem
157
+
158
+ # Encoding <me:encoding>
159
+ magic_envelope.root << XML::Node.new('encoding', 'base64url', me_ns)
160
+ encoding_armored = 'YmFzZTY0dXJs'
161
+
162
+ # Signing Algorithm <me:alg>
163
+ magic_envelope.root << XML::Node.new('alg', 'RSA-SHA256', me_ns)
164
+ algorithm_armored = 'UlNBLVNIQTI1Ng=='
165
+
166
+ # Signature <me:sig>
167
+ plaintext = "#{data_armored}.#{data_type_armored}.#{encoding_armored}.#{algorithm_armored}"
168
+
169
+ # Assign @signature to the signature generated from the plaintext
170
+ sign(plaintext, key)
171
+
172
+ signature_armored = Base64::urlsafe_encode64(@signature)
173
+ magic_envelope.root << XML::Node.new('sig', signature_armored, me_ns)
174
+
175
+ magic_envelope.to_s :indent => true, :encoding => XML::Encoding::UTF_8
176
+ end
177
+
178
+ # Return the EMSA string for this Salmon instance given the size of the
179
+ # public key modulus.
180
+ def signature modulus_byte_length
181
+ plaintext = Digest::SHA2.new(256).digest(@plaintext)
182
+
183
+ prefix = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"
184
+ padding_count = modulus_byte_length - prefix.bytes.count - plaintext.bytes.count - 3
185
+
186
+ padding = ""
187
+ padding_count.times do
188
+ padding = padding + "\xff"
189
+ end
190
+
191
+ "\x00\x01#{padding}\x00#{prefix}#{plaintext}"
192
+ end
193
+
194
+ def sign message, key
195
+ @plaintext = message
196
+
197
+ modulus_byte_count = key.private_key.modulus.size
198
+
199
+ @signature = signature(modulus_byte_count)
200
+ @signature = key.decrypt(@signature)
201
+ end
202
+
203
+ # Use RSA to verify the signature
204
+ # key - RSA::KeyPair with the public key to use
205
+ def verified? key
206
+ # RSA encryption is needed to compare the signatures
207
+
208
+ # Get signature to check
209
+ emsa = self.signature key.public_key.modulus.size
210
+
211
+ # Get signature in payload
212
+ emsa_signature = key.encrypt(@signature)
213
+
214
+ # RSA gem drops leading 0s since it does math upon an Integer
215
+ # As a workaround, I check for what I expect the second byte to be (\x01)
216
+ # This workaround will also handle seeing a \x00 first if the RSA gem is
217
+ # fixed.
218
+ if emsa_signature.getbyte(0) == 1
219
+ emsa_signature = "\x00#{emsa_signature}"
220
+ end
221
+
222
+ # Does the signature match?
223
+ # Return the result.
224
+ emsa_signature == emsa
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,45 @@
1
+ require 'xml/libxml'
2
+ require 'atom/xml/parser.rb'
3
+
4
+ module OStatus
5
+
6
+ # This will parse the Thread Atom extension
7
+ class Thread
8
+ include Atom::Xml::Parseable
9
+ attribute :ref, :type, :source
10
+ uri_attribute :href
11
+
12
+ def initialize(o)
13
+ case o
14
+ when XML::Reader
15
+ if current_node_is?(o, 'in-reply-to')
16
+ parse(o, :once => true)
17
+ else
18
+ raise ArgumentError, "Thread created with node other than thr:in-reply-to: #{o.name}"
19
+ end
20
+ when Hash
21
+ [:href, :ref, :type, :source].each do |attr|
22
+ self.send("#{attr}=", o[attr])
23
+ end
24
+ else
25
+ raise ArgumentError, "Don't know how to handle #{o}"
26
+ end
27
+ end
28
+
29
+ def length=(v)
30
+ @length = v.to_i
31
+ end
32
+
33
+ def to_s
34
+ self.href
35
+ end
36
+
37
+ def ==(o)
38
+ o.respond_to?(:href) && o.href == self.href
39
+ end
40
+
41
+ def inspect
42
+ "<OStatus::Thread href:'#{href}' type:'#{type}'>"
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module OStatus
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
data/lib/ostatus.rb CHANGED
@@ -3,3 +3,5 @@ require_relative 'ostatus/entry'
3
3
  require_relative 'ostatus/author'
4
4
  require_relative 'ostatus/activity'
5
5
  require_relative 'ostatus/portable_contacts'
6
+ require_relative 'ostatus/salmon'
7
+ require_relative 'ostatus/thread'
data/ostatus.gemspec CHANGED
@@ -14,7 +14,6 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "ostatus"
16
16
 
17
- s.add_dependency "oauth"
18
17
  s.add_dependency "ratom"
19
18
  s.add_development_dependency "rspec"
20
19
 
@@ -1,6 +1,7 @@
1
1
  require_relative '../lib/ostatus/feed.rb'
2
2
  require_relative '../lib/ostatus/entry.rb'
3
3
  require_relative '../lib/ostatus/activity.rb'
4
+ require_relative '../lib/ostatus/author.rb'
4
5
 
5
6
  describe OStatus::Activity do
6
7
  before(:each) do
@@ -12,8 +13,8 @@ describe OStatus::Activity do
12
13
  end
13
14
 
14
15
  describe "#object" do
15
- it "should give a String containing the content of the activity:object tag" do
16
- @activity.object.should eql('Foobar')
16
+ it "should give an Author containing the content of the activity:object tag" do
17
+ @activity.object.class.should eql(OStatus::Author)
17
18
  end
18
19
 
19
20
  it "should give nil when no activity:object was given" do
data/spec/feed_spec.rb CHANGED
@@ -6,6 +6,13 @@ describe OStatus::Feed do
6
6
  @feed = OStatus::Feed.from_url('test/example_feed.atom')
7
7
  end
8
8
 
9
+ describe "#initialize" do
10
+ it "should detect a feed URI in an HTML page" do
11
+ @feed = OStatus::Feed.from_url('test/example_page.html')
12
+ @feed.url.should == 'test/example_feed.atom'
13
+ end
14
+ end
15
+
9
16
  describe "#atom" do
10
17
  it "should return a String containing the atom information" do
11
18
  @feed.atom.start_with?("<?xml").should eql(true)
@@ -71,7 +71,40 @@ bar</poco:note>
71
71
  <link href="http://identi.ca/api/statuses/user_timeline/141464.atom" rel="self" type="application/atom+xml"/>
72
72
  <entry>
73
73
  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
74
- <activity:object>Foobar</activity:object>
74
+ <activity:object>
75
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
76
+ <uri>http://identi.ca/user/141464</uri>
77
+ <name>greenmanspirit</name>
78
+ <email>foo@example.com</email>
79
+ <link rel="alternate" type="text/html" href="http://identi.ca/greenmanspirit"/>
80
+ <link rel="avatar" type="image/jpeg" media:width="480" media:height="480" href="http://avatar.identi.ca/141464-480-20100607212940.jpeg"/>
81
+ <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="http://avatar.identi.ca/141464-96-20100607212940.jpeg"/>
82
+ <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="http://avatar.identi.ca/141464-48-20100607212940.jpeg"/>
83
+ <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="http://avatar.identi.ca/141464-24-20100607212940.jpeg"/>
84
+ <georss:point>0 0</georss:point>
85
+ <poco:preferredUsername>greenmanspirit</poco:preferredUsername>
86
+ <poco:displayName>Adam Hobaugh</poco:displayName>
87
+ <poco:id>foobar</poco:id>
88
+ <poco:name>barbaz</poco:name>
89
+ <poco:nickname>spaz</poco:nickname>
90
+ <poco:published>2012-02-21T02:15:14+00:00</poco:published>
91
+ <poco:updated>2013-02-21T02:15:14+00:00</poco:updated>
92
+ <poco:birthday>2014-02-21</poco:birthday>
93
+ <poco:anniversary>2015-02-21</poco:anniversary>
94
+ <poco:gender>male</poco:gender>
95
+ <poco:note>foo
96
+ bar</poco:note>
97
+ <poco:utcOffset>-08:00</poco:utcOffset>
98
+ <poco:connected>true</poco:connected>
99
+ <poco:urls>
100
+ <poco:type>homepage</poco:type>
101
+ <poco:value>http://adamhobaugh.com</poco:value>
102
+ <poco:primary>true</poco:primary>
103
+ </poco:urls>
104
+
105
+ <statusnet:profile_info local_id="141464"></statusnet:profile_info>
106
+ </activity:object>
107
+
75
108
  <activity:target>Barbaz</activity:target>
76
109
  <id>http://identi.ca/notice/64991641</id>
77
110
  <title>staples come out of the head tomorrow, oh yeah</title>
@@ -0,0 +1,4 @@
1
+ <html>
2
+ <!-- This is very invalid on purpose -->
3
+ <a rel="alternate" type="application/atom+xml" href="example_feed.atom">
4
+ </html>
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 7
9
- version: 0.0.7
8
+ - 8
9
+ version: 0.0.8
10
10
  platform: ruby
11
11
  authors:
12
12
  - Hackers of the Severed Hand
@@ -14,11 +14,11 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-04-02 00:00:00 -04:00
17
+ date: 2011-05-20 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: oauth
21
+ name: ratom
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  none: false
@@ -30,23 +30,10 @@ dependencies:
30
30
  version: "0"
31
31
  type: :runtime
32
32
  version_requirements: *id001
33
- - !ruby/object:Gem::Dependency
34
- name: ratom
35
- prerelease: false
36
- requirement: &id002 !ruby/object:Gem::Requirement
37
- none: false
38
- requirements:
39
- - - ">="
40
- - !ruby/object:Gem::Version
41
- segments:
42
- - 0
43
- version: "0"
44
- type: :runtime
45
- version_requirements: *id002
46
33
  - !ruby/object:Gem::Dependency
47
34
  name: rspec
48
35
  prerelease: false
49
- requirement: &id003 !ruby/object:Gem::Requirement
36
+ requirement: &id002 !ruby/object:Gem::Requirement
50
37
  none: false
51
38
  requirements:
52
39
  - - ">="
@@ -55,7 +42,7 @@ dependencies:
55
42
  - 0
56
43
  version: "0"
57
44
  type: :development
58
- version_requirements: *id003
45
+ version_requirements: *id002
59
46
  description: This project is to be used to jumpstart OStatus related projects that implement the PubSubHubbub protocols by providing the common fundamentals of Atom parsing and OStatus object creation.
60
47
  email:
61
48
  - hotsh@xomb.org
@@ -76,6 +63,8 @@ files:
76
63
  - lib/ostatus/entry.rb
77
64
  - lib/ostatus/feed.rb
78
65
  - lib/ostatus/portable_contacts.rb
66
+ - lib/ostatus/salmon.rb
67
+ - lib/ostatus/thread.rb
79
68
  - lib/ostatus/version.rb
80
69
  - ostatus.gemspec
81
70
  - spec/activity_spec.rb
@@ -87,6 +76,7 @@ files:
87
76
  - test/example_feed.atom
88
77
  - test/example_feed_empty_author.atom
89
78
  - test/example_feed_false_connected.atom
79
+ - test/example_page.html
90
80
  has_rdoc: true
91
81
  homepage: http://github.com/hotsh/ostatus
92
82
  licenses: []
@@ -129,3 +119,4 @@ test_files:
129
119
  - test/example_feed.atom
130
120
  - test/example_feed_empty_author.atom
131
121
  - test/example_feed_false_connected.atom
122
+ - test/example_page.html