ostatus 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.
- 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
|