Jump To …

update.rb

An Update is a particular status message sent by one of our users.

class Update
  require 'cgi'
  include MongoMapper::Document

Determines what constitutes a username inside an update text

  USERNAME_REGULAR_EXPRESSION = /(^|[ \t\n\r\f"'\(\[{]+)@([^ \t\n\r\f&?=@%\/\#]*[^ \t\n\r\f&?=@%\/\#.!:;,"'\]}\)])(?:@([^ \t\n\r\f&?=@%\/\#]*[^ \t\n\r\f&?=@%\/\#.!:;,"'\]}\)]))?/

Updates are aggregated in Feeds

  belongs_to :feed
  key :feed_id, ObjectId

Updates are written by Authors

  belongs_to :author
  key :author_id, ObjectId
  validates_presence_of :author_id

The content of the update, unaltered, is stored here

  key :text, String, :default => ""
  validates_length_of :text, :minimum => 1, :maximum => 140
  validate :do_not_repeat_yourself, :on => :create

Mentions are stored in the following array

  key :mention_ids, Array
  many :mentions, :in => :mention_ids, :class_name => 'Author'
  before_save :get_mentions

The following are extra features and identifications for the update

  key :tags, Array, :default => []
  key :twitter, Boolean

For speed, we generate the html for the update lazily when it is rendered

  key :html, String

We also generate the tags upon editing the update

  before_save :get_tags

Updates have a remote url that globally identifies them

  key :remote_url, String

Reply and restate identifiers Local Update id: (nil if remote)

  key :referral_id

Remote Update url: (nil if local)

  key :referral_url, String

  def referral
    Update.first(:id => referral_id)
  end

  def url
    feed.local? ? "/updates/#{id}" : remote_url
  end

  def url=(the_url)
    self.remote_url = the_url
  end

  def to_html
    self.html || generate_html
  end

  def mentioned?(username)
    matches = text.match(/@#{username}\b/)
    matches.nil? ? false : matches.length > 0
  end

These handle sending the update to other nodes and services

  after_create :send_to_remote_mentions
  after_create :send_to_external_accounts

  timestamps!

  def self.hot_updates
    all(:limit => 6, :order => 'created_at desc')
  end

  def get_tags
    self[:tags] = self.text.scan(/#([\w\-\.]*)/).flatten
  end

Return OStatus::Entry instance describing this Update

  def to_atom(base_uri)
    links = []
    links << Atom::Link.new({ :href => ("#{base_uri}updates/#{self.id.to_s}")})

    mentions.each do |author|
      author_url = author.url
      if author_url.start_with?("/")
        author_url = "http://#{author.domain}/feeds/#{author.feed.id}"
      end

      links << Atom::Link.new({ :rel => 'ostatus:attention', :href => author_url })
      links << Atom::Link.new({ :rel => 'mentioned', :href => author_url })
    end

    OStatus::Entry.new(:title => self.text,
                       :content => Atom::Content::Html.new(self.to_html),
                       :updated => self.updated_at,
                       :published => self.created_at,
                       :activity => OStatus::Activity.new(:object_type => :note),
                       :author => self.author.to_atom,
                       :id => "#{base_uri}updates/#{self.id.to_s}",
                       :links => links)
  end

  def to_xml(base_uri)
    to_atom(base_uri).to_xml
  end

  def self.create_from_ostatus(entry, feed)
    u = new(:author     => feed.author,
            :created_at => entry.published,
            :remote_url => entry.url,
            :feed       => feed,
            :updated_at => entry.updated)

    u.sanitize_external_text(entry.content, entry.url)
    u.save
    u
  end

  def sanitize_external_text(entry_text, entry_url)

Strip HTML

    self.text = Nokogiri::HTML::Document.parse(entry_text).text

Truncate text

    truncation_necessary = self.text.length > 140
    if truncation_necessary
      self.text = self.text[0..138]
    end

Generate HTML

    if truncation_necessary
      self.html = "#{self.to_html}<a href='#{entry_url}'>\u2026</a>"
    end
  end

  protected

  def get_mentions
    self.mentions = []

    out = CGI.escapeHTML(text)

    out.gsub!(USERNAME_REGULAR_EXPRESSION) do |match|
      if $3 and a = Author.first(:username => /^#{$2}$/i, :domain => /^#{$3}$/i)
        self.mentions << a
      elsif not $3 and authors = Author.all(:username => /^#{$2}$/i)
        a = nil

        if authors.count == 1
          a = authors.first
        else

Disambiguate

Is it in update to this author?

          if in_reply_to = referral
            if not authors.index(in_reply_to.author).nil?
              a = in_reply_to.author
            end
          end

Is this update is generated by a local user, look at who they are following

          if a.nil? and user = self.author.user
            authors.each do |author|
              if user.following_author?(author)
                a = author
              end
            end
          end
        end

        self.mentions << a unless a.nil?
      end
      match
    end

    self.mentions
  end

Generate and store the html

  def generate_html
    out = CGI.escapeHTML(text)

Replace any absolute addresses with a link Note: Do this first! Otherwise it will add anchors inside anchors!

    out.gsub!(/(http[s]?:\/\/\S+[a-zA-Z0-9\/}])/, "<a href='\\1'>\\1</a>")

we let almost anything be in a username, except those that mess with urls. but you can’t end in a .:;, or ! also ignore container chars [] () “” ‘’ {} XXX: the correct solution will be to use an email validator

    out.gsub!(USERNAME_REGULAR_EXPRESSION) do |match|
      if $3 and a = Author.first(:username => /^#{$2}$/i, :domain => /^#{$3}$/i)
        author_url = a.url
        if author_url.start_with?("/")
          author_url = "http://#{author.domain}#{author_url}"
        end
        "#{$1}<a href='#{author_url}'>@#{$2}@#{$3}</a>"
      elsif not $3 and a = Author.first(:username => /^#{$2}$/i)
        author_url = a.url
        if author_url.start_with?("/")
          author_url = "http://#{author.domain}#{author_url}"
        end
        "#{$1}<a href='#{author_url}'>@#{$2}</a>"
      else
        match
      end
    end

    out.gsub!(/(^|\s+)#(\w+)/) do |match|
      "#{$1}<a href='/search?search=%23#{$2}'>##{$2}</a>"
    end

    self.html = out
  end

  def send_to_remote_mentions

Only local users can do this

    if author.user

For each mention, if they are not following this user, send this update to them as a salmon notification XXX: allow for authors that we do not know (who do not have feeds)

      mentions.each do |mentioned_author|
        unless mentioned_author.domain == author.domain
          mentioned_feed = mentioned_author.feed
          unless author.user.followers.include? mentioned_feed
            author.user.delay.send_mention_notification id, mentioned_feed.id
          end
        end
      end
    end
  end

If a user has twitter enabled on their account and they checked it on update form, repost the update to twitter

  def send_to_external_accounts
    return if ENV['RAILS_ENV'] == 'development'

If there is no user we can’t get to the oauth tokens, abort!

    if author.user

If the twitter flag is true and the user has a twitter account linked send the update

      if self.twitter? && author.user.twitter?
        begin
          Twitter.configure do |config|
            config.consumer_key = ENV["CONSUMER_KEY"]
            config.consumer_secret = ENV["CONSUMER_SECRET"]
            config.oauth_token = author.user.twitter.oauth_token
            config.oauth_token_secret = author.user.twitter.oauth_secret
          end

          Twitter.update(text)
        rescue Exception => e

I should be shot for doing this.

        end
      end
    end

  end

  def do_not_repeat_yourself
    errors.add(:text, "You already posted this update.") if already_posted?
  end

  def already_posted?
    feed.last_update && feed.last_update.id != id && feed.last_update.text == text
  end
end