lotus 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +16 -0
  4. data/README.md +233 -0
  5. data/Rakefile +7 -0
  6. data/lib/lotus.rb +232 -0
  7. data/lib/lotus/activity.rb +134 -0
  8. data/lib/lotus/atom/account.rb +50 -0
  9. data/lib/lotus/atom/address.rb +56 -0
  10. data/lib/lotus/atom/author.rb +167 -0
  11. data/lib/lotus/atom/category.rb +41 -0
  12. data/lib/lotus/atom/entry.rb +159 -0
  13. data/lib/lotus/atom/feed.rb +174 -0
  14. data/lib/lotus/atom/generator.rb +40 -0
  15. data/lib/lotus/atom/link.rb +79 -0
  16. data/lib/lotus/atom/name.rb +57 -0
  17. data/lib/lotus/atom/organization.rb +62 -0
  18. data/lib/lotus/atom/portable_contacts.rb +117 -0
  19. data/lib/lotus/atom/source.rb +168 -0
  20. data/lib/lotus/atom/thread.rb +60 -0
  21. data/lib/lotus/author.rb +177 -0
  22. data/lib/lotus/category.rb +45 -0
  23. data/lib/lotus/crypto.rb +146 -0
  24. data/lib/lotus/feed.rb +190 -0
  25. data/lib/lotus/generator.rb +53 -0
  26. data/lib/lotus/identity.rb +59 -0
  27. data/lib/lotus/link.rb +56 -0
  28. data/lib/lotus/notification.rb +220 -0
  29. data/lib/lotus/publisher.rb +40 -0
  30. data/lib/lotus/subscription.rb +117 -0
  31. data/lib/lotus/version.rb +3 -0
  32. data/lotus.gemspec +27 -0
  33. data/spec/activity_spec.rb +84 -0
  34. data/spec/atom/feed_spec.rb +681 -0
  35. data/spec/author_spec.rb +150 -0
  36. data/spec/crypto_spec.rb +138 -0
  37. data/spec/feed_spec.rb +252 -0
  38. data/spec/helper.rb +8 -0
  39. data/spec/identity_spec.rb +67 -0
  40. data/spec/link_spec.rb +30 -0
  41. data/spec/notification_spec.rb +77 -0
  42. data/test/example_feed.atom +393 -0
  43. data/test/example_feed_empty_author.atom +336 -0
  44. data/test/example_feed_false_connected.atom +359 -0
  45. data/test/example_feed_link_without_href.atom +134 -0
  46. data/test/example_page.html +4 -0
  47. data/test/mime_type_bug_feed.atom +874 -0
  48. metadata +204 -0
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ doc/*
6
+ *.rbc
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ script: bundle exec rake test
3
+ rvm:
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - ruby-head
8
+ - rbx-19mode
9
+ script: "bundle exec rake test"
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ostatus.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem "rake" # rakefile
8
+ gem "minitest" # test framework (specified here for prior rubies)
9
+ gem "ansi" # minitest colors
10
+ gem "turn" # minitest output
11
+ gem "mocha" # stubs
12
+
13
+ gem "awesome_print"
14
+ end
15
+
16
+ gem "redfinger", :git => "git://github.com/hotsh/redfinger.git"
@@ -0,0 +1,233 @@
1
+ # Lotus
2
+
3
+ This gem implements federated protocols which let your website connect and interact with any website implementing a federated space.
4
+ Users of a federated service can communicate, produce and consume with users of all sites within that federation as one large community while also allowing them
5
+ to host such websites on their own servers or choice of hosting provider.
6
+ Specifically, this gem deals with handling the data streams and the technologies that are related to Lotus/pump.io such as ActivityStreams, PortableContacts, Webfinger, PubSubHubbub, and Salmon.
7
+
8
+ One such application of this library (in its older form) is [rstat.us](https://rstat.us), which is a Twitter-like service that you can host yourself.
9
+
10
+ ## Usage
11
+
12
+ Currently, only the immutable interface is available.
13
+
14
+ ```
15
+ require 'ostatus'
16
+
17
+ author = Lotus::Author.new(:uri => "https://rstat.us/users/wilkie",
18
+ :email => "wilkie@xomb.org",
19
+ :name => "wilkie")
20
+
21
+ blog_post = Lotus::Activity.new(:activity => :post,
22
+ :title => "Lotus gem",
23
+ :actor => author,
24
+ :content => "Long blog post",
25
+ :content_type => "text/html",
26
+ :id => "1",
27
+ :uri => "http://blog.davewilkinsonii.com/posts/ostatus_gem",
28
+ :published => Time.now)
29
+
30
+ feed = Lotus::Feed.new(:title => "wilkie writes a thing",
31
+ :url => "http://blog.davewilkinsonii.com",
32
+ :rights => "CC0",
33
+ :hubs => ["http://hub.davewilkinsonii.com"],
34
+ :authors => [author],
35
+ :published => Time.now,
36
+ :entries => [blog_post])
37
+ ```
38
+
39
+ To generate an Atom representation:
40
+
41
+ ```
42
+ feed.to_atom
43
+ ```
44
+
45
+ To generate a collection of Lotus objects from Atom:
46
+
47
+ ```
48
+ Lotus.feed_from_url("https://rstat.us/users/wilkieii/feed")
49
+ ```
50
+
51
+ ## Object Documentation
52
+
53
+ ### Feed
54
+
55
+ The Feed is the aggregate. It holds a collection of entries written by a set of authors or contributors. It's the container for content.
56
+
57
+ #### Usage
58
+
59
+ ```
60
+ author = Lotus::Author.new(:name => "Kelly")
61
+ feed = Lotus::Feed.new(:title => "My Feed",
62
+ :id => "1",
63
+ :url => "http://example.com/feeds/1",
64
+ :authors => [author])
65
+ ```
66
+
67
+ #### Fields
68
+ ```
69
+ id => The unique identifier for this feed.
70
+ url => The url that represents this feed.
71
+ title => The title for this feed. Defaults: "Untitled"
72
+ title_type => The content type for the title.
73
+ subtitle => The subtitle for this feed.
74
+ subtitle_type => The content type for the subtitle.
75
+ authors => The list of Lotus::Author's for this feed.
76
+ Defaults: []
77
+ contributors => The list of Lotus::Author's that contributed to this
78
+ feed. Defaults: []
79
+ entries => The list of Lotus::Activity's for this feed.
80
+ Defaults: []
81
+ icon => The url of the icon that represents this feed. It
82
+ should have an aspect ratio of 1 horizontal to 1
83
+ vertical and optimized for presentation at a
84
+ small size.
85
+ logo => The url of the logo that represents this feed. It
86
+ should have an aspect ratio of 2 horizontal to 1
87
+ vertical.
88
+ categories => An array of Lotus::Category's that describe how to
89
+ categorize and describe the content of the feed.
90
+ Defaults: []
91
+ rights => A String depicting the rights of entries without
92
+ explicit rights of their own. SHOULD NOT be machine
93
+ interpreted.
94
+ updated => The DateTime representing when this feed was last
95
+ modified.
96
+ published => The DateTime representing when this feed was originally
97
+ published.
98
+ salmon_url => The url of the salmon endpoint, if one exists, for this
99
+ feed.
100
+ links => An array of Lotus::Link that adds relations to other
101
+ resources.
102
+ generator => An Lotus::Generator representing the agent
103
+ responsible for generating this feed.
104
+ ```
105
+
106
+ ### Activity
107
+
108
+ Something that is done by a person. It has a verb, which suggests what was done
109
+ (e.g. :follow, or :unfollow, or :post) and it has an object, which is the
110
+ content. The content type is what the entity represents and governs how to
111
+ interpret the object. It can be a :note or :post or :image, etc.
112
+
113
+ #### Usage
114
+ ```
115
+ entry = Lotus::Activity.new(:type => :note,
116
+ :title => "wilkie's Daily Update",
117
+ :content => "My day is going really well!",
118
+ :id => "123",
119
+ :url => "http://example.com/entries/123")
120
+ ```
121
+
122
+ #### Fields
123
+ ```
124
+ :title => The title of the entry. Defaults: "Untitled"
125
+ :actor => An Lotus::Author responsible for generating this entry.
126
+ :content => The content of the entry. Defaults: ""
127
+ :content_type => The MIME type of the content.
128
+ :published => The DateTime depicting when the entry was originally
129
+ published.
130
+ :updated => The DateTime depicting when the entry was modified.
131
+ :url => The canonical url of the entry.
132
+ :id => The unique id that identifies this entry.
133
+ :activity => The activity this entry represents. Either a single string
134
+ denoting what type of object this entry represents, or an
135
+ entire Lotus::Activity when a more detailed description is
136
+ appropriate.
137
+ :in_reply_to => An Lotus::Activity for which this entry is a response.
138
+ Or an array of Lotus::Activity's that this entry is a
139
+ response to. Use this when this Activity is a reply
140
+ to an existing Activity.
141
+ ```
142
+
143
+ ### Author
144
+
145
+ This represents a person that creates or contributes content in the feed.
146
+ Feed and Activity can both have one or more Authors or Contributors. One can
147
+ represent a great deal of information about a person.
148
+
149
+ #### Usage
150
+
151
+ ```
152
+ author = Lotus::Author.new(:name => "wilkie",
153
+ :uri => "https://rstat.us/users/wilkie",
154
+ :email => "wilkie@xomb.org",
155
+ :preferredUsername => "wilkie",
156
+ :organization => {:name => "Hackers of the Severed Hand",
157
+ :department => "Software",
158
+ :title => "Founder"},
159
+ :gender => "androgynous",
160
+ :display_name => "Dave Wilkinson",
161
+ :birthday => Time.new(1987, 2, 9))
162
+ ```
163
+
164
+ #### Fields
165
+
166
+ ```
167
+ :name => The name of the author. Defaults: "anonymous"
168
+ :id => The identifier that uniquely identifies the
169
+ contact.
170
+ :nickname => The nickname of the contact.
171
+ :gender => The gender of the contact.
172
+ :note => A note for this contact.
173
+ :display_name => The display name for this contact.
174
+ :preferred_username => The preferred username for this contact.
175
+ :updated => A DateTime representing when this contact was
176
+ last updated.
177
+ :published => A DateTime representing when this contact was
178
+ originally created.
179
+ :birthday => A DateTime representing a birthday for this
180
+ contact.
181
+ :anniversary => A DateTime representing an anniversary for this
182
+ contact.
183
+ :extended_name => A Hash representing the name of the contact.
184
+ :formatted => The full name of the contact
185
+ :family_name => The family name. "Last name" in Western contexts.
186
+ :given_name => The given name. "First name" in Western contexts.
187
+ :middle_name => The middle name.
188
+ :honorific_prefix => "Title" in Western contexts. (e.g. "Mr." "Mrs.")
189
+ :honorific_suffix => "Suffix" in Western contexts. (e.g. "Esq.")
190
+ :organization => A Hash representing the organization of which the
191
+ contact belongs.
192
+ :name => The name of the organization (e.g. company, school,
193
+ etc) This field is required. Will be used for sorting.
194
+ :department => The department within the organization.
195
+ :title => The title or role within the organization.
196
+ :type => The type of organization. Canonical values include
197
+ "job" or "school"
198
+ :start_date => A DateTime representing when the contact joined
199
+ the organization.
200
+ :end_date => A DateTime representing when the contact left the
201
+ organization.
202
+ :location => The physical location of this organization.
203
+ :description => A free-text description of the role this contact
204
+ played in this organization.
205
+ :account => A Hash describing the authorative account for the
206
+ author.
207
+ :domain => The top-most authoriative domain for this account. (e.g.
208
+ "twitter.com") This is the primary field. Is required.
209
+ Used for sorting.
210
+ :username => An alphanumeric username, typically chosen by the user.
211
+ :userid => A user id, typically assigned, that uniquely refers to
212
+ the user.
213
+ :address => A Hash describing the address of the contact.
214
+ :formatted => A formatted representating of the address. May
215
+ contain newlines.
216
+ :street_address => The full street address. May contain newlines.
217
+ :locality => The city or locality component.
218
+ :region => The state or region component.
219
+ :postal_code => The zipcode or postal code component.
220
+ :country => The country name component.
221
+ :uri => The uri that uniquely identifies this author.
222
+ :email => The email of the author.
223
+ ```
224
+
225
+ ## TODO
226
+
227
+ * General cleanup and abstraction of elements of Atom et al that aren't very consistent or useful.
228
+ * Add a persistence layer and allow this to be mixed with Rails and Sinatra style models.
229
+ * Add rails/sinatra aides to allow rapid development of Lotus powered websites.
230
+ * Add already written osub/opub functionality to allow this gem to subscribe directly to other Lotus powered websites.
231
+ * Add Webfinger user identity and verification written in the Salmon library and pull the remaining logic out of rstat.us.
232
+ * Add JSON backend.
233
+ * Write a PuSH hub to aid in small-scale deployment. (Possibly as a detached project. Lotus talks to the hub via a socket.)
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "spec/**/*_spec.rb"
7
+ end
@@ -0,0 +1,232 @@
1
+ require 'lotus/feed'
2
+ require 'lotus/author'
3
+ require 'lotus/activity'
4
+ require 'lotus/identity'
5
+ require 'lotus/notification'
6
+ require 'lotus/link'
7
+
8
+ require 'lotus/crypto'
9
+
10
+ require 'lotus/subscription'
11
+ require 'lotus/publisher'
12
+
13
+ # This module contains elements that allow federated interaction. It also
14
+ # contains methods to construct these objects from external sources.
15
+ module Lotus
16
+ require 'libxml'
17
+
18
+ # This module isolates Atom generation.
19
+ module Atom; end
20
+
21
+ require 'lotus/atom/feed'
22
+ require 'net/http'
23
+ require 'redfinger'
24
+
25
+ # The order to respect atom links
26
+ MIME_ORDER = ['application/atom+xml',
27
+ 'application/rss+xml',
28
+ 'application/xml']
29
+
30
+ # Will yield an OStatus::Identity for the given fully qualified name
31
+ # (i.e. "user@domain.tld")
32
+ def self.discover_identity(name)
33
+ xrd = Redfinger.finger(name)
34
+
35
+ # magic-envelope public key
36
+ public_key = find_link(xrd, 'magic-public-key')
37
+ public_key = public_key.split(",")[1] || ""
38
+
39
+ # ostatus notification endpoint
40
+ salmon_url = find_link(xrd, 'salmon')
41
+
42
+ # pump.io authentication endpoint
43
+ dialback_url = find_link(xrd, 'dialback')
44
+
45
+ # pump.io activity endpoints
46
+ activity_inbox_endpoint = find_link(xrd, 'activity-inbox')
47
+ activity_outbox_endpoint = find_link(xrd, 'activity-outbox')
48
+
49
+ # profile page
50
+ profile_page = find_link(xrd, 'http://webfinger.net/rel/profile-page')
51
+
52
+ Identity.new(:public_key => public_key,
53
+ :profile_page => profile_page,
54
+ :salmon_endpoint => salmon_url,
55
+ :dialback_endpoint => dialback_url,
56
+ :activity_inbox_endpoint => activity_inbox_endpoint,
57
+ :activity_outbox_endpoint => activity_outbox_endpoint)
58
+ end
59
+
60
+ # Will yield an Lotus::Author for the given person.
61
+ #
62
+ # identity: Can be a String containing a fully qualified name (i.e.
63
+ # "user@domain.tld") or a previously resolved Lotus::Identity.
64
+ def self.discover_author(identity)
65
+ if identity.is_a? String
66
+ identity = self.discover_identity(identity)
67
+ end
68
+
69
+ return nil if identity.nil? || identity.profile_page.nil?
70
+
71
+ # Discover Author information
72
+
73
+ # Pull profile page
74
+ # Look for a feed to pull
75
+ feed = self.discover_feed(identity.profile_page)
76
+ feed.authors.first
77
+ end
78
+
79
+ # Will yield a Lotus::Feed object representing the feed at the given url
80
+ # or identity.
81
+ #
82
+ # Usage:
83
+ # feed = Lotus.discover_feed("https://rstat.us/users/wilkieii/feed")
84
+ #
85
+ # i = Lotus.discover_identity("wilkieii@rstat.us")
86
+ # feed = Lotus.discover_feed(i)
87
+ def self.discover_feed(url_or_identity, content_type = "application/atom+xml")
88
+ if url_or_identity =~ /^(?:acct:)?[^@]+@[^@]+\.[^@]+$/
89
+ url_or_identity = Lotus::discover_identity(url_or_identity)
90
+ end
91
+
92
+ if url_or_identity.is_a? Lotus::Identity
93
+ return self.discover_feed(url_or_identity.profile_page)
94
+ end
95
+
96
+ # Atom is default type to attempt to retrieve
97
+ content_type ||= "application/atom+xml"
98
+
99
+ url = url_or_identity
100
+
101
+ if url =~ /^http[s]?:\/\//
102
+ # Url is an internet resource
103
+ response = Lotus::pull_url(url, content_type)
104
+
105
+ return nil unless response.is_a?(Net::HTTPSuccess)
106
+
107
+ content_type = response.content_type
108
+ str = response.body
109
+ else
110
+ str = open(url).read
111
+ end
112
+
113
+ case content_type
114
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml',
115
+ 'xml', 'atom', 'rss', 'atom+xml', 'rss+xml'
116
+ xml_str = str
117
+
118
+ self.feed_from_string(xml_str, content_type)
119
+ when 'text/html'
120
+ html_str = str
121
+
122
+ # Discover the feed
123
+ doc = Nokogiri::HTML::Document.parse(html_str)
124
+ links = doc.xpath("//link[@rel='alternate']").map {|el|
125
+ {:type => el.attributes['type'].to_s,
126
+ :href => el.attributes['href'].to_s}
127
+ }.select{|e|
128
+ MIME_ORDER.include? e[:type]
129
+ }.sort {|a, b|
130
+ MIME_ORDER.index(a[:type]) <=>
131
+ MIME_ORDER.index(b[:type])
132
+ }
133
+
134
+ return nil if links.empty?
135
+
136
+ # Resolve relative links
137
+ link = URI::parse(links.first[:href]) rescue URI.new
138
+
139
+ unless link.scheme
140
+ link.scheme = URI::parse(url).scheme
141
+ end
142
+
143
+ unless link.host
144
+ link.host = URI::parse(url).host rescue nil
145
+ end
146
+
147
+ unless link.absolute?
148
+ link.path = File::dirname(URI::parse(url).path) \
149
+ + '/' + link.path rescue nil
150
+ end
151
+
152
+ url = link.to_s
153
+ self.discover_feed(url, links.first[:type])
154
+ end
155
+ end
156
+
157
+ # Yield a Lotus::Feed from the given string content.
158
+ def self.feed_from_string(string, content_type = nil)
159
+ # Atom is default type to attempt to retrieve
160
+ content_type ||= "application/atom+xml"
161
+
162
+ case content_type
163
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml'
164
+ Lotus::Atom::Feed.new(XML::Reader.string(string)).to_canonical
165
+ end
166
+ end
167
+
168
+ def self.discover_activity(url)
169
+ self.activity_from_url(url)
170
+ end
171
+
172
+ # Yield a Lotus::Activity from the given url.
173
+ def self.activity_from_url(url, content_type = nil)
174
+ # Atom is default type to attempt to retrieve
175
+ content_type ||= "application/atom+xml"
176
+
177
+ response = Lotus.pull_url(url, content_type)
178
+
179
+ return nil unless response.is_a?(Net::HTTPSuccess)
180
+
181
+ content_type = response.content_type
182
+
183
+ case content_type
184
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml'
185
+ xml_str = response.body
186
+ self.entry_from_string(xml_str, response.content_type)
187
+ end
188
+ end
189
+
190
+ # Yield a Lotus::Activity from the given string content.
191
+ def self.activity_from_string(string, content_type = "application/atom+xml")
192
+ content_type ||= "application/atom+xml"
193
+
194
+ case content_type
195
+ when 'application/atom+xml', 'application/rss+xml', 'application/xml'
196
+ Lotus::Atom::Entry.new(XML::Reader.string(string)).to_canonical
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ # :nodoc:
203
+ def self.pull_url(url, content_type = nil, limit = 10)
204
+ # Atom is default type to attempt to retrieve
205
+ content_type ||= "application/atom+xml"
206
+
207
+ uri = URI(url)
208
+ request = Net::HTTP::Get.new(uri.request_uri)
209
+ request.content_type = content_type
210
+
211
+ http = Net::HTTP.new(uri.hostname, uri.port)
212
+ if uri.scheme == 'https'
213
+ http.use_ssl = true
214
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
215
+ end
216
+
217
+ response = http.request(request)
218
+
219
+ if response.is_a?(Net::HTTPRedirection) && limit > 0
220
+ location = response['location']
221
+ Lotus.pull_url(location, content_type, limit - 1)
222
+ else
223
+ response
224
+ end
225
+ end
226
+
227
+ # :nodoc:
228
+ def self.find_link(xrd, rel)
229
+ link = xrd.links.find {|l| l['rel'].downcase == rel} || {}
230
+ link.fetch("href") { nil }
231
+ end
232
+ end