nelumba-mongodb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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