lotus 0.0.12

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