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,30 @@
|
|
1
|
+
module Nelumba
|
2
|
+
class Article
|
3
|
+
require 'redcarpet'
|
4
|
+
|
5
|
+
include Nelumba::EmbeddedObject
|
6
|
+
|
7
|
+
key :markdown
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
super(options)
|
11
|
+
|
12
|
+
self.render_markdown unless options[:content]
|
13
|
+
end
|
14
|
+
|
15
|
+
def markdown=(value)
|
16
|
+
super value
|
17
|
+
render_markdown
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates content field with the markdown field.
|
21
|
+
def render_markdown
|
22
|
+
return if self.markdown.nil?
|
23
|
+
|
24
|
+
render_as = Redcarpet::Render::HTML
|
25
|
+
engine = Redcarpet::Markdown.new(render_as, :autolink => true,
|
26
|
+
:space_after_headers => true)
|
27
|
+
self.content = engine.render(self.markdown)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module Nelumba
|
2
|
+
# This represents how a Person can authenticate to act on our server.
|
3
|
+
# This is attached to an Identity and a Person. Use this to allow an
|
4
|
+
# Person to generate Activities on this server.
|
5
|
+
class Authorization
|
6
|
+
require 'bcrypt'
|
7
|
+
require 'json'
|
8
|
+
require 'nokogiri'
|
9
|
+
|
10
|
+
include MongoMapper::Document
|
11
|
+
|
12
|
+
# Ensure writes happen
|
13
|
+
safe
|
14
|
+
|
15
|
+
# An Authorization involves a Person.
|
16
|
+
key :person_id, ObjectId
|
17
|
+
belongs_to :person, :class_name => 'Nelumba::Person'
|
18
|
+
|
19
|
+
# Whether or not this authorization requires ssl
|
20
|
+
key :ssl
|
21
|
+
|
22
|
+
# The domain this authorization is registered for
|
23
|
+
key :domain
|
24
|
+
|
25
|
+
# The port authorization is tied to
|
26
|
+
key :port
|
27
|
+
|
28
|
+
# An Authorization involves an Identity.
|
29
|
+
key :identity_id, ObjectId
|
30
|
+
belongs_to :identity, :class_name => 'Nelumba::Identity'
|
31
|
+
|
32
|
+
# You authorize with a username
|
33
|
+
key :username, String
|
34
|
+
|
35
|
+
# A private key can verify that external information originated with this
|
36
|
+
# account.
|
37
|
+
key :private_key, String
|
38
|
+
|
39
|
+
# A password can authenticate you if you are manually signing in as a human
|
40
|
+
# being. The password is hashed to prevent information leaking.
|
41
|
+
key :hashed_password, String
|
42
|
+
|
43
|
+
# You must have enough credentials to be able to log into the system:
|
44
|
+
validates_presence_of :username
|
45
|
+
validates_presence_of :identity
|
46
|
+
validates_presence_of :hashed_password
|
47
|
+
|
48
|
+
# Log modification
|
49
|
+
timestamps!
|
50
|
+
|
51
|
+
# Generate a Hash containing this person's LRDD meta info.
|
52
|
+
def self.lrdd(username)
|
53
|
+
username = username.match /(?:acct\:)?([^@]+)(?:@([^@]+))?$/
|
54
|
+
username = username[1] if username
|
55
|
+
if username.nil?
|
56
|
+
return nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Find the person
|
60
|
+
auth = Authorization.find_by_username(/#{Regexp.escape(username)}/i)
|
61
|
+
return nil unless auth
|
62
|
+
|
63
|
+
domain = auth.identity.domain
|
64
|
+
port = auth.identity.port
|
65
|
+
url = "http#{auth.identity.ssl ? "s" : ""}://#{auth.identity.domain}#{port ? ":#{port}" : ""}"
|
66
|
+
feed_id = auth.identity.outbox.id
|
67
|
+
person_id = auth.person.id
|
68
|
+
|
69
|
+
{
|
70
|
+
:subject => "acct:#{username}@#{domain}#{port ? ":#{port}" : ""}",
|
71
|
+
:expires => "#{(Time.now.utc.to_date >> 1).xmlschema}Z",
|
72
|
+
:aliases => [
|
73
|
+
"#{url}#{auth.identity.profile_page}",
|
74
|
+
"#{url}/feeds/#{feed_id}"
|
75
|
+
],
|
76
|
+
:links => [
|
77
|
+
{:rel => "http://webfinger.net/rel/profile-page",
|
78
|
+
:href => "#{url}/people/#{person_id}"},
|
79
|
+
{:rel => "http://schemas.google.com/g/2010#updates-from",
|
80
|
+
:href => "#{url}/feeds/#{feed_id}"},
|
81
|
+
{:rel => "salmon",
|
82
|
+
:href => "#{url}/people/#{person_id}/salmon"},
|
83
|
+
{:rel => "http://salmon-protocol.org/ns/salmon-replies",
|
84
|
+
:href => "#{url}/people/#{person_id}/salmon"},
|
85
|
+
{:rel => "http://salmon-protocol.org/ns/salmon-mention",
|
86
|
+
:href => "#{url}/people/#{person_id}/salmon"},
|
87
|
+
{:rel => "magic-public-key",
|
88
|
+
:href => "data:application/magic-public-key,#{auth.identity.public_key}"}
|
89
|
+
|
90
|
+
# TODO: ostatus subscribe
|
91
|
+
#{:rel => "http://ostatus.org/schema/1.0/subscribe",
|
92
|
+
# :template => "#{url}/subscriptions?url={uri}&_method=post"}
|
93
|
+
]
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Generate a String containing the json representation of this person's LRDD.
|
98
|
+
def self.jrd(username)
|
99
|
+
lrdd = self.lrdd(username)
|
100
|
+
return nil if lrdd.nil?
|
101
|
+
|
102
|
+
lrdd.to_json
|
103
|
+
end
|
104
|
+
|
105
|
+
# Generate a String containing the XML representaton of this person's LRDD.
|
106
|
+
def self.xrd(username)
|
107
|
+
lrdd = self.lrdd(username)
|
108
|
+
return nil if lrdd.nil?
|
109
|
+
|
110
|
+
# Build xml
|
111
|
+
builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
|
112
|
+
xml.XRD("xmlns" => 'http://docs.oasis-open.org/ns/xri/xrd-1.0',
|
113
|
+
"xmlns:xsi" => 'http://www.w3.org/2001/XMLSchema-instance') do
|
114
|
+
xml.Subject lrdd[:subject]
|
115
|
+
xml.Expires lrdd[:expires]
|
116
|
+
|
117
|
+
lrdd[:aliases].each do |alias_name|
|
118
|
+
xml.Alias alias_name
|
119
|
+
end
|
120
|
+
|
121
|
+
lrdd[:links].each do |link|
|
122
|
+
xml.Link link
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Output
|
128
|
+
builder.to_xml
|
129
|
+
end
|
130
|
+
|
131
|
+
# Create a hash of the password.
|
132
|
+
def self.hash_password(password)
|
133
|
+
BCrypt::Password.create(password, :cost => Nelumba::BCRYPT_ROUNDS)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Determine if the given password matches the account.
|
137
|
+
def authenticated?(password)
|
138
|
+
BCrypt::Password.new(hashed_password) == password
|
139
|
+
end
|
140
|
+
|
141
|
+
# :nodoc: Do not allow the password to be set at any cost.
|
142
|
+
def password=(value)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Cleanup any unexpected keys.
|
146
|
+
def self.sanitize_params(params)
|
147
|
+
# Convert Symbols to Strings
|
148
|
+
params.keys.each do |k|
|
149
|
+
if k.is_a? Symbol
|
150
|
+
params[k.to_s] = params[k]
|
151
|
+
params.delete k
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Delete unknown keys
|
156
|
+
params.keys.each do |k|
|
157
|
+
unless self.keys.keys.map.include?(k)
|
158
|
+
params.delete(k)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Delete immutable fields
|
163
|
+
params.delete("id")
|
164
|
+
params.delete("_id")
|
165
|
+
|
166
|
+
# Convert to symbols
|
167
|
+
params.keys.each do |k|
|
168
|
+
params[k.intern] = params[k]
|
169
|
+
params.delete k
|
170
|
+
end
|
171
|
+
|
172
|
+
params
|
173
|
+
end
|
174
|
+
|
175
|
+
# Create a new Authorization.
|
176
|
+
def initialize(*args)
|
177
|
+
params = {}
|
178
|
+
params = args.shift if args.length > 0
|
179
|
+
|
180
|
+
params["password"] = params[:password] if params[:password]
|
181
|
+
params.delete :password
|
182
|
+
|
183
|
+
params["hashed_password"] = Authorization.hash_password(params["password"])
|
184
|
+
params.delete "password"
|
185
|
+
|
186
|
+
params = Authorization.sanitize_params(params)
|
187
|
+
|
188
|
+
person = Nelumba::Person.new_local(params[:username],
|
189
|
+
params[:domain],
|
190
|
+
params[:port],
|
191
|
+
params[:ssl])
|
192
|
+
person.save
|
193
|
+
params[:person_id] = person.id
|
194
|
+
|
195
|
+
keypair = Nelumba::Crypto.new_keypair
|
196
|
+
params[:private_key] = keypair.private_key
|
197
|
+
|
198
|
+
identity = Nelumba::Identity.new_local(person,
|
199
|
+
params[:username],
|
200
|
+
params[:domain],
|
201
|
+
params[:port],
|
202
|
+
params[:ssl],
|
203
|
+
keypair.public_key)
|
204
|
+
identity.save
|
205
|
+
params[:identity_id] = identity.id
|
206
|
+
|
207
|
+
params[:person] = person
|
208
|
+
params[:identity] = identity
|
209
|
+
|
210
|
+
super params, *args
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Nelumba
|
2
|
+
class Avatar
|
3
|
+
require 'RMagick'
|
4
|
+
|
5
|
+
include MongoMapper::Document
|
6
|
+
|
7
|
+
# The avatar belongs to a particular Person
|
8
|
+
key :author_id, ObjectId
|
9
|
+
belongs_to :author, :class_name => 'Nelumba::Person'
|
10
|
+
|
11
|
+
# The array of sizes this avatar has stored.
|
12
|
+
key :sizes, Array, :default => []
|
13
|
+
|
14
|
+
# The content type for the image.
|
15
|
+
key :content_type
|
16
|
+
|
17
|
+
# Log modification.
|
18
|
+
timestamps!
|
19
|
+
|
20
|
+
# Create a new Avatar from the given blob
|
21
|
+
def self.from_blob!(author, blob, options = {})
|
22
|
+
avatar = Avatar.new(:author_id => author.id,
|
23
|
+
:sizes => options[:sizes])
|
24
|
+
|
25
|
+
image = Magick::ImageList.new
|
26
|
+
image.from_blob(blob)
|
27
|
+
|
28
|
+
# Store the content_type
|
29
|
+
avatar.content_type = image.mime_type
|
30
|
+
|
31
|
+
# Resize the images to fit the given sizes (crop to the aspect ratio)
|
32
|
+
# And store them in the storage backend.
|
33
|
+
options[:sizes] ||= []
|
34
|
+
images = options[:sizes].each do |size|
|
35
|
+
width = size[0]
|
36
|
+
height = size[1]
|
37
|
+
|
38
|
+
# Resize to maintain aspect ratio
|
39
|
+
resized = image.resize_to_fill(width, height)
|
40
|
+
self.storage_write "avatar_#{avatar.id}_#{width}x#{height}", resized.to_blob
|
41
|
+
end
|
42
|
+
|
43
|
+
# Find old avatar
|
44
|
+
old = Avatar.first(:author_id => author.id)
|
45
|
+
if old
|
46
|
+
old.destroy
|
47
|
+
end
|
48
|
+
|
49
|
+
avatar.save
|
50
|
+
avatar
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a new Avatar from the given url
|
54
|
+
def self.from_url!(author, url, options = {})
|
55
|
+
# Pull image down
|
56
|
+
response = self.pull_url(url, options[:content_type])
|
57
|
+
return nil unless response.kind_of? Net::HTTPSuccess
|
58
|
+
|
59
|
+
self.from_blob!(author, response.body, options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def url(size = nil)
|
63
|
+
return nil if self.sizes.empty?
|
64
|
+
|
65
|
+
size = self.sizes.first unless size
|
66
|
+
return nil unless self.sizes.include? size
|
67
|
+
|
68
|
+
"/avatars/#{self.id}/#{size[0]}x#{size[1]}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Retrieve the avatar image as a byte string.
|
72
|
+
def read(size = nil)
|
73
|
+
return nil if self.sizes.empty?
|
74
|
+
|
75
|
+
size = self.sizes.first unless size
|
76
|
+
return nil unless self.sizes.include? size
|
77
|
+
|
78
|
+
Avatar.storage_read "avatar_#{self.id}_#{size[0]}x#{size[1]}"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Yield a base64 string encoded with the content type
|
82
|
+
def read_base64(size = nil)
|
83
|
+
data = self.read(size)
|
84
|
+
return nil unless data
|
85
|
+
|
86
|
+
"data:#{self.content_type};base64,#{Base64.encode64(data)}"
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# :nodoc:
|
92
|
+
def self.pull_url(url, content_type = nil, limit = 10)
|
93
|
+
uri = URI(url)
|
94
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
95
|
+
request.content_type = content_type if content_type
|
96
|
+
|
97
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
98
|
+
if uri.scheme == 'https'
|
99
|
+
http.use_ssl = true
|
100
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
101
|
+
end
|
102
|
+
|
103
|
+
response = http.request(request)
|
104
|
+
|
105
|
+
if response.is_a?(Net::HTTPRedirection) && limit > 0
|
106
|
+
location = response['location']
|
107
|
+
self.pull_url(location, content_type, limit - 1)
|
108
|
+
else
|
109
|
+
response
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# :nodoc:
|
114
|
+
def self.storage
|
115
|
+
@@grid ||= Mongo::Grid.new(MongoMapper.database)
|
116
|
+
end
|
117
|
+
|
118
|
+
# TODO: Add ability to read from filesystem
|
119
|
+
# :nodoc:
|
120
|
+
def self.storage_read(id)
|
121
|
+
self.storage.get(id).read
|
122
|
+
end
|
123
|
+
|
124
|
+
# TODO: Add ability to store on filesystem
|
125
|
+
# :nodoc:
|
126
|
+
def self.storage_write(id, data)
|
127
|
+
self.storage.put(data, :_id => id)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Nelumba
|
2
|
+
module EmbeddedObject
|
3
|
+
def self.included(klass)
|
4
|
+
klass.class_eval do
|
5
|
+
include MongoMapper::EmbeddedDocument
|
6
|
+
|
7
|
+
def initialize(*args, &blk)
|
8
|
+
init(*args, &blk)
|
9
|
+
|
10
|
+
super(*args, &blk)
|
11
|
+
end
|
12
|
+
|
13
|
+
belongs_to :author, :class_name => 'Nelumba::Person'
|
14
|
+
key :author_id, ObjectId
|
15
|
+
|
16
|
+
key :title
|
17
|
+
key :uid
|
18
|
+
key :url
|
19
|
+
key :display_name
|
20
|
+
key :summary
|
21
|
+
key :content
|
22
|
+
key :image
|
23
|
+
|
24
|
+
# Automated Timestamps
|
25
|
+
key :published, Time
|
26
|
+
key :updated, Time
|
27
|
+
before_save :update_timestamps
|
28
|
+
|
29
|
+
def update_timestamps
|
30
|
+
now = Time.now.utc
|
31
|
+
self[:published] ||= now if !persisted?
|
32
|
+
self[:updated] = now
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.find_by_id(id)
|
36
|
+
Nelumba::Activity.object_by_id_and_type(id, self)
|
37
|
+
end
|
38
|
+
|
39
|
+
embedded_in :'Nelumba::Activity'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
module Nelumba
|
2
|
+
class Feed
|
3
|
+
def initialize(*args); super(*args); end
|
4
|
+
|
5
|
+
include MongoMapper::Document
|
6
|
+
|
7
|
+
# Ensure writes happen
|
8
|
+
safe
|
9
|
+
|
10
|
+
# A unique identifier for this Feed.
|
11
|
+
key :uid
|
12
|
+
|
13
|
+
# A URL for this Feed that can be used to retrieve a representation.
|
14
|
+
key :url
|
15
|
+
|
16
|
+
# Feeds generally belong to a person.
|
17
|
+
key :person_id, ObjectId
|
18
|
+
belongs_to :person, :class_name => 'Nelumba::Person'
|
19
|
+
|
20
|
+
remove_method :categories
|
21
|
+
key :categories, :default => []
|
22
|
+
|
23
|
+
# The type of rights one has to this feed generally for human display.
|
24
|
+
key :rights
|
25
|
+
|
26
|
+
# The title of this feed.
|
27
|
+
key :title
|
28
|
+
|
29
|
+
# The representation of the title. (e.g. "html")
|
30
|
+
key :title_type
|
31
|
+
|
32
|
+
# The subtitle of the feed.
|
33
|
+
key :subtitle
|
34
|
+
|
35
|
+
# The representation of the subtitle. (e.g. "html")
|
36
|
+
key :subtitle_type
|
37
|
+
|
38
|
+
# An array of Persons that contributed to this Feed.
|
39
|
+
key :contributors_ids, Array, :default => []
|
40
|
+
remove_method :contributors
|
41
|
+
many :contributors, :class_name => 'Nelumba::Person', :in => :contributors_ids
|
42
|
+
|
43
|
+
# An Array of Persons that create the content in this Feed.
|
44
|
+
key :authors_ids, Array, :default => []
|
45
|
+
remove_method :authors
|
46
|
+
many :authors, :class_name => 'Nelumba::Person', :in => :authors_ids
|
47
|
+
|
48
|
+
# An Array of Activities that are contained in this Feed.
|
49
|
+
key :items_ids, Array
|
50
|
+
many :items, :class_name => 'Nelumba::Activity',
|
51
|
+
:in => :items_ids,
|
52
|
+
:order => :published.desc
|
53
|
+
|
54
|
+
# A Hash containing information about the entity that is generating content
|
55
|
+
# for this Feed when it isn't a person.
|
56
|
+
key :generator
|
57
|
+
|
58
|
+
# Feeds may have an icon to represent them.
|
59
|
+
#key :icon, :class_name => 'Image'
|
60
|
+
|
61
|
+
# Feeds may have an image they use as a logo.
|
62
|
+
#key :logo, :class_name => 'Image'
|
63
|
+
|
64
|
+
# TODO: Normalize the first 100 or so activities. I dunno.
|
65
|
+
key :normalized
|
66
|
+
|
67
|
+
# Automated Timestamps
|
68
|
+
key :published, Time
|
69
|
+
key :updated, Time
|
70
|
+
before_save :update_timestamps
|
71
|
+
|
72
|
+
def update_timestamps
|
73
|
+
now = Time.now.utc
|
74
|
+
self[:published] ||= now if !persisted?
|
75
|
+
self[:updated] = now
|
76
|
+
end
|
77
|
+
|
78
|
+
# The external feeds being aggregated.
|
79
|
+
key :following_ids, Array
|
80
|
+
many :following, :in => :following_ids, :class_name => 'Nelumba::Feed'
|
81
|
+
|
82
|
+
# Who is aggregating this feed.
|
83
|
+
key :followers_ids, Array
|
84
|
+
many :followers, :in => :followers_ids, :class_name => 'Nelumba::Feed'
|
85
|
+
|
86
|
+
# Subscription status.
|
87
|
+
# Since subscriptions are done by the server, we only need to share one
|
88
|
+
# secret/token pair for all users that follow this feed on the server.
|
89
|
+
# This is done at the Feed level since people may want to follow your
|
90
|
+
# "timeline", or your "favorites". Or People who use Nelumba will ignore
|
91
|
+
# the Person aggregate class and go with their own thing.
|
92
|
+
key :subscription_secret
|
93
|
+
key :verification_token
|
94
|
+
|
95
|
+
# Create a new Feed if the given Feed is not found by its id.
|
96
|
+
def self.find_or_create_by_uid!(arg, *args)
|
97
|
+
if arg.is_a? Nelumba::Feed
|
98
|
+
uid = arg.uid
|
99
|
+
else
|
100
|
+
uid = arg[:uid]
|
101
|
+
end
|
102
|
+
|
103
|
+
feed = self.first(:uid => uid)
|
104
|
+
return feed if feed
|
105
|
+
|
106
|
+
begin
|
107
|
+
feed = create!(arg, *args)
|
108
|
+
rescue
|
109
|
+
feed = self.first(:uid => uid) or raise
|
110
|
+
end
|
111
|
+
|
112
|
+
feed
|
113
|
+
end
|
114
|
+
|
115
|
+
# Create a new Feed from a Hash of values or a Nelumba::Feed.
|
116
|
+
def initialize(*args)
|
117
|
+
hash = {}
|
118
|
+
if args.length > 0
|
119
|
+
hash = args.shift
|
120
|
+
end
|
121
|
+
|
122
|
+
if hash.is_a? Nelumba::Feed
|
123
|
+
hash = hash.to_hash
|
124
|
+
end
|
125
|
+
|
126
|
+
if hash[:authors].is_a? Array
|
127
|
+
hash[:authors].map! do |author|
|
128
|
+
if author.is_a? Hash
|
129
|
+
author = Nelumba::Person.find_or_create_by_uid!(author)
|
130
|
+
end
|
131
|
+
author
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
if hash[:contributors].is_a? Array
|
136
|
+
hash[:contributors].map! do |contributor|
|
137
|
+
if contributor.is_a? Hash
|
138
|
+
contributor = Nelumba::Person.find_or_create_by_uid!(contributor)
|
139
|
+
end
|
140
|
+
contributor
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
if hash[:items].is_a? Array
|
145
|
+
hash[:items].map! do |item|
|
146
|
+
if item.is_a? Hash
|
147
|
+
item = Nelumba::Activity.find_or_create_by_uid!(item)
|
148
|
+
end
|
149
|
+
item
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
super hash, *args
|
154
|
+
end
|
155
|
+
|
156
|
+
# Discover a feed by the given feed location or account.
|
157
|
+
def self.discover!(feed_identifier)
|
158
|
+
feed = Feed.first(:url => feed_identifier)
|
159
|
+
return feed if feed
|
160
|
+
|
161
|
+
feed = Nelumba::Discover.feed(feed_identifier)
|
162
|
+
return false unless feed
|
163
|
+
|
164
|
+
existing_feed = Feed.first(:uid => feed.uid)
|
165
|
+
return existing_feed if existing_feed
|
166
|
+
|
167
|
+
self.create!(feed)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Merges the information in the given feed with this one.
|
171
|
+
def merge!(feed)
|
172
|
+
# Merge metadata
|
173
|
+
meta_data = feed.to_hash
|
174
|
+
meta_data.delete :items
|
175
|
+
meta_data.delete :authors
|
176
|
+
meta_data.delete :contributors
|
177
|
+
meta_data.delete :uid
|
178
|
+
self.update_attributes!(meta_data)
|
179
|
+
|
180
|
+
# Merge new/updated authors
|
181
|
+
feed.authors.each do |author|
|
182
|
+
end
|
183
|
+
|
184
|
+
# Merge new/updated contributors
|
185
|
+
feed.contributors.each do |author|
|
186
|
+
end
|
187
|
+
|
188
|
+
# Merge new/updated activities
|
189
|
+
feed.items.each do |activity|
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Retrieve the feed's activities with the most recent first.
|
194
|
+
def ordered
|
195
|
+
Nelumba::Activity.where(:id => self.items_ids).order(:published => :desc)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Follow the given feed. When a new post is placed in this feed, it
|
199
|
+
# will be copied into ours.
|
200
|
+
def follow!(feed)
|
201
|
+
self.following << feed
|
202
|
+
self.save
|
203
|
+
|
204
|
+
# Subscribe to that feed on this server if not already.
|
205
|
+
end
|
206
|
+
|
207
|
+
# Unfollow the given feed. Our feed will no longer receive new posts from
|
208
|
+
# the given feed.
|
209
|
+
def unfollow!(feed)
|
210
|
+
self.following_ids.delete(feed.id)
|
211
|
+
self.save
|
212
|
+
end
|
213
|
+
|
214
|
+
# Denotes that the given feed will contain your posts.
|
215
|
+
def followed_by!(feed)
|
216
|
+
self.followers << feed
|
217
|
+
self.save
|
218
|
+
end
|
219
|
+
|
220
|
+
# Denotes that the given feed will not contain your posts from now on.
|
221
|
+
def unfollowed_by!(feed)
|
222
|
+
self.followers_ids.delete(feed.id)
|
223
|
+
self.save
|
224
|
+
end
|
225
|
+
|
226
|
+
# Add to the feed and tell subscribers.
|
227
|
+
def post!(activity)
|
228
|
+
if activity.is_a?(Hash)
|
229
|
+
# Create a new activity
|
230
|
+
activity = Nelumba::Activity.create!(activity)
|
231
|
+
end
|
232
|
+
|
233
|
+
activity.feed_id = self.id
|
234
|
+
activity.save
|
235
|
+
|
236
|
+
self.items << activity
|
237
|
+
self.save
|
238
|
+
|
239
|
+
publish(activity)
|
240
|
+
|
241
|
+
activity
|
242
|
+
end
|
243
|
+
|
244
|
+
# Remove the activity from the feed.
|
245
|
+
def delete!(activity)
|
246
|
+
self.items_ids.delete(activity.id)
|
247
|
+
self.save
|
248
|
+
end
|
249
|
+
|
250
|
+
# Add a copy to our feed and tell subscribers.
|
251
|
+
def repost!(activity)
|
252
|
+
self.items << activity
|
253
|
+
self.save
|
254
|
+
|
255
|
+
publish(activity)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Publish an activity that is within our feed.
|
259
|
+
def publish(activity)
|
260
|
+
# Push to direct followers
|
261
|
+
self.followers.each do |feed|
|
262
|
+
feed.repost! activity
|
263
|
+
end
|
264
|
+
|
265
|
+
# TODO: PuSH Hubs
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|