nelumba-mongodb 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.
@@ -0,0 +1,30 @@
1
+ module Nelumba
2
+ class Article
3
+ require 'redcarpet'
4
+
5
+ include Nelumba::EmbeddedObject
6
+
7
+ key :markdown
8
+
9
+ def initialize(options = {})
10
+ super(options)
11
+
12
+ self.render_markdown unless options[:content]
13
+ end
14
+
15
+ def markdown=(value)
16
+ super value
17
+ render_markdown
18
+ end
19
+
20
+ # Generates content field with the markdown field.
21
+ def render_markdown
22
+ return if self.markdown.nil?
23
+
24
+ render_as = Redcarpet::Render::HTML
25
+ engine = Redcarpet::Markdown.new(render_as, :autolink => true,
26
+ :space_after_headers => true)
27
+ self.content = engine.render(self.markdown)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,213 @@
1
+ module Nelumba
2
+ # This represents how a Person can authenticate to act on our server.
3
+ # This is attached to an Identity and a Person. Use this to allow an
4
+ # Person to generate Activities on this server.
5
+ class Authorization
6
+ require 'bcrypt'
7
+ require 'json'
8
+ require 'nokogiri'
9
+
10
+ include MongoMapper::Document
11
+
12
+ # Ensure writes happen
13
+ safe
14
+
15
+ # An Authorization involves a Person.
16
+ key :person_id, ObjectId
17
+ belongs_to :person, :class_name => 'Nelumba::Person'
18
+
19
+ # Whether or not this authorization requires ssl
20
+ key :ssl
21
+
22
+ # The domain this authorization is registered for
23
+ key :domain
24
+
25
+ # The port authorization is tied to
26
+ key :port
27
+
28
+ # An Authorization involves an Identity.
29
+ key :identity_id, ObjectId
30
+ belongs_to :identity, :class_name => 'Nelumba::Identity'
31
+
32
+ # You authorize with a username
33
+ key :username, String
34
+
35
+ # A private key can verify that external information originated with this
36
+ # account.
37
+ key :private_key, String
38
+
39
+ # A password can authenticate you if you are manually signing in as a human
40
+ # being. The password is hashed to prevent information leaking.
41
+ key :hashed_password, String
42
+
43
+ # You must have enough credentials to be able to log into the system:
44
+ validates_presence_of :username
45
+ validates_presence_of :identity
46
+ validates_presence_of :hashed_password
47
+
48
+ # Log modification
49
+ timestamps!
50
+
51
+ # Generate a Hash containing this person's LRDD meta info.
52
+ def self.lrdd(username)
53
+ username = username.match /(?:acct\:)?([^@]+)(?:@([^@]+))?$/
54
+ username = username[1] if username
55
+ if username.nil?
56
+ return nil
57
+ end
58
+
59
+ # Find the person
60
+ auth = Authorization.find_by_username(/#{Regexp.escape(username)}/i)
61
+ return nil unless auth
62
+
63
+ domain = auth.identity.domain
64
+ port = auth.identity.port
65
+ url = "http#{auth.identity.ssl ? "s" : ""}://#{auth.identity.domain}#{port ? ":#{port}" : ""}"
66
+ feed_id = auth.identity.outbox.id
67
+ person_id = auth.person.id
68
+
69
+ {
70
+ :subject => "acct:#{username}@#{domain}#{port ? ":#{port}" : ""}",
71
+ :expires => "#{(Time.now.utc.to_date >> 1).xmlschema}Z",
72
+ :aliases => [
73
+ "#{url}#{auth.identity.profile_page}",
74
+ "#{url}/feeds/#{feed_id}"
75
+ ],
76
+ :links => [
77
+ {:rel => "http://webfinger.net/rel/profile-page",
78
+ :href => "#{url}/people/#{person_id}"},
79
+ {:rel => "http://schemas.google.com/g/2010#updates-from",
80
+ :href => "#{url}/feeds/#{feed_id}"},
81
+ {:rel => "salmon",
82
+ :href => "#{url}/people/#{person_id}/salmon"},
83
+ {:rel => "http://salmon-protocol.org/ns/salmon-replies",
84
+ :href => "#{url}/people/#{person_id}/salmon"},
85
+ {:rel => "http://salmon-protocol.org/ns/salmon-mention",
86
+ :href => "#{url}/people/#{person_id}/salmon"},
87
+ {:rel => "magic-public-key",
88
+ :href => "data:application/magic-public-key,#{auth.identity.public_key}"}
89
+
90
+ # TODO: ostatus subscribe
91
+ #{:rel => "http://ostatus.org/schema/1.0/subscribe",
92
+ # :template => "#{url}/subscriptions?url={uri}&_method=post"}
93
+ ]
94
+ }
95
+ end
96
+
97
+ # Generate a String containing the json representation of this person's LRDD.
98
+ def self.jrd(username)
99
+ lrdd = self.lrdd(username)
100
+ return nil if lrdd.nil?
101
+
102
+ lrdd.to_json
103
+ end
104
+
105
+ # Generate a String containing the XML representaton of this person's LRDD.
106
+ def self.xrd(username)
107
+ lrdd = self.lrdd(username)
108
+ return nil if lrdd.nil?
109
+
110
+ # Build xml
111
+ builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
112
+ xml.XRD("xmlns" => 'http://docs.oasis-open.org/ns/xri/xrd-1.0',
113
+ "xmlns:xsi" => 'http://www.w3.org/2001/XMLSchema-instance') do
114
+ xml.Subject lrdd[:subject]
115
+ xml.Expires lrdd[:expires]
116
+
117
+ lrdd[:aliases].each do |alias_name|
118
+ xml.Alias alias_name
119
+ end
120
+
121
+ lrdd[:links].each do |link|
122
+ xml.Link link
123
+ end
124
+ end
125
+ end
126
+
127
+ # Output
128
+ builder.to_xml
129
+ end
130
+
131
+ # Create a hash of the password.
132
+ def self.hash_password(password)
133
+ BCrypt::Password.create(password, :cost => Nelumba::BCRYPT_ROUNDS)
134
+ end
135
+
136
+ # Determine if the given password matches the account.
137
+ def authenticated?(password)
138
+ BCrypt::Password.new(hashed_password) == password
139
+ end
140
+
141
+ # :nodoc: Do not allow the password to be set at any cost.
142
+ def password=(value)
143
+ end
144
+
145
+ # Cleanup any unexpected keys.
146
+ def self.sanitize_params(params)
147
+ # Convert Symbols to Strings
148
+ params.keys.each do |k|
149
+ if k.is_a? Symbol
150
+ params[k.to_s] = params[k]
151
+ params.delete k
152
+ end
153
+ end
154
+
155
+ # Delete unknown keys
156
+ params.keys.each do |k|
157
+ unless self.keys.keys.map.include?(k)
158
+ params.delete(k)
159
+ end
160
+ end
161
+
162
+ # Delete immutable fields
163
+ params.delete("id")
164
+ params.delete("_id")
165
+
166
+ # Convert to symbols
167
+ params.keys.each do |k|
168
+ params[k.intern] = params[k]
169
+ params.delete k
170
+ end
171
+
172
+ params
173
+ end
174
+
175
+ # Create a new Authorization.
176
+ def initialize(*args)
177
+ params = {}
178
+ params = args.shift if args.length > 0
179
+
180
+ params["password"] = params[:password] if params[:password]
181
+ params.delete :password
182
+
183
+ params["hashed_password"] = Authorization.hash_password(params["password"])
184
+ params.delete "password"
185
+
186
+ params = Authorization.sanitize_params(params)
187
+
188
+ person = Nelumba::Person.new_local(params[:username],
189
+ params[:domain],
190
+ params[:port],
191
+ params[:ssl])
192
+ person.save
193
+ params[:person_id] = person.id
194
+
195
+ keypair = Nelumba::Crypto.new_keypair
196
+ params[:private_key] = keypair.private_key
197
+
198
+ identity = Nelumba::Identity.new_local(person,
199
+ params[:username],
200
+ params[:domain],
201
+ params[:port],
202
+ params[:ssl],
203
+ keypair.public_key)
204
+ identity.save
205
+ params[:identity_id] = identity.id
206
+
207
+ params[:person] = person
208
+ params[:identity] = identity
209
+
210
+ super params, *args
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,130 @@
1
+ module Nelumba
2
+ class Avatar
3
+ require 'RMagick'
4
+
5
+ include MongoMapper::Document
6
+
7
+ # The avatar belongs to a particular Person
8
+ key :author_id, ObjectId
9
+ belongs_to :author, :class_name => 'Nelumba::Person'
10
+
11
+ # The array of sizes this avatar has stored.
12
+ key :sizes, Array, :default => []
13
+
14
+ # The content type for the image.
15
+ key :content_type
16
+
17
+ # Log modification.
18
+ timestamps!
19
+
20
+ # Create a new Avatar from the given blob
21
+ def self.from_blob!(author, blob, options = {})
22
+ avatar = Avatar.new(:author_id => author.id,
23
+ :sizes => options[:sizes])
24
+
25
+ image = Magick::ImageList.new
26
+ image.from_blob(blob)
27
+
28
+ # Store the content_type
29
+ avatar.content_type = image.mime_type
30
+
31
+ # Resize the images to fit the given sizes (crop to the aspect ratio)
32
+ # And store them in the storage backend.
33
+ options[:sizes] ||= []
34
+ images = options[:sizes].each do |size|
35
+ width = size[0]
36
+ height = size[1]
37
+
38
+ # Resize to maintain aspect ratio
39
+ resized = image.resize_to_fill(width, height)
40
+ self.storage_write "avatar_#{avatar.id}_#{width}x#{height}", resized.to_blob
41
+ end
42
+
43
+ # Find old avatar
44
+ old = Avatar.first(:author_id => author.id)
45
+ if old
46
+ old.destroy
47
+ end
48
+
49
+ avatar.save
50
+ avatar
51
+ end
52
+
53
+ # Create a new Avatar from the given url
54
+ def self.from_url!(author, url, options = {})
55
+ # Pull image down
56
+ response = self.pull_url(url, options[:content_type])
57
+ return nil unless response.kind_of? Net::HTTPSuccess
58
+
59
+ self.from_blob!(author, response.body, options)
60
+ end
61
+
62
+ def url(size = nil)
63
+ return nil if self.sizes.empty?
64
+
65
+ size = self.sizes.first unless size
66
+ return nil unless self.sizes.include? size
67
+
68
+ "/avatars/#{self.id}/#{size[0]}x#{size[1]}"
69
+ end
70
+
71
+ # Retrieve the avatar image as a byte string.
72
+ def read(size = nil)
73
+ return nil if self.sizes.empty?
74
+
75
+ size = self.sizes.first unless size
76
+ return nil unless self.sizes.include? size
77
+
78
+ Avatar.storage_read "avatar_#{self.id}_#{size[0]}x#{size[1]}"
79
+ end
80
+
81
+ # Yield a base64 string encoded with the content type
82
+ def read_base64(size = nil)
83
+ data = self.read(size)
84
+ return nil unless data
85
+
86
+ "data:#{self.content_type};base64,#{Base64.encode64(data)}"
87
+ end
88
+
89
+ private
90
+
91
+ # :nodoc:
92
+ def self.pull_url(url, content_type = nil, limit = 10)
93
+ uri = URI(url)
94
+ request = Net::HTTP::Get.new(uri.request_uri)
95
+ request.content_type = content_type if content_type
96
+
97
+ http = Net::HTTP.new(uri.hostname, uri.port)
98
+ if uri.scheme == 'https'
99
+ http.use_ssl = true
100
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
101
+ end
102
+
103
+ response = http.request(request)
104
+
105
+ if response.is_a?(Net::HTTPRedirection) && limit > 0
106
+ location = response['location']
107
+ self.pull_url(location, content_type, limit - 1)
108
+ else
109
+ response
110
+ end
111
+ end
112
+
113
+ # :nodoc:
114
+ def self.storage
115
+ @@grid ||= Mongo::Grid.new(MongoMapper.database)
116
+ end
117
+
118
+ # TODO: Add ability to read from filesystem
119
+ # :nodoc:
120
+ def self.storage_read(id)
121
+ self.storage.get(id).read
122
+ end
123
+
124
+ # TODO: Add ability to store on filesystem
125
+ # :nodoc:
126
+ def self.storage_write(id, data)
127
+ self.storage.put(data, :_id => id)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,7 @@
1
+ module Nelumba
2
+ class Comment
3
+ include Nelumba::EmbeddedObject
4
+
5
+ key :in_reply_to, :default => []
6
+ end
7
+ end
@@ -0,0 +1,43 @@
1
+ module Nelumba
2
+ module EmbeddedObject
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+ include MongoMapper::EmbeddedDocument
6
+
7
+ def initialize(*args, &blk)
8
+ init(*args, &blk)
9
+
10
+ super(*args, &blk)
11
+ end
12
+
13
+ belongs_to :author, :class_name => 'Nelumba::Person'
14
+ key :author_id, ObjectId
15
+
16
+ key :title
17
+ key :uid
18
+ key :url
19
+ key :display_name
20
+ key :summary
21
+ key :content
22
+ key :image
23
+
24
+ # Automated Timestamps
25
+ key :published, Time
26
+ key :updated, Time
27
+ before_save :update_timestamps
28
+
29
+ def update_timestamps
30
+ now = Time.now.utc
31
+ self[:published] ||= now if !persisted?
32
+ self[:updated] = now
33
+ end
34
+
35
+ def self.find_by_id(id)
36
+ Nelumba::Activity.object_by_id_and_type(id, self)
37
+ end
38
+
39
+ embedded_in :'Nelumba::Activity'
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,268 @@
1
+ module Nelumba
2
+ class Feed
3
+ def initialize(*args); super(*args); end
4
+
5
+ include MongoMapper::Document
6
+
7
+ # Ensure writes happen
8
+ safe
9
+
10
+ # A unique identifier for this Feed.
11
+ key :uid
12
+
13
+ # A URL for this Feed that can be used to retrieve a representation.
14
+ key :url
15
+
16
+ # Feeds generally belong to a person.
17
+ key :person_id, ObjectId
18
+ belongs_to :person, :class_name => 'Nelumba::Person'
19
+
20
+ remove_method :categories
21
+ key :categories, :default => []
22
+
23
+ # The type of rights one has to this feed generally for human display.
24
+ key :rights
25
+
26
+ # The title of this feed.
27
+ key :title
28
+
29
+ # The representation of the title. (e.g. "html")
30
+ key :title_type
31
+
32
+ # The subtitle of the feed.
33
+ key :subtitle
34
+
35
+ # The representation of the subtitle. (e.g. "html")
36
+ key :subtitle_type
37
+
38
+ # An array of Persons that contributed to this Feed.
39
+ key :contributors_ids, Array, :default => []
40
+ remove_method :contributors
41
+ many :contributors, :class_name => 'Nelumba::Person', :in => :contributors_ids
42
+
43
+ # An Array of Persons that create the content in this Feed.
44
+ key :authors_ids, Array, :default => []
45
+ remove_method :authors
46
+ many :authors, :class_name => 'Nelumba::Person', :in => :authors_ids
47
+
48
+ # An Array of Activities that are contained in this Feed.
49
+ key :items_ids, Array
50
+ many :items, :class_name => 'Nelumba::Activity',
51
+ :in => :items_ids,
52
+ :order => :published.desc
53
+
54
+ # A Hash containing information about the entity that is generating content
55
+ # for this Feed when it isn't a person.
56
+ key :generator
57
+
58
+ # Feeds may have an icon to represent them.
59
+ #key :icon, :class_name => 'Image'
60
+
61
+ # Feeds may have an image they use as a logo.
62
+ #key :logo, :class_name => 'Image'
63
+
64
+ # TODO: Normalize the first 100 or so activities. I dunno.
65
+ key :normalized
66
+
67
+ # Automated Timestamps
68
+ key :published, Time
69
+ key :updated, Time
70
+ before_save :update_timestamps
71
+
72
+ def update_timestamps
73
+ now = Time.now.utc
74
+ self[:published] ||= now if !persisted?
75
+ self[:updated] = now
76
+ end
77
+
78
+ # The external feeds being aggregated.
79
+ key :following_ids, Array
80
+ many :following, :in => :following_ids, :class_name => 'Nelumba::Feed'
81
+
82
+ # Who is aggregating this feed.
83
+ key :followers_ids, Array
84
+ many :followers, :in => :followers_ids, :class_name => 'Nelumba::Feed'
85
+
86
+ # Subscription status.
87
+ # Since subscriptions are done by the server, we only need to share one
88
+ # secret/token pair for all users that follow this feed on the server.
89
+ # This is done at the Feed level since people may want to follow your
90
+ # "timeline", or your "favorites". Or People who use Nelumba will ignore
91
+ # the Person aggregate class and go with their own thing.
92
+ key :subscription_secret
93
+ key :verification_token
94
+
95
+ # Create a new Feed if the given Feed is not found by its id.
96
+ def self.find_or_create_by_uid!(arg, *args)
97
+ if arg.is_a? Nelumba::Feed
98
+ uid = arg.uid
99
+ else
100
+ uid = arg[:uid]
101
+ end
102
+
103
+ feed = self.first(:uid => uid)
104
+ return feed if feed
105
+
106
+ begin
107
+ feed = create!(arg, *args)
108
+ rescue
109
+ feed = self.first(:uid => uid) or raise
110
+ end
111
+
112
+ feed
113
+ end
114
+
115
+ # Create a new Feed from a Hash of values or a Nelumba::Feed.
116
+ def initialize(*args)
117
+ hash = {}
118
+ if args.length > 0
119
+ hash = args.shift
120
+ end
121
+
122
+ if hash.is_a? Nelumba::Feed
123
+ hash = hash.to_hash
124
+ end
125
+
126
+ if hash[:authors].is_a? Array
127
+ hash[:authors].map! do |author|
128
+ if author.is_a? Hash
129
+ author = Nelumba::Person.find_or_create_by_uid!(author)
130
+ end
131
+ author
132
+ end
133
+ end
134
+
135
+ if hash[:contributors].is_a? Array
136
+ hash[:contributors].map! do |contributor|
137
+ if contributor.is_a? Hash
138
+ contributor = Nelumba::Person.find_or_create_by_uid!(contributor)
139
+ end
140
+ contributor
141
+ end
142
+ end
143
+
144
+ if hash[:items].is_a? Array
145
+ hash[:items].map! do |item|
146
+ if item.is_a? Hash
147
+ item = Nelumba::Activity.find_or_create_by_uid!(item)
148
+ end
149
+ item
150
+ end
151
+ end
152
+
153
+ super hash, *args
154
+ end
155
+
156
+ # Discover a feed by the given feed location or account.
157
+ def self.discover!(feed_identifier)
158
+ feed = Feed.first(:url => feed_identifier)
159
+ return feed if feed
160
+
161
+ feed = Nelumba::Discover.feed(feed_identifier)
162
+ return false unless feed
163
+
164
+ existing_feed = Feed.first(:uid => feed.uid)
165
+ return existing_feed if existing_feed
166
+
167
+ self.create!(feed)
168
+ end
169
+
170
+ # Merges the information in the given feed with this one.
171
+ def merge!(feed)
172
+ # Merge metadata
173
+ meta_data = feed.to_hash
174
+ meta_data.delete :items
175
+ meta_data.delete :authors
176
+ meta_data.delete :contributors
177
+ meta_data.delete :uid
178
+ self.update_attributes!(meta_data)
179
+
180
+ # Merge new/updated authors
181
+ feed.authors.each do |author|
182
+ end
183
+
184
+ # Merge new/updated contributors
185
+ feed.contributors.each do |author|
186
+ end
187
+
188
+ # Merge new/updated activities
189
+ feed.items.each do |activity|
190
+ end
191
+ end
192
+
193
+ # Retrieve the feed's activities with the most recent first.
194
+ def ordered
195
+ Nelumba::Activity.where(:id => self.items_ids).order(:published => :desc)
196
+ end
197
+
198
+ # Follow the given feed. When a new post is placed in this feed, it
199
+ # will be copied into ours.
200
+ def follow!(feed)
201
+ self.following << feed
202
+ self.save
203
+
204
+ # Subscribe to that feed on this server if not already.
205
+ end
206
+
207
+ # Unfollow the given feed. Our feed will no longer receive new posts from
208
+ # the given feed.
209
+ def unfollow!(feed)
210
+ self.following_ids.delete(feed.id)
211
+ self.save
212
+ end
213
+
214
+ # Denotes that the given feed will contain your posts.
215
+ def followed_by!(feed)
216
+ self.followers << feed
217
+ self.save
218
+ end
219
+
220
+ # Denotes that the given feed will not contain your posts from now on.
221
+ def unfollowed_by!(feed)
222
+ self.followers_ids.delete(feed.id)
223
+ self.save
224
+ end
225
+
226
+ # Add to the feed and tell subscribers.
227
+ def post!(activity)
228
+ if activity.is_a?(Hash)
229
+ # Create a new activity
230
+ activity = Nelumba::Activity.create!(activity)
231
+ end
232
+
233
+ activity.feed_id = self.id
234
+ activity.save
235
+
236
+ self.items << activity
237
+ self.save
238
+
239
+ publish(activity)
240
+
241
+ activity
242
+ end
243
+
244
+ # Remove the activity from the feed.
245
+ def delete!(activity)
246
+ self.items_ids.delete(activity.id)
247
+ self.save
248
+ end
249
+
250
+ # Add a copy to our feed and tell subscribers.
251
+ def repost!(activity)
252
+ self.items << activity
253
+ self.save
254
+
255
+ publish(activity)
256
+ end
257
+
258
+ # Publish an activity that is within our feed.
259
+ def publish(activity)
260
+ # Push to direct followers
261
+ self.followers.each do |feed|
262
+ feed.repost! activity
263
+ end
264
+
265
+ # TODO: PuSH Hubs
266
+ end
267
+ end
268
+ end