nelumba 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +20 -0
  4. data/README.md +242 -0
  5. data/Rakefile +7 -0
  6. data/assets/lotus_logo_purple.png +0 -0
  7. data/assets/lotus_logo_purple.svg +262 -0
  8. data/lib/nelumba.rb +47 -0
  9. data/lib/nelumba/activity.rb +250 -0
  10. data/lib/nelumba/application.rb +11 -0
  11. data/lib/nelumba/article.rb +11 -0
  12. data/lib/nelumba/atom/account.rb +50 -0
  13. data/lib/nelumba/atom/address.rb +56 -0
  14. data/lib/nelumba/atom/author.rb +176 -0
  15. data/lib/nelumba/atom/category.rb +41 -0
  16. data/lib/nelumba/atom/comment.rb +96 -0
  17. data/lib/nelumba/atom/entry.rb +216 -0
  18. data/lib/nelumba/atom/feed.rb +198 -0
  19. data/lib/nelumba/atom/generator.rb +40 -0
  20. data/lib/nelumba/atom/link.rb +79 -0
  21. data/lib/nelumba/atom/name.rb +57 -0
  22. data/lib/nelumba/atom/organization.rb +62 -0
  23. data/lib/nelumba/atom/person.rb +179 -0
  24. data/lib/nelumba/atom/portable_contacts.rb +117 -0
  25. data/lib/nelumba/atom/source.rb +179 -0
  26. data/lib/nelumba/atom/thread.rb +60 -0
  27. data/lib/nelumba/audio.rb +39 -0
  28. data/lib/nelumba/badge.rb +11 -0
  29. data/lib/nelumba/binary.rb +52 -0
  30. data/lib/nelumba/bookmark.rb +30 -0
  31. data/lib/nelumba/category.rb +49 -0
  32. data/lib/nelumba/collection.rb +34 -0
  33. data/lib/nelumba/comment.rb +47 -0
  34. data/lib/nelumba/crypto.rb +144 -0
  35. data/lib/nelumba/device.rb +11 -0
  36. data/lib/nelumba/discover.rb +362 -0
  37. data/lib/nelumba/event.rb +57 -0
  38. data/lib/nelumba/feed.rb +173 -0
  39. data/lib/nelumba/file.rb +43 -0
  40. data/lib/nelumba/generator.rb +53 -0
  41. data/lib/nelumba/group.rb +11 -0
  42. data/lib/nelumba/identity.rb +63 -0
  43. data/lib/nelumba/image.rb +30 -0
  44. data/lib/nelumba/link.rb +56 -0
  45. data/lib/nelumba/note.rb +34 -0
  46. data/lib/nelumba/notification.rb +229 -0
  47. data/lib/nelumba/object.rb +251 -0
  48. data/lib/nelumba/person.rb +306 -0
  49. data/lib/nelumba/place.rb +34 -0
  50. data/lib/nelumba/product.rb +30 -0
  51. data/lib/nelumba/publisher.rb +44 -0
  52. data/lib/nelumba/question.rb +30 -0
  53. data/lib/nelumba/review.rb +30 -0
  54. data/lib/nelumba/service.rb +11 -0
  55. data/lib/nelumba/subscription.rb +117 -0
  56. data/lib/nelumba/version.rb +3 -0
  57. data/lib/nelumba/video.rb +43 -0
  58. data/nelumba.gemspec +28 -0
  59. data/spec/activity_spec.rb +116 -0
  60. data/spec/application_spec.rb +136 -0
  61. data/spec/article_spec.rb +136 -0
  62. data/spec/atom/comment_spec.rb +455 -0
  63. data/spec/atom/feed_spec.rb +684 -0
  64. data/spec/audio_spec.rb +164 -0
  65. data/spec/badge_spec.rb +136 -0
  66. data/spec/binary_spec.rb +218 -0
  67. data/spec/bookmark.rb +150 -0
  68. data/spec/collection_spec.rb +152 -0
  69. data/spec/comment_spec.rb +128 -0
  70. data/spec/crypto_spec.rb +126 -0
  71. data/spec/device_spec.rb +136 -0
  72. data/spec/event_spec.rb +239 -0
  73. data/spec/feed_spec.rb +252 -0
  74. data/spec/file_spec.rb +190 -0
  75. data/spec/group_spec.rb +136 -0
  76. data/spec/helper.rb +10 -0
  77. data/spec/identity_spec.rb +67 -0
  78. data/spec/image_spec.rb +150 -0
  79. data/spec/link_spec.rb +30 -0
  80. data/spec/note_spec.rb +163 -0
  81. data/spec/notification_spec.rb +89 -0
  82. data/spec/person_spec.rb +244 -0
  83. data/spec/place_spec.rb +162 -0
  84. data/spec/product_spec.rb +150 -0
  85. data/spec/question_spec.rb +156 -0
  86. data/spec/review_spec.rb +149 -0
  87. data/spec/service_spec.rb +136 -0
  88. data/spec/video_spec.rb +164 -0
  89. data/test/example_feed.atom +393 -0
  90. data/test/example_feed_empty_author.atom +336 -0
  91. data/test/example_feed_false_connected.atom +359 -0
  92. data/test/example_feed_link_without_href.atom +134 -0
  93. data/test/example_page.html +4 -0
  94. data/test/mime_type_bug_feed.atom +874 -0
  95. metadata +288 -0
@@ -0,0 +1,34 @@
1
+ module Nelumba
2
+ class Note
3
+ include Nelumba::Object
4
+
5
+ # Create a new note.
6
+ #
7
+ # options:
8
+ # :title => The title of the note. Defaults: "Untitled"
9
+ # :text => The content of the note. Defaults: ""
10
+ # :html => The content of the note as html.
11
+ # :author => An Person that wrote the note.
12
+ # :url => Permanent location for an html representation of the
13
+ # note.
14
+ # :published => When the note was originally published.
15
+ # :updated => When the note was last updated.
16
+ # :uid => The unique id that identifies this note.
17
+ def initialize(options = {}, &blk)
18
+ init(options, &blk)
19
+ end
20
+
21
+ # Returns a hash of all relevant fields.
22
+ def to_hash
23
+ super.to_hash
24
+ end
25
+
26
+ # Returns a hash of all relevant fields with JSON activity streams
27
+ # conventions.
28
+ def to_json_hash
29
+ {
30
+ :objectType => "note",
31
+ }.merge(super)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,229 @@
1
+ module Nelumba
2
+ # This represents a notification that can be sent to a server when you wish
3
+ # to send information to a server that has not yet subscribed to you. Since
4
+ # this implies a lack of trust, a notification adds a layer so that the
5
+ # recipiant can verify the message contents.
6
+ class Notification
7
+ require 'xml'
8
+ require 'digest/sha2'
9
+
10
+ # The Activity that is represented by this notification.
11
+ attr_reader :activity
12
+
13
+ # The identity of the sender that can be used to discover the Identity
14
+ attr_reader :account
15
+
16
+ # Create an instance for a particular Nelumba::Activity.
17
+ def initialize activity, signature = nil, plaintext = nil
18
+ @activity = activity
19
+ @signature = signature
20
+ @plaintext = plaintext
21
+
22
+ account = activity.actor.uri
23
+
24
+ # XXX: Negotiate various weird uri schemes to find identity account
25
+ @account = account
26
+ end
27
+
28
+ # Creates an activity for following a particular Person.
29
+ def self.from_follow(user_author, followed_author)
30
+ activity = Nelumba::Activity.new(
31
+ :verb => :follow,
32
+ :object => followed_author,
33
+ :actor => user_author,
34
+ :title => "Now following #{followed_author.name}",
35
+ :content => "Now following #{followed_author.name}",
36
+ :content_type => "html"
37
+ )
38
+
39
+ self.new(activity)
40
+ end
41
+
42
+ # Creates an activity for unfollowing a particular Person.
43
+ def self.from_unfollow(user_author, followed_author)
44
+ activity = Nelumba::Activity.new(
45
+ :verb => "http://ostatus.org/schema/1.0/unfollow",
46
+ :object => followed_author,
47
+ :actor => user_author,
48
+ :title => "Stopped following #{followed_author.name}",
49
+ :content => "Stopped following #{followed_author.name}",
50
+ :content_type => "html"
51
+ )
52
+
53
+ self.new(activity)
54
+ end
55
+
56
+ # Creates an activity for a profile update.
57
+ def self.from_profile_update(user_author)
58
+ activity = Nelumba::Activity.new(
59
+ :verb => "http://ostatus.org/schema/1.0/update-profile",
60
+ :actor => user_author,
61
+ :title => "#{user_author.name} changed their profile information.",
62
+ :content => "#{user_author.name} changed their profile information.",
63
+ :content_type => "html"
64
+ )
65
+
66
+ self.new(activity)
67
+ end
68
+
69
+ # Will pull a Nelumba::Activity from the given payload and MIME type.
70
+ def self.from_data(content, content_type)
71
+ case content_type
72
+ when 'xml',
73
+ 'magic-envelope+xml',
74
+ 'application/xml',
75
+ 'application/text+xml',
76
+ 'application/magic-envelope+xml'
77
+ self.from_xml content
78
+ when 'json',
79
+ 'magic-envelope+json',
80
+ 'application/json',
81
+ 'application/text+json',
82
+ 'application/magic-envelope+json'
83
+ self.from_json content
84
+ end
85
+ end
86
+
87
+ # Will pull a Nelumba::Activity from a magic envelope described by the JSON.
88
+ def self.from_json(source)
89
+ end
90
+
91
+ # Will pull a Nelumba::Activity from a magic envelope described by the XML.
92
+ def self.from_xml(source)
93
+ if source.is_a?(String)
94
+ if source.length == 0
95
+ return nil
96
+ end
97
+
98
+ source = XML::Document.string(source,
99
+ :options => XML::Parser::Options::NOENT)
100
+ else
101
+ return nil
102
+ end
103
+
104
+ # Retrieve the envelope
105
+ envelope = source.find('/me:env',
106
+ 'me:http://salmon-protocol.org/ns/magic-env').first
107
+
108
+ return nil unless envelope
109
+
110
+ data = envelope.find('me:data',
111
+ 'me:http://salmon-protocol.org/ns/magic-env').first
112
+ return nil unless data
113
+
114
+ data_type = data.attributes["type"]
115
+ if data_type.nil?
116
+ data_type = 'application/atom+xml'
117
+ armored_data_type = ''
118
+ else
119
+ armored_data_type = Base64::urlsafe_encode64(data_type)
120
+ end
121
+
122
+ encoding = envelope.find('me:encoding',
123
+ 'me:http://salmon-protocol.org/ns/magic-env').first
124
+
125
+ algorithm = envelope.find(
126
+ 'me:alg',
127
+ 'me:http://salmon-protocol.org/ns/magic-env').first
128
+
129
+ signature = source.find('me:sig',
130
+ 'me:http://salmon-protocol.org/ns/magic-env').first
131
+
132
+ # Parse fields
133
+
134
+ # Well, if we cannot verify, we don't accept
135
+ return nil unless signature
136
+
137
+ # XXX: Handle key_id attribute
138
+ signature = signature.content
139
+ signature = Base64::urlsafe_decode64(signature)
140
+
141
+ if encoding.nil?
142
+ # When the encoding is omitted, use base64url
143
+ # Cite: Magic Envelope Draft Spec Section 3.3
144
+ armored_encoding = ''
145
+ encoding = 'base64url'
146
+ else
147
+ armored_encoding = Base64::urlsafe_encode64(encoding.content)
148
+ encoding = encoding.content.downcase
149
+ end
150
+
151
+ if algorithm.nil?
152
+ # When algorithm is omitted, use 'RSA-SHA256'
153
+ # Cite: Magic Envelope Draft Spec Section 3.3
154
+ armored_algorithm = ''
155
+ algorithm = 'rsa-sha256'
156
+ else
157
+ armored_algorithm = Base64::urlsafe_encode64(algorithm.content)
158
+ algorithm = algorithm.content.downcase
159
+ end
160
+
161
+ # Retrieve and decode data payload
162
+
163
+ data = data.content
164
+ armored_data = data
165
+
166
+ case encoding
167
+ when 'base64url'
168
+ data = Base64::urlsafe_decode64(data)
169
+ else
170
+ # Unsupported data encoding
171
+ return nil
172
+ end
173
+
174
+ # Signature plaintext
175
+ plaintext = "#{armored_data}.#{armored_data_type}.#{armored_encoding}.#{armored_algorithm}"
176
+
177
+ # Interpret data payload
178
+ payload = XML::Reader.string(data)
179
+ self.new Nelumba::Atom::Entry.new(payload).to_canonical, signature, plaintext
180
+ end
181
+
182
+ # Generate the xml for this notice and sign with the given private key.
183
+ def to_xml private_key
184
+ # Generate magic envelope
185
+ magic_envelope = XML::Document.new
186
+
187
+ magic_envelope.root = XML::Node.new 'env'
188
+
189
+ me_ns = XML::Namespace.new(magic_envelope.root,
190
+ 'me', 'http://salmon-protocol.org/ns/magic-env')
191
+
192
+ magic_envelope.root.namespaces.namespace = me_ns
193
+
194
+ # Armored Data <me:data>
195
+ data = @activity.to_atom
196
+ @plaintext = data
197
+ data_armored = Base64::urlsafe_encode64(data)
198
+ elem = XML::Node.new 'data', data_armored, me_ns
199
+ elem.attributes['type'] = 'application/atom+xml'
200
+ data_type_armored = 'YXBwbGljYXRpb24vYXRvbSt4bWw='
201
+ magic_envelope.root << elem
202
+
203
+ # Encoding <me:encoding>
204
+ magic_envelope.root << XML::Node.new('encoding', 'base64url', me_ns)
205
+ encoding_armored = 'YmFzZTY0dXJs'
206
+
207
+ # Signing Algorithm <me:alg>
208
+ magic_envelope.root << XML::Node.new('alg', 'RSA-SHA256', me_ns)
209
+ algorithm_armored = 'UlNBLVNIQTI1Ng=='
210
+
211
+ # Signature <me:sig>
212
+ plaintext =
213
+ "#{data_armored}.#{data_type_armored}.#{encoding_armored}.#{algorithm_armored}"
214
+
215
+ # Assign @signature to the signature generated from the plaintext
216
+ @signature = Nelumba::Crypto.emsa_sign(plaintext, private_key)
217
+
218
+ signature_armored = Base64::urlsafe_encode64(@signature)
219
+ magic_envelope.root << XML::Node.new('sig', signature_armored, me_ns)
220
+
221
+ magic_envelope.to_s :indent => true, :encoding => XML::Encoding::UTF_8
222
+ end
223
+
224
+ # Check the origin of this notification.
225
+ def verified? key
226
+ Nelumba::Crypto.emsa_verify(@plaintext, @signature, key)
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,251 @@
1
+ module Nelumba
2
+ module Object
3
+ require 'json'
4
+ require 'cgi'
5
+
6
+ # Determines what constitutes a username inside an update text
7
+ USERNAME_REGULAR_EXPRESSION = /(^|[ \t\n\r\f"'\(\[{]+)@([^ \t\n\r\f&?=@%\/\#]*[^ \t\n\r\f&?=@%\/\#.!:;,"'\]}\)])(?:@([^ \t\n\r\f&?=@%\/\#]*[^ \t\n\r\f&?=@%\/\#.!:;,"'\]}\)]))?/
8
+
9
+ attr_reader :title
10
+ attr_reader :author
11
+ attr_reader :display_name
12
+ attr_reader :uid
13
+ attr_reader :url
14
+
15
+ # Natural-language description of this object
16
+ attr_reader :summary
17
+
18
+ # The image representation of this object
19
+ attr_reader :image
20
+
21
+ # Natural-language text content
22
+ attr_reader :content
23
+
24
+ attr_reader :published
25
+ attr_reader :updated
26
+
27
+ # Holds the content as plain text.
28
+ attr_reader :text
29
+
30
+ # Holds the content as html.
31
+ attr_reader :html
32
+
33
+ def initialize(options = {}, &blk)
34
+ init(options, &blk)
35
+ end
36
+
37
+ def init(options = {}, &blk)
38
+ @author = options[:author]
39
+ @content = options[:content]
40
+ @display_name = options[:display_name]
41
+ @uid = options[:uid]
42
+ @url = options[:url]
43
+ @summary = options[:summary]
44
+ @published = options[:published] || Time.now
45
+ @updated = options[:updated] || Time.now
46
+ @title = options[:title] || "Untitled"
47
+
48
+ options[:published] = @published
49
+ options[:updated] = @updated
50
+ options[:title] = @title
51
+
52
+ # Alternative representations of 'content'
53
+ @text = options[:text] || @content || ""
54
+ @content = @content || options[:text]
55
+ options[:text] = @text
56
+
57
+ @html = options[:html] || to_html(&blk)
58
+ @content = @content || @html
59
+ options[:html] = @html
60
+ end
61
+
62
+ # TODO: Convert html to safe text
63
+ def to_text()
64
+ return @text if @text
65
+
66
+ return "" if @html.nil?
67
+
68
+ ""
69
+ end
70
+
71
+ # Produces an HTML string representing the Object's content.
72
+ #
73
+ # Requires a block that is given two arguments: the username and the domain
74
+ # that should return a Nelumba::Person that matches when a @username tag
75
+ # is found.
76
+ def to_html(&blk)
77
+ return @html if @html
78
+
79
+ return "" if @text.nil?
80
+
81
+ out = CGI.escapeHTML(@text)
82
+
83
+ # Replace any absolute addresses with a link
84
+ # Note: Do this first! Otherwise it will add anchors inside anchors!
85
+ out.gsub!(/(http[s]?:\/\/\S+[a-zA-Z0-9\/}])/, "<a href='\\1'>\\1</a>")
86
+
87
+ # we let almost anything be in a username, except those that mess with urls.
88
+ # but you can't end in a .:;, or !
89
+ # also ignore container chars [] () "" '' {}
90
+ # XXX: the _correct_ solution will be to use an email validator
91
+ out.gsub!(USERNAME_REGULAR_EXPRESSION) do |match|
92
+ if blk
93
+ author = blk.call($2, $3)
94
+ end
95
+
96
+ if author
97
+ "#{$1}<a href='#{author.uri}'>@#{$2}</a>"
98
+ else
99
+ "#{$1}@#{$2}"
100
+ end
101
+ end
102
+
103
+ out.gsub!( /(^|\s+)#(\p{Word}+)/ ) do |match|
104
+ "#{$1}<a href='/search?search=%23#{$2}'>##{$2}</a>"
105
+ end
106
+
107
+ out.gsub!(/\n/, "<br/>")
108
+
109
+ out
110
+ end
111
+
112
+ # Returns a list of Nelumba::Person's for those replied by the object.
113
+ #
114
+ # Requires a block that is given two arguments: the username and the domain
115
+ # that should return a Nelumba::Person that matches when a @username tag
116
+ # is found.
117
+ #
118
+ # Usage:
119
+ #
120
+ # note = Nelumba::Note.new(:text => "Hello @foo")
121
+ # note.reply_to do |username, domain|
122
+ # i = identities.select {|e| e.username == username && e.domain == domain }.first
123
+ # i.author if i
124
+ # end
125
+ #
126
+ # With a persistence backend:
127
+ # note.reply_to do |username, domain|
128
+ # i = Identity.first(:username => /^#{Regexp.escape(username)}$/i
129
+ # i.author if i
130
+ # end
131
+ def reply_to(&blk)
132
+ out = CGI.escapeHTML(@text)
133
+
134
+ # we let almost anything be in a username, except those that mess with urls.
135
+ # but you can't end in a .:;, or !
136
+ # also ignore container chars [] () "" '' {}
137
+ # XXX: the _correct_ solution will be to use an email validator
138
+ ret = []
139
+ out.match(/^#{USERNAME_REGULAR_EXPRESSION}/) do |beginning, username, domain|
140
+ ret << blk.call(username, domain) if blk
141
+ end
142
+
143
+ ret
144
+ end
145
+
146
+ def to_hash(scheme = 'https', domain = 'example.org', port = nil)
147
+ url_start = "#{scheme}://#{domain}#{port.nil? ? "" : ":#{port}"}"
148
+
149
+ uid = self.uid
150
+ url = self.url
151
+
152
+ if uid && uid.start_with?("/")
153
+ uid = "#{url_start}#{uid}"
154
+ end
155
+
156
+ if url && url.start_with?("/")
157
+ url = "#{url_start}#{url}"
158
+ end
159
+
160
+ {
161
+ :author => self.author,
162
+ :summary => self.summary,
163
+ :content => self.content,
164
+ :display_name => self.display_name,
165
+ :uid => uid,
166
+ :url => url,
167
+ :published => self.published,
168
+ :updated => self.updated,
169
+ :title => self.title,
170
+ :text => self.text,
171
+ :html => self.html,
172
+ }
173
+ end
174
+
175
+ def to_json_hash(scheme = 'https', domain = 'example.org', port = nil)
176
+ url_start = "#{scheme}://#{domain}#{port.nil? ? "" : ":#{port}"}"
177
+
178
+ uid = self.uid
179
+ url = self.url
180
+
181
+ if uid && uid.start_with?("/")
182
+ uid = "#{url_start}#{uid}"
183
+ end
184
+
185
+ if url && url.start_with?("/")
186
+ url = "#{url_start}#{url}"
187
+ end
188
+
189
+ {
190
+ :author => self.author,
191
+ :content => self.content,
192
+ :summary => self.summary,
193
+ :displayName => self.display_name,
194
+ :id => uid,
195
+ :url => url,
196
+ :title => self.title,
197
+ :published => (self.published ? self.published.to_date.rfc3339 + 'Z' : nil),
198
+ :updated => (self.updated ? self.updated.to_date.rfc3339 + 'Z' : nil),
199
+ }
200
+ end
201
+
202
+ # Returns a list of Nelumba::Person's for those mentioned within the object.
203
+ #
204
+ # Requires a block that is given two arguments: the username and the domain
205
+ # that should return a Nelumba::Person that matches when a @username tag
206
+ # is found.
207
+ #
208
+ # Usage:
209
+ #
210
+ # note = Nelumba::Note.new(:text => "Hello @foo")
211
+ # note.mentions do |username, domain|
212
+ # i = identities.select {|e| e.username == username && e.domain == domain }.first
213
+ # i.author if i
214
+ # end
215
+ #
216
+ # With a persistence backend:
217
+ # note.mentions do |username, domain|
218
+ # i = Identity.first(:username => /^#{Regexp.escape(username)}$/i)
219
+ # i.author if i
220
+ # end
221
+ def mentions(&blk)
222
+ if self.respond_to? :text
223
+ out = self.text || ""
224
+ else
225
+ out = self.content || ""
226
+ end
227
+
228
+ out = CGI.escapeHTML(out)
229
+
230
+ # we let almost anything be in a username, except those that mess with urls.
231
+ # but you can't end in a .:;, or !
232
+ # also ignore container chars [] () "" '' {}
233
+ # XXX: the _correct_ solution will be to use an email validator
234
+ ret = []
235
+ out.scan(USERNAME_REGULAR_EXPRESSION) do |beginning, username, domain|
236
+ ret << blk.call(username, domain) if blk
237
+ end
238
+
239
+ ret
240
+ end
241
+
242
+ # Returns a string containing the JSON representation of this Object.
243
+ def to_json(*args)
244
+ to_json_hash.delete_if{|k,v| v.nil?}.to_json(*args)
245
+ end
246
+
247
+ def to_as1(*args)
248
+ to_json_hash.delete_if{|k,v| v.nil?}.to_json(*args)
249
+ end
250
+ end
251
+ end