ostatus 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +9 -0
- data/Rakefile +15 -0
- data/lib/ostatus.rb +4 -0
- data/lib/ostatus/activity.rb +63 -0
- data/lib/ostatus/author.rb +62 -0
- data/lib/ostatus/entry.rb +107 -0
- data/lib/ostatus/feed.rb +156 -0
- data/lib/ostatus/portable_contacts.rb +124 -0
- data/lib/ostatus/version.rb +3 -0
- data/ostatus.gemspec +26 -0
- data/spec/activity_spec.rb +53 -0
- data/spec/author_spec.rb +62 -0
- data/spec/entry_spec.rb +97 -0
- data/spec/feed_spec.rb +34 -0
- data/spec/portable_contacts_spec.rb +157 -0
- data/test/example_feed.atom +360 -0
- data/test/example_feed_empty_author.atom +336 -0
- data/test/example_feed_false_connected.atom +359 -0
- metadata +142 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
OStatus
|
2
|
+
=======
|
3
|
+
|
4
|
+
This gem implements the OStatus protocol data streams and the technologies that are related to it such as ActivityStreams, PortableContacts, and Salmon.
|
5
|
+
|
6
|
+
What it does
|
7
|
+
------------
|
8
|
+
|
9
|
+
Right now, it simply parses Atom and gives the ability to parse the XML for each of the objects in the OStatus world.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
# rake spec
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
5
|
+
spec.pattern = 'spec/*_spec.rb'
|
6
|
+
end
|
7
|
+
|
8
|
+
# rake doc
|
9
|
+
RSpec::Core::RakeTask.new(:doc) do |spec|
|
10
|
+
spec.pattern = 'spec/*_spec.rb'
|
11
|
+
spec.rspec_opts = ['--format documentation']
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'bundler'
|
15
|
+
Bundler::GemHelper.install_tasks
|
data/lib/ostatus.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module OStatus
|
2
|
+
|
3
|
+
# This class represents an Activity object for an OStatus entry.
|
4
|
+
class Activity
|
5
|
+
|
6
|
+
# This will create an instance of an Activity class populated
|
7
|
+
# with the given data as a Hash or parsable XML given by a
|
8
|
+
# Nokogiri::XML::Element that serves as the root node of
|
9
|
+
# anything containing the activity tags.
|
10
|
+
def initialize(activity_root)
|
11
|
+
if activity_root.class == Hash
|
12
|
+
@activity_data = activity_root
|
13
|
+
@activity = nil
|
14
|
+
else
|
15
|
+
@activity = activity_root
|
16
|
+
@activity_data = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pick_first_node(a)
|
21
|
+
if a.empty?
|
22
|
+
nil
|
23
|
+
else
|
24
|
+
a[0].content
|
25
|
+
end
|
26
|
+
end
|
27
|
+
private :pick_first_node
|
28
|
+
|
29
|
+
# Returns the object field or nil if it does not exist.
|
30
|
+
def object
|
31
|
+
return @activity_data[:object] unless @activity_data == nil
|
32
|
+
pick_first_node(@activity.xpath('./activity:object'))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the target field or nil if it does not exist.
|
36
|
+
def target
|
37
|
+
return @activity_data[:target] unless @activity_data == nil
|
38
|
+
pick_first_node(@activity.xpath('./activity:target'))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the verb field or nil if it does not exist.
|
42
|
+
def verb
|
43
|
+
return @activity_data[:verb] unless @activity_data == nil
|
44
|
+
pick_first_node(@activity.xpath('./activity:verb'))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the object-type field or nil if it does not exist.
|
48
|
+
def object_type
|
49
|
+
return @activity_data[:object_type] unless @activity_data == nil
|
50
|
+
pick_first_node(@activity.xpath('./activity:object-type'))
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns a hash of all relevant fields.
|
54
|
+
def info
|
55
|
+
{
|
56
|
+
:object => self.object,
|
57
|
+
:target => self.target,
|
58
|
+
:verb => self.verb,
|
59
|
+
:object_type => self.object_type
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative 'activity'
|
2
|
+
require_relative 'portable_contacts'
|
3
|
+
|
4
|
+
module OStatus
|
5
|
+
|
6
|
+
# Holds information about the author of the Feed.
|
7
|
+
class Author
|
8
|
+
|
9
|
+
# Instantiates an Author object either from a given <author></author> root
|
10
|
+
# passed as an instance of a Nokogiri::XML::Element or a Hash containing
|
11
|
+
# the properties.
|
12
|
+
def initialize(author_node)
|
13
|
+
if author_node.class == Hash
|
14
|
+
@author_data = author_node
|
15
|
+
@author = nil
|
16
|
+
else
|
17
|
+
@author = author_node
|
18
|
+
@author_data = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Gives an instance of an OStatus::Activity that parses the fields
|
23
|
+
# having an activity prefix.
|
24
|
+
def activity
|
25
|
+
OStatus::Activity.new(@author)
|
26
|
+
end
|
27
|
+
|
28
|
+
def pick_first_node(a)
|
29
|
+
if a.empty?
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
a[0].content
|
33
|
+
end
|
34
|
+
end
|
35
|
+
private :pick_first_node
|
36
|
+
|
37
|
+
# Returns the name of the author, if it exists.
|
38
|
+
def name
|
39
|
+
return @author_data[:name] unless @author_data == nil
|
40
|
+
pick_first_node(@author.css('name'))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the email of the author, if it exists.
|
44
|
+
def email
|
45
|
+
return @author_data[:email] unless @author_data == nil
|
46
|
+
pick_first_node(@author.css('email'))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the uri of the author, if it exists.
|
50
|
+
def uri
|
51
|
+
return @author_data[:uri] unless @author_data == nil
|
52
|
+
pick_first_node(@author.css('uri'))
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns an instance of a PortableContacts that further describe the
|
56
|
+
# author's contact information, if it exists.
|
57
|
+
def portable_contacts
|
58
|
+
return @author_data[:portable_contacts] unless @author_data == nil
|
59
|
+
PortableContacts.new(@author)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative 'activity'
|
2
|
+
|
3
|
+
module OStatus
|
4
|
+
|
5
|
+
# Holds information about an individual entry in the Feed.
|
6
|
+
class Entry
|
7
|
+
|
8
|
+
# Instantiates an Entry object from either a given <entry></entry> root
|
9
|
+
# passed as an instance of a Nokogiri::XML::Element or a Hash
|
10
|
+
# containing the properties.
|
11
|
+
def initialize(entry_node)
|
12
|
+
if entry_node.class == Hash
|
13
|
+
@entry_data = entry_node
|
14
|
+
@entry = nil
|
15
|
+
else
|
16
|
+
@entry = entry_node
|
17
|
+
@entry_data = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Gives an instance of an OStatus::Activity that parses the fields
|
22
|
+
# having an activity prefix.
|
23
|
+
def activity
|
24
|
+
Activity.new(@entry)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pick_first_node(a)
|
28
|
+
if a.empty?
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
a[0].content
|
32
|
+
end
|
33
|
+
end
|
34
|
+
private :pick_first_node
|
35
|
+
|
36
|
+
# Returns the title of the entry.
|
37
|
+
def title
|
38
|
+
return @entry_data[:title] unless @entry_data == nil
|
39
|
+
pick_first_node(@entry.css('title'))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the content of the entry.
|
43
|
+
def content
|
44
|
+
return @entry_data[:content] unless @entry_data == nil
|
45
|
+
pick_first_node(@entry.css('content'))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the content-type of the entry.
|
49
|
+
def content_type
|
50
|
+
return @entry_data[:content_type] unless @entry_data == nil
|
51
|
+
content = @entry.css('content')
|
52
|
+
content.empty? ? "" : content[0]['type']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the DateTime that this entry was published.
|
56
|
+
def published
|
57
|
+
return @entry_data[:published] unless @entry_data == nil
|
58
|
+
DateTime.parse(pick_first_node(@entry.css('published')))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the DateTime that this entry was updated.
|
62
|
+
def updated
|
63
|
+
return @entry_data[:updated] unless @entry_data == nil
|
64
|
+
DateTime.parse(pick_first_node(@entry.css('updated')))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the id of the entry.
|
68
|
+
def id
|
69
|
+
return @entry_data[:id] unless @entry_data == nil
|
70
|
+
pick_first_node(@entry.css('id'))
|
71
|
+
end
|
72
|
+
|
73
|
+
def link
|
74
|
+
return @entry_data[:link] unless @entry_data == nil
|
75
|
+
|
76
|
+
result = {}
|
77
|
+
|
78
|
+
@entry.css('link').each do |node|
|
79
|
+
if node[:rel] != nil
|
80
|
+
rel = node[:rel].intern
|
81
|
+
if result[rel] == nil
|
82
|
+
result[rel] = []
|
83
|
+
end
|
84
|
+
|
85
|
+
result[rel] << node
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a Hash of all fields.
|
93
|
+
def info
|
94
|
+
return @entry_data unless @entry_data == nil
|
95
|
+
{
|
96
|
+
:activity => self.activity.info,
|
97
|
+
:id => pick_first_node(@entry.css('id')),
|
98
|
+
:title => self.title,
|
99
|
+
:content => self.content,
|
100
|
+
:content_type => self.content_type,
|
101
|
+
:link => self.link,
|
102
|
+
:published => self.published,
|
103
|
+
:updated => self.updated
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/ostatus/feed.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'tinyatom'
|
4
|
+
|
5
|
+
require_relative 'entry'
|
6
|
+
|
7
|
+
module OStatus
|
8
|
+
|
9
|
+
# This class represents an OStatus Feed object.
|
10
|
+
class Feed
|
11
|
+
def initialize(url, access_token, author, entries, id, title, links)
|
12
|
+
@url = url
|
13
|
+
@access_token = access_token
|
14
|
+
@author = author
|
15
|
+
@entries = entries
|
16
|
+
@id = id
|
17
|
+
@title = title
|
18
|
+
@links = links
|
19
|
+
|
20
|
+
if id == nil
|
21
|
+
@xml = Nokogiri::XML::Document.parse(self.atom)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a new Feed instance given by the atom feed located at 'url'
|
26
|
+
# and optionally using the OAuth::AccessToken given.
|
27
|
+
def Feed.from_url(url, access_token = nil)
|
28
|
+
Feed.new(url, access_token, nil, nil, nil, nil, nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates a new Feed instance that contains the information given by
|
32
|
+
# the various instances of author and entries.
|
33
|
+
def Feed.from_data(id, title, url, author, entries, links)
|
34
|
+
Feed.new(url, nil, author, entries, id, title, links)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of Nokogiri::XML::Element instances for all link tags
|
38
|
+
# that have a rel equal to that given by attribute. This can be used
|
39
|
+
# generally as a Hash where the keys are intern strings that give an attribute.
|
40
|
+
#
|
41
|
+
# For example:
|
42
|
+
# link(:hub).first[:href] -- Gets the first link tag with rel="hub" and
|
43
|
+
# returns the contents of the href attribute.
|
44
|
+
#
|
45
|
+
def link(attribute)
|
46
|
+
return @links[attribute] unless @links == nil
|
47
|
+
|
48
|
+
# get all links with rel attribute being equal to attribute
|
49
|
+
@xml.xpath('/xmlns:feed/xmlns:link').select do |link|
|
50
|
+
link[:rel] == attribute.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns an array of URLs for each hub link tag.
|
55
|
+
def hubs
|
56
|
+
link(:hub).map do |link|
|
57
|
+
link[:href]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the salmon URL from the link tag.
|
62
|
+
def salmon
|
63
|
+
link(:salmon).first[:href]
|
64
|
+
end
|
65
|
+
|
66
|
+
# This method will return a String containing the actual content of
|
67
|
+
# the atom feed. It will make a network request (through OAuth if
|
68
|
+
# an access token was given) to retrieve the document if necessary.
|
69
|
+
def atom
|
70
|
+
if @id == nil and @access_token == nil
|
71
|
+
# simply open the url
|
72
|
+
open(@url).read
|
73
|
+
elsif @id == nil and @url != nil
|
74
|
+
# open the url through OAuth
|
75
|
+
@access_token.get(@url).body
|
76
|
+
else
|
77
|
+
# build the atom file from internal information
|
78
|
+
feed = TinyAtom::Feed.new(
|
79
|
+
self.id,
|
80
|
+
self.title,
|
81
|
+
@url,
|
82
|
+
|
83
|
+
:author_name => self.author.name,
|
84
|
+
:author_email => self.author.email,
|
85
|
+
:author_uri => self.author.uri,
|
86
|
+
|
87
|
+
:hubs => self.hubs
|
88
|
+
)
|
89
|
+
|
90
|
+
@entries.each do |entry|
|
91
|
+
feed.add_entry(
|
92
|
+
entry.id,
|
93
|
+
entry.title,
|
94
|
+
entry.updated,
|
95
|
+
|
96
|
+
'',
|
97
|
+
|
98
|
+
:content => entry.content,
|
99
|
+
|
100
|
+
:author_name => self.author.name,
|
101
|
+
:author_email => self.author.email,
|
102
|
+
:author_uri => self.author.uri
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
feed.make(:indent => 2)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def pick_first_node(a)
|
111
|
+
if a.empty?
|
112
|
+
nil
|
113
|
+
else
|
114
|
+
a[0].content
|
115
|
+
end
|
116
|
+
end
|
117
|
+
private :pick_first_node
|
118
|
+
|
119
|
+
def id
|
120
|
+
return @id if @xml == nil
|
121
|
+
|
122
|
+
pick_first_node(@xml.xpath('/xmlns:feed/xmlns:id'))
|
123
|
+
end
|
124
|
+
|
125
|
+
def title
|
126
|
+
return @title if @xml == nil
|
127
|
+
|
128
|
+
pick_first_node(@xml.xpath('/xmlns:feed/xmlns:title'))
|
129
|
+
end
|
130
|
+
|
131
|
+
def url
|
132
|
+
return @url
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns an OStatus::Author that will parse the author information
|
136
|
+
# within the Feed.
|
137
|
+
def author
|
138
|
+
return @author if @xml == nil
|
139
|
+
|
140
|
+
author_xml = @xml.at_css('author')
|
141
|
+
OStatus::Author.new(author_xml)
|
142
|
+
end
|
143
|
+
|
144
|
+
# This method gives you an array of OStatus::Entry instances for
|
145
|
+
# each entry listed in the feed.
|
146
|
+
def entries
|
147
|
+
return @entries if @xml == nil
|
148
|
+
|
149
|
+
entries_xml = @xml.css('entry')
|
150
|
+
|
151
|
+
entries_xml.map do |entry|
|
152
|
+
OStatus::Entry.new(entry)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module OStatus
|
2
|
+
|
3
|
+
# Holds information about the extended contact information
|
4
|
+
# in the Feed given in the Portable Contacts specification.
|
5
|
+
class PortableContacts
|
6
|
+
|
7
|
+
# Instantiates a OStatus::PortableContacts object from either
|
8
|
+
# a given root that contains all <poco:*> tags as a
|
9
|
+
# Nokogiri::XML::Element or a Hash containing the properties.
|
10
|
+
def initialize(author_node)
|
11
|
+
if author_node.class == Hash
|
12
|
+
@poco_data = author_node
|
13
|
+
@poco = nil
|
14
|
+
else
|
15
|
+
@poco = author_node
|
16
|
+
@poco_data = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pick_first_node(a)
|
21
|
+
if a.empty?
|
22
|
+
nil
|
23
|
+
else
|
24
|
+
a[0].content
|
25
|
+
end
|
26
|
+
end
|
27
|
+
private :pick_first_node
|
28
|
+
|
29
|
+
# Returns the id of the contact, if it exists.
|
30
|
+
def id
|
31
|
+
return @poco_data[:id] unless @poco_data == nil
|
32
|
+
pick_first_node(@poco.xpath('./poco:id'))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the display_name of the contact, if it exists.
|
36
|
+
def display_name
|
37
|
+
return @poco_data[:display_name] unless @poco_data == nil
|
38
|
+
pick_first_node(@poco.xpath('./poco:displayName'))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the name of the contact, if it exists.
|
42
|
+
def name
|
43
|
+
return @poco_data[:name] unless @poco_data == nil
|
44
|
+
pick_first_node(@poco.xpath('./poco:name'))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the nickname of the contact, if it exists.
|
48
|
+
def nickname
|
49
|
+
return @poco_data[:nickname] unless @poco_data == nil
|
50
|
+
pick_first_node(@poco.xpath('./poco:nickname'))
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the published of the contact, if it exists.
|
54
|
+
def published
|
55
|
+
return @poco_data[:published] unless @poco_data == nil
|
56
|
+
pub = pick_first_node(@poco.xpath('./poco:published'))
|
57
|
+
if pub != nil
|
58
|
+
DateTime.parse(pub)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the updated of the contact, if it exists.
|
63
|
+
def updated
|
64
|
+
return @poco_data[:updated] unless @poco_data == nil
|
65
|
+
upd = pick_first_node(@poco.xpath('./poco:updated'))
|
66
|
+
if upd != nil
|
67
|
+
DateTime.parse(upd)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the birthday of the contact, if it exists.
|
72
|
+
def birthday
|
73
|
+
return @poco_data[:birthday] unless @poco_data == nil
|
74
|
+
bday = pick_first_node(@poco.xpath('./poco:birthday'))
|
75
|
+
if bday != nil
|
76
|
+
Date.parse(bday)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the anniversary of the contact, if it exists.
|
81
|
+
def anniversary
|
82
|
+
return @poco_data[:anniversary] unless @poco_data == nil
|
83
|
+
anni = pick_first_node(@poco.xpath('./poco:anniversary'))
|
84
|
+
if anni != nil
|
85
|
+
Date.parse(anni)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the gender of the contact, if it exists.
|
90
|
+
def gender
|
91
|
+
return @poco_data[:gender] unless @poco_data == nil
|
92
|
+
pick_first_node(@poco.xpath('./poco:gender'))
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns the note of the contact, if it exists.
|
96
|
+
def note
|
97
|
+
return @poco_data[:note] unless @poco_data == nil
|
98
|
+
pick_first_node(@poco.xpath('./poco:note'))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns the preferred username of the contact, if it exists.
|
102
|
+
def preferred_username
|
103
|
+
return @poco_data[:preferred_username] unless @poco_data == nil
|
104
|
+
pick_first_node(@poco.xpath('./poco:preferredUsername'))
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns a boolean that indicates that a bi-directional connection
|
108
|
+
# has been established between the user and the contact, if it is
|
109
|
+
# able to assert this.
|
110
|
+
def connected
|
111
|
+
return @poco_data[:connected] unless @poco_data == nil
|
112
|
+
str = pick_first_node(@poco.xpath('./poco:connected'))
|
113
|
+
return nil if str == nil
|
114
|
+
|
115
|
+
if str == "true"
|
116
|
+
true
|
117
|
+
elsif str == "false"
|
118
|
+
false
|
119
|
+
else
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|