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 |