-
Notifications
You must be signed in to change notification settings - Fork 215
/
user.rb
365 lines (289 loc) · 9.71 KB
/
user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# The User model contains all of the information that a particular user of our
# site needs: their username/password, etc. It all comes from here. Even users
# that sign up via Twitter get a User model, though it's a bit empty in that
# particular case.
require 'crypto'
class User
require 'digest/md5'
include MongoMapper::Document
# Associations
# XXX: These don't seem to be getting set when you sign up with Twitter, etc?
many :authorizations, :dependant => :destroy
belongs_to :author
# Users MUST have a username
key :username, String, :required => true
# Users MIGHT have an email
key :email, String
key :email_confirmed, Boolean
# RSA for salmon usage
key :private_key, String
# Required for confirmation
key :perishable_token, String
validate :email_already_confirmed
validates_uniqueness_of :username, :allow_nil => :true, :case_sensitive => false
# The maximum is arbitrary
# Twitter has 15, let's be different
validates_length_of :username, :maximum => 17, :message => "must be 17 characters or fewer."
# Validate users don't have special characters in their username
validate :no_malformed_username
# This will establish other entities related to the User
after_create :finalize
def feed
self.author.feed
end
def updates
self.author.feed.updates.sort(:created_at.desc)
end
# Before a user is created, we will generate some RSA keys
def generate_rsa_pair
keypair = Crypto.generate_keypair
self.author.public_key = keypair.public_key
self.author.save
self.private_key = keypair.private_key
end
# Retrieves a valid RSA::KeyPair for the User's private key
def self.to_rsa_keypair
Crypto.make_rsa_keypair(nil, private_key)
end
# After a user is created, create the feed and reset the token
def finalize
create_feed
generate_rsa_pair
reset_perishable_token
end
# Generate a multi-use token for account confirmation and password resets
def set_perishable_token
self.perishable_token = Digest::MD5.hexdigest( rand.to_s )
save
end
# Reset the perishable token
def reset_perishable_token
self.perishable_token = nil
save
end
# Determines a url that leads to the profile of this user
def url
"/users/#{feed.author.username}"
end
# Returns true when this user has a twitter authorization
def twitter?
has_authorization?(:twitter)
end
# Returns the twitter authorization
def twitter
get_authorization(:twitter)
end
# Returns true when this user has a facebook authorization
def facebook?
has_authorization?(:facebook)
end
# Returns the facebook authorization
def facebook
get_authorization(:facebook)
end
# Check if a a user has a certain authorization by providing the associated
# provider
def has_authorization?(auth)
a = Authorization.first(:provider => auth.to_s, :user_id => self.id)
#return false if not authenticated and true otherwise.
!a.nil?
end
# Get an authorization by providing the assoaciated provider
def get_authorization(auth)
Authorization.first(:provider => auth.to_s, :user_id => self.id)
end
# Users follow many feeds
key :following_ids, Array
many :following, :in => :following_ids, :class_name => 'Feed'
# Users have feeds that follow them
key :followers_ids, Array
many :followers, :in => :followers_ids, :class_name => 'Feed'
# A particular feed follows this user
def followed_by! f
followers << f
save
end
# A particular feed unfollows this user
def unfollowed_by! f
followers_ids.delete(f.id)
save
end
# Follow a particular feed
def follow! f
# can't follow yourself
if f == self.feed
return
end
following << f
save
if f.local?
# Add the inverse relationship
followee = User.first(:author_id => f.author.id)
followee.followed_by! self.feed
else
# Queue a notification job
self.delay.send_follow_notification(f.id)
end
f
end
# Send Salmon notification so that the remote user
# knows this user is following them
def send_follow_notification to_feed_id
f = Feed.first :id => to_feed_id
salmon = OStatus::Salmon.from_follow(author.to_atom, f.author.to_atom)
envelope = salmon.to_xml self.to_rsa_keypair
# Send envelope to Author's Salmon endpoint
uri = URI.parse(f.author.salmon_url)
http = Net::HTTP.new(uri.host, uri.port)
res = http.post(uri.path, envelope, {"Content-Type" => "application/magic-envelope+xml"})
end
# unfollow takes a feed (since it is guaranteed to exist)
def unfollow! followed_feed
following_ids.delete(followed_feed.id)
save
if followed_feed.local?
followee = User.first(:author_id => followed_feed.author.id)
followee.unfollowed_by!(self.feed)
else
# Queue a notification job
self.delay.send_unfollow_notification(followed_feed.id)
end
end
# Send Salmon notification so that the remote user
# knows this user has stopped following them
def send_unfollow_notification to_feed_id
f = Feed.first :id => to_feed_id
salmon = OStatus::Salmon.from_unfollow(author.to_atom, f.author.to_atom)
envelope = salmon.to_xml self.to_rsa_keypair
# Send envelope to Author's Salmon endpoint
uri = URI.parse(f.author.salmon_url)
http = Net::HTTP.new(uri.host, uri.port)
res = http.post(uri.path, envelope, {"Content-Type" => "application/magic-envelope+xml"})
end
# Send an update to a remote user as a Salmon notification
def send_mention_notification update_id, to_feed_id
f = Feed.first :id => to_feed_id
u = Update.first :id => update_id
base_uri = "http://#{author.domain}/"
salmon = OStatus::Salmon.new(u.to_atom(base_uri))
envelope = salmon.to_xml self.to_rsa_keypair
# Send envelope to Author's Salmon endpoint
uri = URI.parse(f.author.salmon_url)
http = Net::HTTP.new(uri.host, uri.port)
res = http.post(uri.path, envelope, {"Content-Type" => "application/magic-envelope+xml"})
end
def followed_by? f
followers.include? f
end
def following_feed? f
following.include? f
end
def following_author? author
following.include?(author.feed)
end
def following_url? feed_url
# Handle possibly created multiple feeds for the same remote_url
existing_feeds = Feed.all(:remote_url => feed_url)
# local feed?
if existing_feeds.empty? and feed_url.start_with?("http://#{author.domain}/")
feed_id = feed_url[/^\/feeds\/(.+)$/,1]
existing_feeds = [Feed.first(:id => feed_id)]
end
if existing_feeds.empty?
false
else
# Intersect the feeds we're following and the possibly
# created multiple feeds for the remote
!(following & existing_feeds).empty?
end
end
timestamps!
# Retrieve the list of Updates in the user's timeline
def timeline(params = nil)
following_plus_me = following.map(&:author_id)
following_plus_me << self.author.id
Update.where(:author_id => following_plus_me).order(['created_at', 'descending'])
end
# Retrieve the list of Updates that are replies to this user
def at_replies(params)
Update.where(:text => /^@#{Regexp.quote(username)}\b/).order(['created_at', 'descending'])
end
# User MUST be confirmed
key :status
# Users have a passwork
key :hashed_password, String
key :password_reset_sent, DateTime, :default => nil
# Store the hash of the password
def password=(pass)
self.hashed_password = BCrypt::Password.create(pass, :cost => 10)
end
# Create a new perishable token and set the date the password reset token was
# sent so tokens can be expired after 2 days
def set_password_reset_token
self.password_reset_sent = DateTime.now
set_perishable_token
self.perishable_token
end
# Set a new password, clear the date the password reset token was sent and
# reset the perishable token
def reset_password(pass)
self.password = pass
self.password_reset_sent = nil
reset_perishable_token
end
# Authenticate the user by checking their credentials
def self.authenticate(username, pass)
user = User.first(:username => username)
return nil if user.nil?
return user if BCrypt::Password.new(user.hashed_password) == pass
nil
end
# Edit profile information
def edit_user_profile(params)
unless params[:password].nil? or params[:password].empty?
if params[:password] == params[:password_confirm]
self.password = params[:password]
self.save
else
return "Passwords must match"
end
end
self.email_confirmed = self.email == params[:email]
self.email = params[:email]
self.save
author.name = params[:name]
author.email = params[:email]
author.website = params[:website]
author.bio = params[:bio]
author.save
# TODO: Send out notice to other nodes
# To each remote domain that is following you via hub
# and to each remote domain that you follow via salmon
author.feed.ping_hubs
return true
end
# A better name would be very welcome.
def self.find_by_case_insensitive_username(username)
User.first(:username => /^#{Regexp.escape(username)}$/i)
end
private
def create_feed
f = Feed.create(
:author => self.author
)
self.author.save
save
end
def no_malformed_username
unless (username =~ /[@!"#$\%&'()*,^~{}|`=:;\\\/\[\]\s?]/).nil? && (username =~ /^[.]/).nil? && (username =~ /[.]$/).nil? && (username =~ /[.]{2,}/).nil?
errors.add(:username, "contains restricted characters. Try sticking to letters, numbers, hyphens and underscores.")
end
end
def email_already_confirmed
if User.where(:email => self.email,
:email_confirmed => true,
:username.ne => self.username).count > 0
errors.add(:email, "is already taken.")
end
end
end