@@ -1,4 +1,7 @@ | |||||
class ProfileController < ApplicationController | class ProfileController < ApplicationController | ||||
def show | def show | ||||
end | end | ||||
def entry | |||||
end | |||||
end | end |
@@ -93,6 +93,87 @@ module AtomHelper | |||||
xml['poco'].note account.note | xml['poco'].note account.note | ||||
end | 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 | private | ||||
def root_tag(xml, tag, &block) | def root_tag(xml, tag, &block) | ||||
@@ -5,6 +5,7 @@ class Account < ActiveRecord::Base | |||||
# Timelines | # Timelines | ||||
has_many :stream_entries, inverse_of: :account | has_many :stream_entries, inverse_of: :account | ||||
has_many :statuses, inverse_of: :account | has_many :statuses, inverse_of: :account | ||||
has_many :favourites, inverse_of: :account | |||||
# Follow relations | # Follow relations | ||||
has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy | has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy | ||||
@@ -41,7 +42,7 @@ class Account < ActiveRecord::Base | |||||
self.username | self.username | ||||
end | end | ||||
def summary | |||||
def content | |||||
self.note | self.note | ||||
end | end | ||||
@@ -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 |
@@ -2,20 +2,23 @@ class Follow < ActiveRecord::Base | |||||
belongs_to :account | belongs_to :account | ||||
belongs_to :target_account, class_name: 'Account' | belongs_to :target_account, class_name: 'Account' | ||||
has_one :stream_entry, as: :activity | |||||
validates :account, :target_account, presence: true | validates :account, :target_account, presence: true | ||||
validates :account_id, uniqueness: { scope: :target_account_id } | |||||
def verb | def verb | ||||
:follow | :follow | ||||
end | end | ||||
def object_type | |||||
:person | |||||
end | |||||
def target | def target | ||||
self.target_account | self.target_account | ||||
end | end | ||||
def object_type | |||||
target.object_type | |||||
end | |||||
def content | def content | ||||
"#{self.account.acct} started following #{self.target_account.acct}" | "#{self.account.acct} started following #{self.target_account.acct}" | ||||
end | end | ||||
@@ -24,6 +27,10 @@ class Follow < ActiveRecord::Base | |||||
content | content | ||||
end | end | ||||
def mentions | |||||
[] | |||||
end | |||||
after_create do | after_create do | ||||
self.account.stream_entries.create!(activity: self) | self.account.stream_entries.create!(activity: self) | ||||
end | end | ||||
@@ -1,24 +1,56 @@ | |||||
class Status < ActiveRecord::Base | class Status < ActiveRecord::Base | ||||
belongs_to :account, inverse_of: :statuses | 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 :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 | def verb | ||||
:post | |||||
reblog? ? :share : :post | |||||
end | end | ||||
def object_type | def object_type | ||||
:note | |||||
reply? ? :comment : :note | |||||
end | end | ||||
def content | def content | ||||
self.text | |||||
reblog? ? self.reblog.text : self.text | |||||
end | |||||
def target | |||||
self.reblog | |||||
end | end | ||||
def title | def title | ||||
content.truncate(80, omission: "...") | content.truncate(80, omission: "...") | ||||
end | end | ||||
def mentions | |||||
m = [] | |||||
m << thread.account if reply? | |||||
m << reblog.account if reblog? | |||||
m | |||||
end | |||||
after_create do | after_create do | ||||
self.account.stream_entries.create!(activity: self) | self.account.stream_entries.create!(activity: self) | ||||
end | end | ||||
@@ -5,7 +5,7 @@ class StreamEntry < ActiveRecord::Base | |||||
validates :account, :activity, presence: true | validates :account, :activity, presence: true | ||||
def object_type | def object_type | ||||
self.activity.object_type | |||||
targeted? ? :activity : self.activity.object_type | |||||
end | end | ||||
def verb | def verb | ||||
@@ -13,7 +13,7 @@ class StreamEntry < ActiveRecord::Base | |||||
end | end | ||||
def targeted? | def targeted? | ||||
[:follow].include? self.verb | |||||
[:follow, :share, :favorite].include? verb | |||||
end | end | ||||
def target | def target | ||||
@@ -27,4 +27,16 @@ class StreamEntry < ActiveRecord::Base | |||||
def content | def content | ||||
self.activity.content | self.activity.content | ||||
end | end | ||||
def threaded? | |||||
[:favorite, :comment].include? verb | |||||
end | |||||
def thread | |||||
self.activity.thread | |||||
end | |||||
def mentions | |||||
self.activity.mentions | |||||
end | |||||
end | end |
@@ -15,6 +15,7 @@ class FollowRemoteAccountService | |||||
account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href | account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href | ||||
account.salmon_url = data.link('salmon').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.public_key = magic_key_to_pem(data.link('magic-public-key').href) | ||||
account.private_key = nil | account.private_key = nil | ||||
@@ -3,10 +3,10 @@ class ProcessInteractionService | |||||
body = salmon.unpack(envelope) | body = salmon.unpack(envelope) | ||||
xml = Nokogiri::XML(body) | 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 | domain = Addressable::URI.parse(url).host | ||||
account = Account.find_by(username: username, domain: domain) | account = Account.find_by(username: username, domain: domain) | ||||
@@ -1,37 +1,9 @@ | |||||
Nokogiri::XML::Builder.new do |xml| | Nokogiri::XML::Builder.new do |xml| | ||||
entry(xml, true) do | 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 | 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 | end | ||||
link_self xml, atom_entry_url(id: @entry.id) | |||||
include_entry xml, @entry | |||||
end | end | ||||
end | |||||
end.to_xml |
@@ -6,12 +6,7 @@ Nokogiri::XML::Builder.new do |xml| | |||||
updated_at xml, stream_updated_at | updated_at xml, stream_updated_at | ||||
author(xml) do | 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 | end | ||||
link_alternate xml, profile_url(name: @account.username) | 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| | @account.stream_entries.order('id desc').each do |stream_entry| | ||||
entry(xml, false) do | 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 | end | ||||
end | end | ||||
@@ -5,6 +5,7 @@ Rails.application.routes.draw do | |||||
get 'atom/entries/:id', to: 'atom#entry', as: :atom_entry | get 'atom/entries/:id', to: 'atom#entry', as: :atom_entry | ||||
get 'atom/users/:id', to: 'atom#user_stream', as: :atom_user_stream | get 'atom/users/:id', to: 'atom#user_stream', as: :atom_user_stream | ||||
get 'users/:name', to: 'profile#show', as: :profile | get 'users/:name', to: 'profile#show', as: :profile | ||||
get 'users/:name/:id', to: 'profile#entry', as: :status | |||||
mount Mastodon::API => '/api/' | mount Mastodon::API => '/api/' | ||||
@@ -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 |
@@ -0,0 +1,5 @@ | |||||
class MakeUrisNullableInStatuses < ActiveRecord::Migration | |||||
def change | |||||
change_column :statuses, :uri, :string, null: true, default: nil | |||||
end | |||||
end |
@@ -0,0 +1,5 @@ | |||||
class AddUrlToStatuses < ActiveRecord::Migration | |||||
def change | |||||
add_column :statuses, :url, :string, null: true, default: nil | |||||
end | |||||
end |
@@ -0,0 +1,5 @@ | |||||
class AddUrlToAccounts < ActiveRecord::Migration | |||||
def change | |||||
add_column :accounts, :url, :string, null: true, default: nil | |||||
end | |||||
end |
@@ -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 |
@@ -11,7 +11,7 @@ | |||||
# | # | ||||
# It's strongly recommended that you check this file into your version control system. | # 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 | # These are extensions that must be enabled in order to support this database | ||||
enable_extension "plpgsql" | enable_extension "plpgsql" | ||||
@@ -31,10 +31,20 @@ ActiveRecord::Schema.define(version: 20160222143943) do | |||||
t.text "note", default: "", null: false | t.text "note", default: "", null: false | ||||
t.string "display_name", default: "", null: false | t.string "display_name", default: "", null: false | ||||
t.string "uri", default: "", null: false | t.string "uri", default: "", null: false | ||||
t.string "url" | |||||
end | end | ||||
add_index "accounts", ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree | 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| | create_table "follows", force: :cascade do |t| | ||||
t.integer "account_id", null: false | t.integer "account_id", null: false | ||||
t.integer "target_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 | 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| | 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 | end | ||||
add_index "statuses", ["uri"], name: "index_statuses_on_uri", unique: true, using: :btree | add_index "statuses", ["uri"], name: "index_statuses_on_uri", unique: true, using: :btree | ||||
@@ -0,0 +1,5 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe Favourite, type: :model do | |||||
pending "add some examples to (or delete) #{__FILE__}" | |||||
end |