diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index 2374318eb..42698f7c7 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -1,4 +1,7 @@ class ProfileController < ApplicationController def show end + + def entry + end end diff --git a/app/helpers/atom_helper.rb b/app/helpers/atom_helper.rb index 7c8f5ed1a..f091d1afe 100644 --- a/app/helpers/atom_helper.rb +++ b/app/helpers/atom_helper.rb @@ -93,6 +93,87 @@ module AtomHelper xml['poco'].note account.note end + def in_reply_to(xml, uri, url) + xml['thr'].send('in-reply-to', { ref: uri, href: url, type: 'text/html' }) + end + + def disambiguate_uri(target) + if target.local? + if target.object_type == :person + profile_url(name: target.username) + else + unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type) + end + else + target.uri + end + end + + def disambiguate_url(target) + if target.local? + if target.object_type == :person + profile_url(name: target.username) + else + status_url(name: target.stream_entry.account.username, id: target.stream_entry.id) + end + else + target.url + end + end + + def link_mention(xml, account) + xml.link(rel: 'mentioned', href: disambiguate_uri(account)) + end + + def include_author(xml, account) + object_type xml, :person + uri xml, profile_url(name: account.username) + name xml, account.username + summary xml, account.note + link_alternate xml, profile_url(name: account.username) + portable_contact xml, account + end + + def include_entry(xml, stream_entry) + unique_id xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type + published_at xml, stream_entry.activity.created_at + updated_at xml, stream_entry.activity.updated_at + title xml, stream_entry.title + content xml, stream_entry.content + verb xml, stream_entry.verb + link_self xml, atom_entry_url(id: stream_entry.id) + object_type xml, stream_entry.object_type + + # Comments need thread element + if stream_entry.threaded? + in_reply_to xml, disambiguate_uri(stream_entry.thread), disambiguate_url(stream_entry.thread) + end + + if stream_entry.targeted? + target(xml) do + object_type xml, stream_entry.target.object_type + simple_id xml, disambiguate_uri(stream_entry.target) + title xml, stream_entry.target.title + link_alternate xml, disambiguate_url(stream_entry.target) + + # People have summary and portable contacts information + if stream_entry.target.object_type == :person + summary xml, stream_entry.target.content + portable_contact xml, stream_entry.target + end + + # Statuses have content + if [:note, :comment].include? stream_entry.target.object_type + content xml, stream_entry.target.content + end + end + end + + stream_entry.mentions.each do |mentioned| + link_mention xml, mentioned + end + end + private def root_tag(xml, tag, &block) diff --git a/app/models/account.rb b/app/models/account.rb index 42d92eddf..fc399d69c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -5,6 +5,7 @@ class Account < ActiveRecord::Base # Timelines has_many :stream_entries, inverse_of: :account has_many :statuses, inverse_of: :account + has_many :favourites, inverse_of: :account # Follow relations has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy @@ -41,7 +42,7 @@ class Account < ActiveRecord::Base self.username end - def summary + def content self.note end diff --git a/app/models/favourite.rb b/app/models/favourite.rb new file mode 100644 index 000000000..20260f46b --- /dev/null +++ b/app/models/favourite.rb @@ -0,0 +1,38 @@ +class Favourite < ActiveRecord::Base + belongs_to :account, inverse_of: :favourites + belongs_to :status, inverse_of: :favourites + + has_one :stream_entry, as: :activity + + def verb + :favorite + end + + def title + "#{self.account.acct} favourited a status by #{self.status.account.acct}" + end + + def content + title + end + + def object_type + target.object_type + end + + def target + self.status + end + + def mentions + [] + end + + def thread + target + end + + after_create do + self.account.stream_entries.create!(activity: self) + end +end diff --git a/app/models/follow.rb b/app/models/follow.rb index 203215947..aa723d705 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -2,20 +2,23 @@ class Follow < ActiveRecord::Base belongs_to :account belongs_to :target_account, class_name: 'Account' + has_one :stream_entry, as: :activity + validates :account, :target_account, presence: true + validates :account_id, uniqueness: { scope: :target_account_id } def verb :follow end - def object_type - :person - end - def target self.target_account end + def object_type + target.object_type + end + def content "#{self.account.acct} started following #{self.target_account.acct}" end @@ -24,6 +27,10 @@ class Follow < ActiveRecord::Base content end + def mentions + [] + end + after_create do self.account.stream_entries.create!(activity: self) end diff --git a/app/models/status.rb b/app/models/status.rb index c0b0ca9d9..72bf1b790 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -1,24 +1,56 @@ class Status < ActiveRecord::Base belongs_to :account, inverse_of: :statuses + belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status' + belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status' + + has_one :stream_entry, as: :activity + has_many :favourites, inverse_of: :status + validates :account, presence: true + validates :uri, uniqueness: true, unless: 'local?' + + def local? + self.uri.nil? + end + + def reblog? + !self.reblog_of_id.nil? + end + + def reply? + !self.in_reply_to_id.nil? + end def verb - :post + reblog? ? :share : :post end def object_type - :note + reply? ? :comment : :note end def content - self.text + reblog? ? self.reblog.text : self.text + end + + def target + self.reblog end def title content.truncate(80, omission: "...") end + def mentions + m = [] + + m << thread.account if reply? + m << reblog.account if reblog? + + m + end + after_create do self.account.stream_entries.create!(activity: self) end diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb index 7a182bb5d..a3ae099a1 100644 --- a/app/models/stream_entry.rb +++ b/app/models/stream_entry.rb @@ -5,7 +5,7 @@ class StreamEntry < ActiveRecord::Base validates :account, :activity, presence: true def object_type - self.activity.object_type + targeted? ? :activity : self.activity.object_type end def verb @@ -13,7 +13,7 @@ class StreamEntry < ActiveRecord::Base end def targeted? - [:follow].include? self.verb + [:follow, :share, :favorite].include? verb end def target @@ -27,4 +27,16 @@ class StreamEntry < ActiveRecord::Base def content self.activity.content end + + def threaded? + [:favorite, :comment].include? verb + end + + def thread + self.activity.thread + end + + def mentions + self.activity.mentions + end end diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb index bd3c760d7..405d6c62e 100644 --- a/app/services/follow_remote_account_service.rb +++ b/app/services/follow_remote_account_service.rb @@ -15,6 +15,7 @@ class FollowRemoteAccountService account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href account.salmon_url = data.link('salmon').href + account.url = data.link('http://webfinger.net/rel/profile-page').href account.public_key = magic_key_to_pem(data.link('magic-public-key').href) account.private_key = nil diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb index 6f9b7cf73..dd9e76956 100644 --- a/app/services/process_interaction_service.rb +++ b/app/services/process_interaction_service.rb @@ -3,10 +3,10 @@ class ProcessInteractionService body = salmon.unpack(envelope) xml = Nokogiri::XML(body) - return if !involves_target_account(xml, target_account) || xml.at_xpath('//author/name').nil? || xml.at_xpath('//author/uri').nil? + return if !involves_target_account(xml, target_account) || xml.at_xpath('//xmlns:author/xmlns:name').nil? || xml.at_xpath('//xmlns:author/xmlns:uri').nil? - username = xml.at_xpath('//author/name').content - url = xml.at_xpath('//author/uri').content + username = xml.at_xpath('//xmlns:author/xmlns:name').content + url = xml.at_xpath('//xmlns:author/xmlns:uri').content domain = Addressable::URI.parse(url).host account = Account.find_by(username: username, domain: domain) diff --git a/app/views/atom/entry.xml.ruby b/app/views/atom/entry.xml.ruby index 2a26e624a..e0e089f46 100644 --- a/app/views/atom/entry.xml.ruby +++ b/app/views/atom/entry.xml.ruby @@ -1,37 +1,9 @@ Nokogiri::XML::Builder.new do |xml| entry(xml, true) do - unique_id xml, @entry.created_at, @entry.activity_id, @entry.activity_type - published_at xml, @entry.activity.created_at - updated_at xml, @entry.activity.updated_at - title xml, @entry.title - content xml, @entry.content - verb xml, @entry.verb - author(xml) do - object_type xml, :person - uri xml, profile_url(name: @entry.account.username) - name xml, @entry.account.username - summary xml, @entry.account.note - link_alternate xml, profile_url(name: @entry.account.username) - portable_contact xml, @entry.account - end - - if @entry.targeted? - target(xml) do - object_type xml, @entry.target.object_type - simple_id xml, @entry.target.uri - title xml, @entry.target.title - summary xml, @entry.target.summary - link_alternate xml, @entry.target.uri - - if @entry.target.object_type == :person - portable_contact xml, @entry.target - end - end - else - object_type xml, @entry.object_type + include_author xml, @entry.account end - link_self xml, atom_entry_url(id: @entry.id) + include_entry xml, @entry end -end +end.to_xml diff --git a/app/views/atom/user_stream.xml.ruby b/app/views/atom/user_stream.xml.ruby index 2b0c0aaa0..d7e0d5843 100644 --- a/app/views/atom/user_stream.xml.ruby +++ b/app/views/atom/user_stream.xml.ruby @@ -6,12 +6,7 @@ Nokogiri::XML::Builder.new do |xml| updated_at xml, stream_updated_at author(xml) do - object_type xml, :person - uri xml, profile_url(name: @account.username) - name xml, @account.username - summary xml, @account.note - link_alternate xml, profile_url(name: @account.username) - portable_contact xml, @account + include_author xml, @account end link_alternate xml, profile_url(name: @account.username) @@ -21,29 +16,7 @@ Nokogiri::XML::Builder.new do |xml| @account.stream_entries.order('id desc').each do |stream_entry| entry(xml, false) do - unique_id xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type - published_at xml, stream_entry.activity.created_at - updated_at xml, stream_entry.activity.updated_at - title xml, stream_entry.title - content xml, stream_entry.content - verb xml, stream_entry.verb - link_self xml, atom_entry_url(id: stream_entry.id) - - if stream_entry.targeted? - target(xml) do - object_type xml, stream_entry.target.object_type - simple_id xml, stream_entry.target.uri - title xml, stream_entry.target.title - summary xml, stream_entry.target.summary - link_alternate xml, stream_entry.target.uri - - if stream_entry.target.object_type == :person - portable_contact xml, stream_entry.target - end - end - else - object_type xml, stream_entry.object_type - end + include_entry xml, stream_entry end end end diff --git a/config/routes.rb b/config/routes.rb index c57baea28..72cfd3e3e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,7 @@ Rails.application.routes.draw do get 'atom/entries/:id', to: 'atom#entry', as: :atom_entry get 'atom/users/:id', to: 'atom#user_stream', as: :atom_user_stream get 'users/:name', to: 'profile#show', as: :profile + get 'users/:name/:id', to: 'profile#entry', as: :status mount Mastodon::API => '/api/' diff --git a/db/migrate/20160223162837_add_metadata_to_statuses.rb b/db/migrate/20160223162837_add_metadata_to_statuses.rb new file mode 100644 index 000000000..7120e582a --- /dev/null +++ b/db/migrate/20160223162837_add_metadata_to_statuses.rb @@ -0,0 +1,6 @@ +class AddMetadataToStatuses < ActiveRecord::Migration + def change + add_column :statuses, :in_reply_to_id, :integer, null: true + add_column :statuses, :reblog_of_id, :integer, null: true + end +end diff --git a/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb b/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb new file mode 100644 index 000000000..0fc1c39c3 --- /dev/null +++ b/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb @@ -0,0 +1,5 @@ +class MakeUrisNullableInStatuses < ActiveRecord::Migration + def change + change_column :statuses, :uri, :string, null: true, default: nil + end +end diff --git a/db/migrate/20160223165723_add_url_to_statuses.rb b/db/migrate/20160223165723_add_url_to_statuses.rb new file mode 100644 index 000000000..a5aa1613a --- /dev/null +++ b/db/migrate/20160223165723_add_url_to_statuses.rb @@ -0,0 +1,5 @@ +class AddUrlToStatuses < ActiveRecord::Migration + def change + add_column :statuses, :url, :string, null: true, default: nil + end +end diff --git a/db/migrate/20160223165855_add_url_to_accounts.rb b/db/migrate/20160223165855_add_url_to_accounts.rb new file mode 100644 index 000000000..59dd2b97b --- /dev/null +++ b/db/migrate/20160223165855_add_url_to_accounts.rb @@ -0,0 +1,5 @@ +class AddUrlToAccounts < ActiveRecord::Migration + def change + add_column :accounts, :url, :string, null: true, default: nil + end +end diff --git a/db/migrate/20160223171800_create_favourites.rb b/db/migrate/20160223171800_create_favourites.rb new file mode 100644 index 000000000..bb35f491f --- /dev/null +++ b/db/migrate/20160223171800_create_favourites.rb @@ -0,0 +1,12 @@ +class CreateFavourites < ActiveRecord::Migration + def change + create_table :favourites do |t| + t.integer :account_id, null: false + t.integer :status_id, null: false + + t.timestamps null: false + end + + add_index :favourites, [:account_id, :status_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7cd7c371d..28b382906 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160222143943) do +ActiveRecord::Schema.define(version: 20160223171800) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -31,10 +31,20 @@ ActiveRecord::Schema.define(version: 20160222143943) do t.text "note", default: "", null: false t.string "display_name", default: "", null: false t.string "uri", default: "", null: false + t.string "url" end add_index "accounts", ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree + create_table "favourites", force: :cascade do |t| + t.integer "account_id", null: false + t.integer "status_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "favourites", ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree + create_table "follows", force: :cascade do |t| t.integer "account_id", null: false t.integer "target_account_id", null: false @@ -45,11 +55,14 @@ ActiveRecord::Schema.define(version: 20160222143943) do add_index "follows", ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true, using: :btree create_table "statuses", force: :cascade do |t| - t.string "uri", default: "", null: false - t.integer "account_id", null: false - t.text "text", default: "", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "uri" + t.integer "account_id", null: false + t.text "text", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "in_reply_to_id" + t.integer "reblog_of_id" + t.string "url" end add_index "statuses", ["uri"], name: "index_statuses_on_uri", unique: true, using: :btree diff --git a/spec/models/favourite_spec.rb b/spec/models/favourite_spec.rb new file mode 100644 index 000000000..271aef4a4 --- /dev/null +++ b/spec/models/favourite_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Favourite, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end