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