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.
- data/.gitignore +17 -0
- data/.travis.yml +10 -0
- data/Gemfile +25 -0
- data/LICENSE +127 -0
- data/README.md +29 -0
- data/Rakefile +60 -0
- data/lib/nelumba-mongodb.rb +35 -0
- data/lib/nelumba-mongodb/activity.rb +316 -0
- data/lib/nelumba-mongodb/article.rb +30 -0
- data/lib/nelumba-mongodb/authorization.rb +213 -0
- data/lib/nelumba-mongodb/avatar.rb +130 -0
- data/lib/nelumba-mongodb/comment.rb +7 -0
- data/lib/nelumba-mongodb/embedded_object.rb +43 -0
- data/lib/nelumba-mongodb/feed.rb +268 -0
- data/lib/nelumba-mongodb/identity.rb +191 -0
- data/lib/nelumba-mongodb/image.rb +121 -0
- data/lib/nelumba-mongodb/note.rb +5 -0
- data/lib/nelumba-mongodb/object.rb +43 -0
- data/lib/nelumba-mongodb/person.rb +533 -0
- data/lib/nelumba-mongodb/version.rb +3 -0
- data/nelumba-mongodb.gemspec +26 -0
- data/spec/activity_spec.rb +337 -0
- data/spec/article_spec.rb +71 -0
- data/spec/authorization_spec.rb +514 -0
- data/spec/avatar_spec.rb +418 -0
- data/spec/feed_spec.rb +485 -0
- data/spec/helper.rb +72 -0
- data/spec/identity_spec.rb +245 -0
- data/spec/note_spec.rb +71 -0
- data/spec/person_spec.rb +922 -0
- metadata +164 -0
@@ -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,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
|