user.rb |
|
---|---|
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'
require 'bcrypt'
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, :dependent => :destroy
belongs_to :author
key :author_id, ObjectId |
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 |
Tokens are valid for 2 days, they’re checked against this |
key :perishable_token_set, DateTime, :default => nil
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 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 and the date it was set to nil |
def reset_perishable_token
self.perishable_token = nil
self.perishable_token_set = 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 |
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 password |
key :hashed_password, String |
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 token was sent so tokens can be expired after 2 days. This is used for password resets and email confirmations |
def create_token
self.perishable_token_set = 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
reset_perishable_token
end |
Authenticate the user by checking their credentials |
def self.authenticate(username, pass)
user = User.find_by_case_insensitive_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
def token_expired?
self.perishable_token_set.to_time < 2.days.ago
end
def to_param
username
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 |