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