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,191 @@
1
+ module Nelumba
2
+ # This represents the information necessary to talk to an Person that is
3
+ # external to our node, or it represents how to talk to us.
4
+ # An Identity stores endpoints that are used to push or pull Activities from.
5
+ class Identity
6
+ def initialize(*args); super(*args); end
7
+
8
+ include MongoMapper::Document
9
+
10
+ # Ensure writes happen
11
+ safe
12
+
13
+ # public keys are good for 4 weeks
14
+ PUBLIC_KEY_LEASE_DAYS = 28
15
+
16
+ belongs_to :person, :class_name => 'Nelumba::Person'
17
+ key :person_id, ObjectId
18
+
19
+ key :username
20
+ key :ssl
21
+ key :domain
22
+ key :port
23
+
24
+ # Identities have a public key that they use to sign salmon responses.
25
+ # Leasing: To ensure that keys can only be compromised in a small window but
26
+ # not require the server to retrieve a key per update, we store a lease.
27
+ # When the lease expires, and a notification comes, we retrieve the key.
28
+ key :public_key
29
+ key :public_key_lease, Date
30
+
31
+ key :salmon_endpoint
32
+ key :dialback_endpoint
33
+ key :activity_inbox_endpoint
34
+ key :activity_outbox_endpoint
35
+ key :profile_page
36
+
37
+ key :outbox_id, ObjectId
38
+ belongs_to :outbox, :class_name => 'Nelumba::Feed'
39
+
40
+ key :inbox_id, ObjectId
41
+ belongs_to :inbox, :class_name => 'Nelumba::Feed'
42
+
43
+ timestamps!
44
+
45
+ # Extends the lease for the public key so it remains valid through the given
46
+ # expiry period.
47
+ def reset_key_lease
48
+ self.public_key_lease = (DateTime.now + PUBLIC_KEY_LEASE_DAYS).to_date
49
+ end
50
+
51
+ # Extends the lease for the public key so it remains valid through the given
52
+ # expiry period and saves.
53
+ def reset_key_lease!
54
+ reset_key_lease
55
+ self.save
56
+ end
57
+
58
+ # Invalidates the public key
59
+ def invalidate_public_key!
60
+ self.public_key_lease = nil
61
+ self.save
62
+ end
63
+
64
+ # Returns the valid public key
65
+ def return_or_discover_public_key
66
+ if self.public_key_lease.nil? or
67
+ self.public_key_lease < DateTime.now.to_date
68
+ # Lease has expired, get the public key again
69
+ identity = Nelumba.discover_identity("acct:#{self.username}@#{self.domain}")
70
+
71
+ self.public_key = identity.public_key
72
+ reset_key_lease
73
+
74
+ self.save
75
+ end
76
+
77
+ self.public_key
78
+ end
79
+
80
+ def self.new_local(person, username, domain, port, ssl, public_key)
81
+ Nelumba::Identity.new(
82
+ :username => username,
83
+ :domain => domain,
84
+ :ssl => ssl,
85
+ :port => port,
86
+ :person_id => person.id,
87
+ :public_key => public_key,
88
+ :salmon_endpoint => "/people/#{person.id}/salmon",
89
+ :dialback_endpoint => "/people/#{person.id}/dialback",
90
+ :activity_inbox_endpoint => "/people/#{person.id}/inbox",
91
+ :activity_outbox_endpoint => "/people/#{person.id}/outbox",
92
+ :profile_page => "/people/#{person.id}",
93
+ :outbox_id => person.activities.id,
94
+ :inbox_id => person.timeline.id
95
+ )
96
+ end
97
+
98
+ def self.find_by_identifier(identifier)
99
+ matches = identifier.match /^(?:.+\:)?([^@]+)@(.+)$/
100
+
101
+ username = matches[1].downcase
102
+ domain = matches[2].downcase
103
+
104
+ Nelumba::Identity.first(:username => username,
105
+ :domain => domain)
106
+ end
107
+
108
+ # Create a new Identity from a Hash of values or a Nelumba::Identity.
109
+ # TODO: Create outbox and inbox aggregates to hold feed and sent activities
110
+ def self.create!(*args)
111
+ hash = {}
112
+ if args.length > 0
113
+ hash = args.shift
114
+ end
115
+
116
+ if hash.is_a? Nelumba::Identity
117
+ hash = hash.to_hash
118
+ end
119
+
120
+ hash["username"] = hash["username"].downcase if hash["username"]
121
+ hash["username"] = hash[:username].downcase if hash[:username]
122
+ hash.delete :username
123
+
124
+ hash["domain"] = hash["domain"].downcase if hash["domain"]
125
+ hash["domain"] = hash[:domain].downcase if hash[:domain]
126
+ hash.delete :domain
127
+
128
+ hash = self.sanitize_params(hash)
129
+
130
+ super hash, *args
131
+ end
132
+
133
+ # Create a new Identity from a Hash of values or a Nelumba::Identity.
134
+ def self.create(*args)
135
+ self.create! *args
136
+ end
137
+
138
+ # Ensure params has only valid keys
139
+ def self.sanitize_params(params)
140
+ params.keys.each do |k|
141
+ if k.is_a? Symbol
142
+ params[k.to_s] = params[k]
143
+ params.delete k
144
+ end
145
+ end
146
+
147
+ # Delete unknown keys
148
+ params.keys.each do |k|
149
+ unless self.keys.keys.include? k
150
+ params.delete(k)
151
+ end
152
+ end
153
+
154
+ # Delete immutable fields
155
+ params.delete("_id")
156
+
157
+ # Convert back to symbols
158
+ params.keys.each do |k|
159
+ params[k.intern] = params[k]
160
+ params.delete k
161
+ end
162
+
163
+ params
164
+ end
165
+
166
+ # Discover an identity from the given user identifier.
167
+ def self.discover!(account)
168
+ identity = Nelumba::Identity.find_by_identifier(account)
169
+ return identity if identity
170
+
171
+ identity = Nelumba.discover_identity(account)
172
+ return false unless identity
173
+
174
+ self.create!(identity)
175
+ end
176
+
177
+ # Discover the associated author for this identity.
178
+ def discover_person!
179
+ Person.discover!("acct:#{self.username}@#{self.domain}")
180
+ end
181
+
182
+ # Post an existing activity to the inbox of the person that owns this Identity
183
+ def post!(activity)
184
+ if self.person.local?
185
+ self.person.local_deliver! activity
186
+ else
187
+ self.inbox.repost! activity
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,121 @@
1
+ module Nelumba
2
+ class Image
3
+ require 'RMagick'
4
+
5
+ include Nelumba::EmbeddedObject
6
+
7
+ # The array of sizes this image has stored.
8
+ key :sizes, Array, :default => []
9
+
10
+ # The content type for the image.
11
+ key :content_type
12
+
13
+ # Create a new Image from the given blob
14
+ def self.from_blob!(author, blob, options = {})
15
+ image = self.new({:author_id => author.id}.merge(options))
16
+
17
+ canvas = Magick::ImageList.new
18
+ canvas.from_blob(blob)
19
+
20
+ self.storage_write "full_image_#{image.id}", blob
21
+
22
+ # Store the content_type
23
+ image.content_type = canvas.mime_type
24
+
25
+ # Resize the canvass to fit the given sizes (crop to the aspect ratio)
26
+ # And store them in the storage backend.
27
+ options[:sizes] ||= []
28
+ images = options[:sizes].each do |size|
29
+ width = size[0]
30
+ height = size[1]
31
+
32
+ # Resize to maintain aspect ratio
33
+ resized = canvas.resize_to_fill(width, height)
34
+ self.storage_write "image_#{image.id}_#{width}x#{height}", resized.to_blob
35
+ end
36
+
37
+ image.url = "/images/#{image.id}"
38
+ image.uid = image.url
39
+
40
+ image.save
41
+ image
42
+ end
43
+
44
+ # Create a new Avatar from the given url
45
+ def self.from_url!(author, url, options = {})
46
+ # Pull canvas down
47
+ response = self.pull_url(url, options[:content_type])
48
+ return nil unless response.kind_of? Net::HTTPSuccess
49
+
50
+ self.from_blob!(author, response.body, options)
51
+ end
52
+
53
+ def image(size = nil)
54
+ return nil if self.sizes.empty?
55
+
56
+ size = self.sizes.first unless size
57
+ return nil unless self.sizes.include? size
58
+
59
+ Nelumba::Image.storage_read "image_#{self.id}_#{size[0]}x#{size[1]}"
60
+ end
61
+
62
+ def full_image
63
+ Nelumba::Image.storage_read "full_image_#{self.id}"
64
+ end
65
+
66
+ def image_base64(size = nil)
67
+ data = self.image(size)
68
+ return nil unless data
69
+
70
+ "data:#{self.content_type};base64,#{Base64.encode64(data)}"
71
+ end
72
+
73
+ def full_image_base64
74
+ data = self.full_image
75
+ return nil unless data
76
+
77
+ "data:#{self.content_type};base64,#{Base64.encode64(data)}"
78
+ end
79
+
80
+ private
81
+
82
+ # :nodoc:
83
+ def self.pull_url(url, content_type = nil, limit = 10)
84
+ uri = URI(url)
85
+ request = Net::HTTP::Get.new(uri.request_uri)
86
+ request.content_type = content_type if content_type
87
+
88
+ http = Net::HTTP.new(uri.hostname, uri.port)
89
+ if uri.scheme == 'https'
90
+ http.use_ssl = true
91
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
92
+ end
93
+
94
+ response = http.request(request)
95
+
96
+ if response.is_a?(Net::HTTPRedirection) && limit > 0
97
+ location = response['location']
98
+ self.pull_url(location, content_type, limit - 1)
99
+ else
100
+ response
101
+ end
102
+ end
103
+
104
+ # :nodoc:
105
+ def self.storage
106
+ @@grid ||= Mongo::Grid.new(MongoMapper.database)
107
+ end
108
+
109
+ # TODO: Add ability to read from filesystem
110
+ # :nodoc:
111
+ def self.storage_read(id)
112
+ self.storage.get(id).read
113
+ end
114
+
115
+ # TODO: Add ability to store on filesystem
116
+ # :nodoc:
117
+ def self.storage_write(id, data)
118
+ self.storage.put(data, :_id => id)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,5 @@
1
+ module Nelumba
2
+ class Note
3
+ include Nelumba::EmbeddedObject
4
+ end
5
+ end
@@ -0,0 +1,43 @@
1
+ module Nelumba
2
+ module Object
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+ include MongoMapper::Document
6
+
7
+ def initialize(*args, &blk)
8
+ init(*args, &blk)
9
+
10
+ super(*args, &blk)
11
+ end
12
+
13
+ # Ensure writes happen (lol mongo defaults)
14
+ safe
15
+
16
+ belongs_to :author, :class_name => 'Nelumba::Person'
17
+ key :author_id, ObjectId
18
+
19
+ key :title
20
+ key :uid
21
+ key :url
22
+ key :display_name
23
+ key :summary
24
+ key :content
25
+ key :image
26
+
27
+ key :text
28
+ key :html
29
+
30
+ # Automated Timestamps
31
+ key :published, Time
32
+ key :updated, Time
33
+ before_save :update_timestamps
34
+
35
+ def update_timestamps
36
+ now = Time.now.utc
37
+ self[:published] ||= now if !persisted?
38
+ self[:updated] = now
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,533 @@
1
+ module Nelumba
2
+ # Represents a typical social experience. This contains a feed of our
3
+ # contributions, our consumable feed (timeline), our list of favorites,
4
+ # a list of things that mention us and replies to us. It keeps track of
5
+ # our social presence with who follows us and who we follow.
6
+ class Person
7
+ def initialize(*args); super(*args); end
8
+
9
+ include MongoMapper::Document
10
+
11
+ # Ensure writes happen
12
+ safe
13
+
14
+ # Every Person has a representation of their central Identity.
15
+ one :identity, :class_name => 'Nelumba::Identity'
16
+
17
+ # A Person MAY have an Authorization, if they are local
18
+ one :authorization, :class_name => 'Nelumba::Authorization'
19
+
20
+ # Each Person has an Avatar icon that identifies them.
21
+ one :avatar, :class_name => 'Nelumba::Avatar'
22
+
23
+ # Our contributions.
24
+ key :activities_id, ObjectId
25
+ belongs_to :activities, :class_name => 'Nelumba::Feed'
26
+
27
+ # The combined contributions of ourself and others we follow.
28
+ key :timeline_id, ObjectId
29
+ belongs_to :timeline, :class_name => 'Nelumba::Feed'
30
+
31
+ # The things we like.
32
+ key :favorites_id, ObjectId
33
+ belongs_to :favorites, :class_name => 'Nelumba::Feed'
34
+
35
+ # The things we shared.
36
+ key :shared_id, ObjectId
37
+ belongs_to :shared, :class_name => 'Nelumba::Feed'
38
+
39
+ # Replies to our stuff.
40
+ key :replies_id, ObjectId
41
+ belongs_to :replies, :class_name => 'Nelumba::Feed'
42
+
43
+ # Stuff that mentions us.
44
+ key :mentions_id, ObjectId
45
+ belongs_to :mentions, :class_name => 'Nelumba::Feed'
46
+
47
+ # The people that follow us.
48
+ key :following_ids, Array
49
+ many :following, :in => :following_ids, :class_name => 'Nelumba::Person'
50
+
51
+ # Who is aggregating this feed.
52
+ key :followers_ids, Array
53
+ many :followers, :in => :followers_ids, :class_name => 'Nelumba::Person'
54
+
55
+ # A unique identifier for this author.
56
+ key :uid
57
+
58
+ # A nickname for this author.
59
+ key :nickname
60
+
61
+ # A Hash containing a representation of (typically) the Person's real name:
62
+ # :formatted => The full name of the contact
63
+ # :family_name => The family name. "Last name" in Western contexts.
64
+ # :given_name => The given name. "First name" in Western contexts.
65
+ # :middle_name => The middle name.
66
+ # :honorific_prefix => "Title" in Western contexts. (e.g. "Mr." "Mrs.")
67
+ # :honorific_suffix => "Suffix" in Western contexts. (e.g. "Esq.")
68
+ key :extended_name
69
+
70
+ # A URI that identifies this author and can be used to access a
71
+ # canonical representation of this structure.
72
+ key :url
73
+
74
+ # The email for this Person.
75
+ key :email
76
+
77
+ # The name for this Person.
78
+ key :name
79
+
80
+ # A Hash containing information about the organization this Person
81
+ # represents:
82
+ # :name => The name of the organization (e.g. company, school,
83
+ # etc) This field is required. Will be used for sorting.
84
+ # :department => The department within the organization.
85
+ # :title => The title or role within the organization.
86
+ # :type => The type of organization. Canonical values include
87
+ # "job" or "school"
88
+ # :start_date => A DateTime representing when the contact joined
89
+ # the organization.
90
+ # :end_date => A DateTime representing when the contact left the
91
+ # organization.
92
+ # :location => The physical location of this organization.
93
+ # :description => A free-text description of the role this contact
94
+ # played in this organization.
95
+ key :organization
96
+
97
+ # A Hash containing the location of this Person:
98
+ # :formatted => A formatted representating of the address. May
99
+ # contain newlines.
100
+ # :street_address => The full street address. May contain newlines.
101
+ # :locality => The city or locality component.
102
+ # :region => The state or region component.
103
+ # :postal_code => The zipcode or postal code component.
104
+ # :country => The country name component.
105
+ key :address
106
+
107
+ # A Hash containing the account information for this Person:
108
+ # :domain => The top-most authoriative domain for this account. (e.g.
109
+ # "twitter.com") This is the primary field. Is required.
110
+ # Used for sorting.
111
+ # :username => An alphanumeric username, typically chosen by the user.
112
+ # :userid => A user id, typically assigned, that uniquely refers to
113
+ # the user.
114
+ key :account
115
+
116
+ # The Person's gender.
117
+ key :gender
118
+
119
+ # The Person's requested pronouns.
120
+ #
121
+ # Contains at least one of the following:
122
+ # :plural => Whether or not the Person is considered plural
123
+ # :personal => The personal pronoun (xe)
124
+ # :possessive => The possessive pronoun (her)
125
+ key :pronoun
126
+
127
+ # A biographical note.
128
+ key :note
129
+
130
+ # The name the Person wishes to be used in display.
131
+ key :display_name
132
+
133
+ # The preferred username for the Person.
134
+ key :preferred_username
135
+
136
+ # A Date indicating the Person's birthday.
137
+ key :birthday
138
+
139
+ # A Date indicating an anniversary.
140
+ key :anniversary
141
+
142
+ # Automated Timestamps
143
+ key :published, Time
144
+ key :updated, Time
145
+ before_save :update_timestamps
146
+
147
+ def update_timestamps
148
+ now = Time.now.utc
149
+ self[:published] ||= now if !persisted?
150
+ self[:updated] = now
151
+ end
152
+
153
+ # Create a new local Person
154
+ def self.new_local(username, domain, port, ssl)
155
+ person = Nelumba::Person.new(:nickname => username,
156
+ :name => username,
157
+ :display_name => username,
158
+ :preferred_username => username)
159
+
160
+ # Create url and uid for local Person
161
+ person.url =
162
+ "http#{ssl ? "s" : ""}://#{domain}#{port ? ":#{port}" : ""}/people/#{person.id}"
163
+ person.uid = person.url
164
+
165
+ # Create feeds for local Person
166
+ person.activities = Nelumba::Feed.new(:person_id => person.id,
167
+ :authors => [person])
168
+ person.activities_id = person.activities.id
169
+
170
+ person.timeline = Nelumba::Feed.new(:person_id => person.id,
171
+ :authors => [person])
172
+ person.timeline_id = person.timeline.id
173
+
174
+ person.shared = Nelumba::Feed.new(:person_id => person.id,
175
+ :authors => [person])
176
+ person.shared_id = person.shared.id
177
+
178
+ person.favorites = Nelumba::Feed.new(:person_id => person.id,
179
+ :authors => [person])
180
+ person.favorites_id = person.favorites.id
181
+
182
+ person.replies = Nelumba::Feed.new(:person_id => person.id,
183
+ :authors => [person])
184
+ person.replies_id = person.replies.id
185
+
186
+ person.mentions = Nelumba::Feed.new(:person_id => person.id,
187
+ :authors => [person])
188
+ person.mentions_id = person.mentions.id
189
+
190
+ person
191
+ end
192
+
193
+ # Create a new Person if the given Person is not found by its id.
194
+ def self.find_or_create_by_uid!(arg, *args)
195
+ if arg.is_a? Nelumba::Person
196
+ uid = arg.uid
197
+
198
+ arg = arg.to_hash
199
+ else
200
+ uid = arg[:uid]
201
+ end
202
+
203
+ person = self.first(:uid => uid)
204
+ return person if person
205
+
206
+ begin
207
+ person = self.create!(arg, *args)
208
+ rescue
209
+ person = self.first(:uid => uid) or raise
210
+ end
211
+
212
+ person
213
+ end
214
+
215
+ # Updates so that we now follow the given Person.
216
+ def follow!(author)
217
+ if author.is_a? Nelumba::Identity
218
+ author = author.person
219
+ end
220
+
221
+ # add the author from our list of followers
222
+ self.following << author
223
+ self.save
224
+
225
+ # determine the feed to subscribe to
226
+ self.timeline.follow! author.identity.outbox
227
+
228
+ # tell local users that somebody on this server is following them.
229
+ if author.local?
230
+ author.followed_by! self
231
+ end
232
+
233
+ # Add the activity
234
+ self.activities.post!(:verb => :follow,
235
+ :actor_id => self.id,
236
+ :actor_type => 'Person',
237
+ :external_object_id => author.id,
238
+ :external_object_type => 'Person')
239
+ end
240
+
241
+ # Updates so that we do not follow the given Person.
242
+ def unfollow!(author)
243
+ if author.is_a? Nelumba::Identity
244
+ author = author.person
245
+ end
246
+
247
+ # remove the person from our list of followers
248
+ self.following_ids.delete(author.id)
249
+ self.save
250
+
251
+ # unfollow their timeline feed
252
+ self.timeline.unfollow! author.identity.outbox
253
+
254
+ # tell local users that somebody on this server has stopped following them.
255
+ if author.local?
256
+ author.unfollowed_by! self
257
+ end
258
+
259
+ # Add the activity
260
+ self.activities.post!(:verb => :"stop-following",
261
+ :actor_id => self.id,
262
+ :actor_type => 'Person',
263
+ :external_object_id => author.id,
264
+ :external_object_type => 'Person')
265
+ end
266
+
267
+ def follow?(author)
268
+ if author.is_a? Nelumba::Identity
269
+ author = author.person
270
+ end
271
+
272
+ self.following_ids.include? author.id
273
+ end
274
+
275
+ # Updates to show we are now followed by the given Person.
276
+ def followed_by!(author)
277
+ if author.is_a? Nelumba::Identity
278
+ author = author.person
279
+ end
280
+
281
+ return if author.nil?
282
+
283
+ # add them from our list
284
+ self.followers << author
285
+ self.save
286
+
287
+ # determine their feed
288
+ self.activities.followed_by! author.identity.inbox
289
+ end
290
+
291
+ # Updates to show we are not followed by the given Person.
292
+ def unfollowed_by!(author)
293
+ if author.is_a? Nelumba::Identity
294
+ author = author.person
295
+ end
296
+
297
+ return if author.nil?
298
+
299
+ # remove them from our list
300
+ self.followers_ids.delete(author.id)
301
+ self.save
302
+
303
+ # remove their feed as a syndicate of our activities
304
+ self.activities.unfollowed_by! author.identity.inbox
305
+ end
306
+
307
+ # Add the given Activity to our list of favorites.
308
+ def favorite!(activity)
309
+ self.favorites.repost! activity
310
+
311
+ self.activities.post!(:verb => :favorite,
312
+ :actor_id => self.id,
313
+ :actor_type => 'Person',
314
+ :external_object_id => activity.id,
315
+ :external_object_type => 'Activity')
316
+ end
317
+
318
+ # Remove the given Activity from our list of favorites.
319
+ def unfavorite!(activity)
320
+ self.favorites.delete! activity
321
+
322
+ self.activities.post!(:verb => :unfavorite,
323
+ :actor_id => self.id,
324
+ :actor_type => 'Person',
325
+ :external_object_id => activity.id,
326
+ :external_object_type => 'Activity')
327
+ end
328
+
329
+ # Add the given Activity to our list of those that mention us.
330
+ def mentioned_by!(activity)
331
+ self.mentions.repost! activity
332
+ end
333
+
334
+ # Add the given Activity to our list of those that are replies to our posts.
335
+ def replied_by!(activity)
336
+ self.replies.repost! activity
337
+ end
338
+
339
+ # Post a new Activity.
340
+ def post!(activity)
341
+ if activity.is_a? Hash
342
+ activity["actor_id"] = self.id
343
+ activity["actor_type"] = 'Person'
344
+
345
+ activity["verb"] = :post unless activity["verb"] || activity[:verb]
346
+
347
+ # Create a new activity
348
+ activity = Activity.create!(activity)
349
+ end
350
+
351
+ self.activities.post! activity
352
+ self.timeline.repost! activity
353
+
354
+ # Check mentions and replies
355
+ activity.mentions.each do |author|
356
+ author.identity.post! activity
357
+ end
358
+ end
359
+
360
+ # Repost an existing Activity.
361
+ def share!(activity)
362
+ self.timeline.repost! activity
363
+ self.shared.repost! activity
364
+
365
+ self.activities.post!(:verb => :share,
366
+ :actor_id => self.id,
367
+ :actor_type => 'Person',
368
+ :external_object_id => activity.id,
369
+ :external_object_type => 'Activity')
370
+ end
371
+
372
+ # Deliver an external Activity from somebody we follow.
373
+ #
374
+ # This goes in our timeline.
375
+ def deliver!(activity)
376
+ # Determine the original feed as duplicate it in our timeline
377
+ author = Nelumba::Person.find(:id => activity.author.id)
378
+
379
+ # Do not deliver if we do not follow the Person
380
+ return false if author.nil?
381
+ return false unless followings.include?(author)
382
+
383
+ # We should know how to talk back to this person
384
+ identity = Nelumba::Identity.find_by_author(author)
385
+ return false if identity.nil?
386
+
387
+ # Add to author's outbox feed
388
+ identity.outbox.post! activity
389
+
390
+ # Copy activity to timeline
391
+ if activity.type == :note
392
+ self.timeline.repost! activity
393
+ end
394
+ end
395
+
396
+ # Receive an external Activity from somebody we don't know.
397
+ #
398
+ # Generally, will be a mention or reply. Shouldn't go into timeline.
399
+ def receive!(activity)
400
+ end
401
+
402
+ # Deliver an activity from within the server
403
+ def local_deliver!(activity)
404
+ # If we follow, add to the timeline
405
+ self.timeline.repost! activity if self.follow?(activity.actor)
406
+
407
+ # Determine if it is a mention or reply and filter
408
+ self.mentions.repost! activity if activity.mentions? self
409
+ end
410
+
411
+ # Updates our avatar with the given url.
412
+ def update_avatar!(url)
413
+ Nelumba::Avatar.from_url!(self, url, :sizes => [[48, 48]])
414
+ end
415
+
416
+ def remote?
417
+ !self.local?
418
+ end
419
+
420
+ def local?
421
+ !self.authorization.nil?
422
+ end
423
+
424
+ def self.sanitize_params(params)
425
+ # Convert Symbols to Strings
426
+ params.keys.each do |k|
427
+ if k.is_a? Symbol
428
+ params[k.to_s] = params[k]
429
+ params.delete k
430
+ end
431
+ end
432
+
433
+ # Delete unknown subkeys
434
+ if params["extended_name"]
435
+ unless params["extended_name"].is_a? Hash
436
+ params.delete "extended_name"
437
+ else
438
+ params["extended_name"].keys.each do |k|
439
+ if ["formatted", "given_name", "family_name", "honorific_prefix",
440
+ "honorific_suffix", "middle_name"].include?(k.to_s)
441
+ params["extended_name"][(k.to_sym rescue k)] =
442
+ params["extended_name"].delete(k)
443
+ else
444
+ params["extended_name"].delete(k)
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ if params["organization"]
451
+ unless params["organization"].is_a? Hash
452
+ params.delete "organization"
453
+ else
454
+ params["organization"].keys.each do |k|
455
+ if ["name", "department", "title", "type", "start_date", "end_date",
456
+ "description"].include?(k.to_s)
457
+ params["organization"][(k.to_sym rescue k)] =
458
+ params["organization"].delete(k)
459
+ else
460
+ params["organization"].delete(k)
461
+ end
462
+ end
463
+ end
464
+ end
465
+
466
+ if params["address"]
467
+ unless params["address"].is_a? Hash
468
+ params.delete "address"
469
+ else
470
+ params["address"].keys.each do |k|
471
+ if ["formatted", "street_address", "locality", "region", "country",
472
+ "postal_code"].include?(k.to_s)
473
+ params["address"][(k.to_sym rescue k)] =
474
+ params["address"].delete(k)
475
+ else
476
+ params["address"].delete(k)
477
+ end
478
+ end
479
+ end
480
+ end
481
+
482
+ # Delete unknown keys
483
+ params.keys.each do |k|
484
+ unless self.keys.keys.include?(k)
485
+ params.delete(k)
486
+ end
487
+ end
488
+
489
+ # Delete immutable fields
490
+ params.delete("_id")
491
+
492
+ # Convert to symbols
493
+ params.keys.each do |k|
494
+ params[k.intern] = params[k]
495
+ params.delete k
496
+ end
497
+
498
+ params
499
+ end
500
+
501
+ # Discover and populate the associated activity feed for this author.
502
+ def discover_feed!
503
+ Nelumba::Discover.feed(self.identity)
504
+ end
505
+
506
+ # Discover an Person by the given feed location or account.
507
+ def self.discover!(author_identifier)
508
+ # Did we already discover this Person?
509
+ identity = Nelumba::Identity.find_by_identifier(author_identifier)
510
+ return identity.person if identity
511
+
512
+ # Discover the Identity
513
+ identity = Nelumba::Discover.identity(author_identifier)
514
+ return nil unless identity
515
+
516
+ # Use their Identity to discover their feed and their Person
517
+ feed = Nelumba::Discover.feed(identity)
518
+ return nil unless feed
519
+
520
+ feed.save
521
+
522
+ identity = identity.to_hash.merge(:outbox => feed,
523
+ :person_id => feed.authors.first.id)
524
+
525
+ identity = Nelumba::Identity.create!(identity)
526
+ identity.person
527
+ end
528
+
529
+ def to_xml(*args)
530
+ self.to_atom
531
+ end
532
+ end
533
+ end