nelumba 0.0.13

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