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