parent
9c4856bdb1
commit
709c6685a9
@ -0,0 +1,3 @@ |
|||||||
|
# Place all the behaviors and hooks related to the matching controller here. |
||||||
|
# All this logic will automatically be available in application.js. |
||||||
|
# You can use CoffeeScript in this file: http://coffeescript.org/ |
@ -0,0 +1,3 @@ |
|||||||
|
# Place all the behaviors and hooks related to the matching controller here. |
||||||
|
# All this logic will automatically be available in application.js. |
||||||
|
# You can use CoffeeScript in this file: http://coffeescript.org/ |
@ -0,0 +1,3 @@ |
|||||||
|
# Place all the behaviors and hooks related to the matching controller here. |
||||||
|
# All this logic will automatically be available in application.js. |
||||||
|
# You can use CoffeeScript in this file: http://coffeescript.org/ |
@ -0,0 +1,3 @@ |
|||||||
|
# Place all the behaviors and hooks related to the matching controller here. |
||||||
|
# All this logic will automatically be available in application.js. |
||||||
|
# You can use CoffeeScript in this file: http://coffeescript.org/ |
@ -0,0 +1,3 @@ |
|||||||
|
// Place all the styles related to the Atom controller here. |
||||||
|
// They will automatically be included in application.css. |
||||||
|
// You can use Sass (SCSS) here: http://sass-lang.com/ |
@ -0,0 +1,3 @@ |
|||||||
|
// Place all the styles related to the Home controller here. |
||||||
|
// They will automatically be included in application.css. |
||||||
|
// You can use Sass (SCSS) here: http://sass-lang.com/ |
@ -0,0 +1,3 @@ |
|||||||
|
// Place all the styles related to the Profile controller here. |
||||||
|
// They will automatically be included in application.css. |
||||||
|
// You can use Sass (SCSS) here: http://sass-lang.com/ |
@ -0,0 +1,3 @@ |
|||||||
|
// Place all the styles related to the XRD controller here. |
||||||
|
// They will automatically be included in application.css. |
||||||
|
// You can use Sass (SCSS) here: http://sass-lang.com/ |
@ -0,0 +1,14 @@ |
|||||||
|
class AtomController < ApplicationController |
||||||
|
before_filter :set_format |
||||||
|
|
||||||
|
def user_stream |
||||||
|
@account = Account.find_by!(id: params[:id], domain: nil) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def set_format |
||||||
|
request.format = 'xml' |
||||||
|
response.headers['Content-Type'] = 'application/atom+xml' |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,4 @@ |
|||||||
|
class HomeController < ApplicationController |
||||||
|
def index |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,4 @@ |
|||||||
|
class ProfileController < ApplicationController |
||||||
|
def show |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,39 @@ |
|||||||
|
class XrdController < ApplicationController |
||||||
|
before_filter :set_format |
||||||
|
|
||||||
|
def host_meta |
||||||
|
@webfinger_template = "#{webfinger_url}?resource={uri}" |
||||||
|
end |
||||||
|
|
||||||
|
def webfinger |
||||||
|
@account = Account.find_by!(username: username_from_resource, domain: nil) |
||||||
|
@canonical_account_uri = "acct:#{@account.username}#{LOCAL_DOMAIN}" |
||||||
|
@magic_key = pem_to_magic_key(@account.keypair.public_key) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def set_format |
||||||
|
request.format = 'xml' |
||||||
|
response.headers['Content-Type'] = 'application/xrd+xml' |
||||||
|
end |
||||||
|
|
||||||
|
def username_from_resource |
||||||
|
params[:resource].split('@').first.gsub('acct:', '') |
||||||
|
end |
||||||
|
|
||||||
|
def pem_to_magic_key(public_key) |
||||||
|
modulus, exponent = [public_key.n, public_key.e].map do |component| |
||||||
|
result = "" |
||||||
|
|
||||||
|
until component == 0 do |
||||||
|
result << [component % 256].pack('C') |
||||||
|
component >>= 8 |
||||||
|
end |
||||||
|
|
||||||
|
result.reverse! |
||||||
|
end |
||||||
|
|
||||||
|
(["RSA"] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.') |
||||||
|
end |
||||||
|
end |
@ -1,2 +1,19 @@ |
|||||||
module ApplicationHelper |
module ApplicationHelper |
||||||
|
include GrapeRouteHelpers::NamedRouteMatcher |
||||||
|
|
||||||
|
def unique_tag(date, id, type) |
||||||
|
"tag:#{LOCAL_DOMAIN},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}" |
||||||
|
end |
||||||
|
|
||||||
|
def subscription_url(account) |
||||||
|
add_base_url_prefix subscription_path(id: account.id, format: '') |
||||||
|
end |
||||||
|
|
||||||
|
def salmon_url(account) |
||||||
|
add_base_url_prefix salmon_path(id: account.id, format: '') |
||||||
|
end |
||||||
|
|
||||||
|
def add_base_url_prefix(suffix) |
||||||
|
"#{root_url}api#{suffix}" |
||||||
|
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,5 @@ |
|||||||
|
module AtomHelper |
||||||
|
def stream_updated_at |
||||||
|
@account.stream_entries.last ? @account.stream_entries.last.created_at.iso8601 : @account.updated_at.iso8601 |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,2 @@ |
|||||||
|
module HomeHelper |
||||||
|
end |
@ -0,0 +1,2 @@ |
|||||||
|
module ProfileHelper |
||||||
|
end |
@ -0,0 +1,2 @@ |
|||||||
|
module XrdHelper |
||||||
|
end |
@ -0,0 +1,8 @@ |
|||||||
|
class Follow < ActiveRecord::Base |
||||||
|
belongs_to :account |
||||||
|
belongs_to :target_account, class_name: 'Account' |
||||||
|
|
||||||
|
after_create do |
||||||
|
self.account.stream_entries.create!(activity: self) |
||||||
|
end |
||||||
|
end |
@ -1,3 +1,7 @@ |
|||||||
class Status < ActiveRecord::Base |
class Status < ActiveRecord::Base |
||||||
belongs_to :account, inverse_of: :statuses |
belongs_to :account, inverse_of: :statuses |
||||||
|
|
||||||
|
after_create do |
||||||
|
self.account.stream_entries.create!(activity: self) |
||||||
|
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,33 @@ |
|||||||
|
class StreamEntry < ActiveRecord::Base |
||||||
|
belongs_to :account, inverse_of: :stream_entries |
||||||
|
belongs_to :activity, polymorphic: true |
||||||
|
|
||||||
|
def object_type |
||||||
|
case self.activity_type |
||||||
|
when 'Status' |
||||||
|
:note |
||||||
|
when 'Follow' |
||||||
|
:person |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def verb |
||||||
|
case self.activity_type |
||||||
|
when 'Status' |
||||||
|
:post |
||||||
|
when 'Follow' |
||||||
|
:follow |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def target |
||||||
|
case self.activity_type |
||||||
|
when 'Follow' |
||||||
|
self.activity.target_account |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def content |
||||||
|
self.activity.text if self.activity_type == 'Status' |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,3 @@ |
|||||||
|
class User < ActiveRecord::Base |
||||||
|
belongs_to :account, inverse_of: :user |
||||||
|
end |
@ -1,5 +1,15 @@ |
|||||||
class FetchFeedService |
class FetchFeedService |
||||||
def call(account) |
def call(account) |
||||||
# todo |
process_service.(http_client.get(account.remote_url), account) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def process_service |
||||||
|
ProcessFeedService.new |
||||||
|
end |
||||||
|
|
||||||
|
def http_client |
||||||
|
HTTP |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,12 @@ |
|||||||
|
class FollowService |
||||||
|
def call(source_account, uri) |
||||||
|
target_account = follow_remote_account_service.(uri) |
||||||
|
source_account.follow!(target_account) |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def follow_remote_account_service |
||||||
|
FollowRemoteAccountService.new |
||||||
|
end |
||||||
|
end |
@ -1,4 +1,4 @@ |
|||||||
class ProcessFeedUpdateService |
class ProcessFeedService |
||||||
def call(body, account) |
def call(body, account) |
||||||
xml = Nokogiri::XML(body) |
xml = Nokogiri::XML(body) |
||||||
|
|
@ -0,0 +1,38 @@ |
|||||||
|
class ProcessInteractionService |
||||||
|
def call(envelope, target_account) |
||||||
|
body = salmon.unpack(envelope) |
||||||
|
xml = Nokogiri::XML(body) |
||||||
|
|
||||||
|
return if xml.at_xpath('//author/name').nil? || xml.at_xpath('//author/uri').nil? |
||||||
|
|
||||||
|
username = xml.at_xpath('//author/name').content |
||||||
|
url = xml.at_xpath('//author/uri').content |
||||||
|
domain = Addressable::URI.parse(url).host |
||||||
|
account = Account.find_by(username: username, domain: domain) |
||||||
|
|
||||||
|
if account.nil? |
||||||
|
account = follow_remote_account_service.("acct:#{username}@#{domain}") |
||||||
|
end |
||||||
|
|
||||||
|
if salmon.verify(envelope, account.keypair) |
||||||
|
verb = xml.at_path('//activity:verb').content |
||||||
|
|
||||||
|
case verb |
||||||
|
when 'http://activitystrea.ms/schema/1.0/follow', 'follow' |
||||||
|
account.follow!(target_account) |
||||||
|
when 'http://activitystrea.ms/schema/1.0/unfollow', 'unfollow' |
||||||
|
account.unfollow!(target_account) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def salmon |
||||||
|
OStatus2::Salmon.new |
||||||
|
end |
||||||
|
|
||||||
|
def follow_remote_account_service |
||||||
|
FollowRemoteAccountService.new |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,14 @@ |
|||||||
|
class SetupLocalAccountService |
||||||
|
def call(user, username) |
||||||
|
user.build_account |
||||||
|
|
||||||
|
user.account.username = username |
||||||
|
user.account.domain = nil |
||||||
|
|
||||||
|
keypair = OpenSSL::PKey::RSA.new(2048) |
||||||
|
user.account.private_key = keypair.to_pem |
||||||
|
user.account.public_key = keypair.public_key.to_pem |
||||||
|
|
||||||
|
user.save! |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,35 @@ |
|||||||
|
Nokogiri::XML::Builder.new do |xml| |
||||||
|
xml.feed(xmlns: 'http://www.w3.org/2005/Atom', 'xmlns:thr': 'http://purl.org/syndication/thread/1.0', 'xmlns:activity': 'http://activitystrea.ms/spec/1.0/') do |
||||||
|
xml.id_ atom_user_stream_url(id: @account.id) |
||||||
|
xml.title @account.display_name |
||||||
|
xml.subtitle @account.note |
||||||
|
xml.updated stream_updated_at |
||||||
|
|
||||||
|
xml.author do |
||||||
|
xml['activity'].send('object-type', 'http://activitystrea.ms/schema/1.0/person') |
||||||
|
xml.uri profile_url(name: @account.username) |
||||||
|
xml.name @account.username |
||||||
|
xml.summary @account.note |
||||||
|
|
||||||
|
xml.link(rel: 'alternate', type: 'text/html', href: profile_url(name: @account.username)) |
||||||
|
end |
||||||
|
|
||||||
|
xml.link(rel: 'alternate', type: 'text/html', href: profile_url(name: @account.username)) |
||||||
|
xml.link(rel: 'hub', href: '') |
||||||
|
xml.link(rel: 'salmon', href: salmon_url(@account)) |
||||||
|
xml.link(rel: 'self', type: 'application/atom+xml', href: atom_user_stream_url(id: @account.id)) |
||||||
|
|
||||||
|
@account.stream_entries.each do |stream_entry| |
||||||
|
xml.entry do |
||||||
|
xml.id_ unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type) |
||||||
|
xml.published stream_entry.activity.created_at.iso8601 |
||||||
|
xml.updated stream_entry.activity.updated_at.iso8601 |
||||||
|
xml.content({ type: 'html' }, stream_entry.content) |
||||||
|
xml.title |
||||||
|
|
||||||
|
xml['activity'].send('verb', "http://activitystrea.ms/schema/1.0/#{stream_entry.verb}") |
||||||
|
xml['activity'].send('object-type', "http://activitystrea.ms/schema/1.0/#{stream_entry.object_type}") |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end.to_xml |
@ -0,0 +1 @@ |
|||||||
|
Mastodon |
@ -1,14 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<title>Mastodon</title> |
|
||||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> |
|
||||||
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> |
|
||||||
<%= csrf_meta_tags %> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
|
|
||||||
<%= yield %> |
|
||||||
|
|
||||||
</body> |
|
||||||
</html> |
|
@ -0,0 +1,10 @@ |
|||||||
|
!!! |
||||||
|
%html |
||||||
|
%head |
||||||
|
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ |
||||||
|
%title Mastodon |
||||||
|
= stylesheet_link_tag 'application', media: 'all' |
||||||
|
= javascript_include_tag 'application' |
||||||
|
= csrf_meta_tags |
||||||
|
%body |
||||||
|
= yield |
@ -0,0 +1,2 @@ |
|||||||
|
%h1 Profile#show |
||||||
|
%p Find me in app/views/profile/show.html.haml |
@ -0,0 +1,5 @@ |
|||||||
|
Nokogiri::XML::Builder.new do |xml| |
||||||
|
xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do |
||||||
|
xml.Link(rel: 'lrdd', type: 'application/xrd+xml', template: @webfinger_template) |
||||||
|
end |
||||||
|
end.to_xml |
@ -0,0 +1,8 @@ |
|||||||
|
Nokogiri::XML::Builder.new do |xml| |
||||||
|
xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do |
||||||
|
xml.Subject @canonical_account_uri |
||||||
|
xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: atom_user_stream_url(id: @account.id)) |
||||||
|
xml.Link(rel: 'salmon', href: salmon_url(@account)) |
||||||
|
xml.Link(rel: 'magic-public-key', href: @magic_key) |
||||||
|
end |
||||||
|
end.to_xml |
@ -0,0 +1 @@ |
|||||||
|
LOCAL_DOMAIN = ENV['LOCAL_DOMAIN'] || 'localhost' |
@ -1,3 +1,11 @@ |
|||||||
Rails.application.routes.draw do |
Rails.application.routes.draw do |
||||||
|
get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta |
||||||
|
get '.well-known/webfinger', to: 'xrd#webfinger', as: :webfinger |
||||||
|
|
||||||
|
get 'atom/:id', to: 'atom#user_stream', as: :atom_user_stream |
||||||
|
get 'user/:name', to: 'profile#show', as: :profile |
||||||
|
|
||||||
mount Mastodon::API => '/api/' |
mount Mastodon::API => '/api/' |
||||||
|
|
||||||
|
root 'home#index' |
||||||
end |
end |
||||||
|
@ -0,0 +1,12 @@ |
|||||||
|
class CreateUsers < ActiveRecord::Migration |
||||||
|
def change |
||||||
|
create_table :users do |t| |
||||||
|
t.string :email, null: false, default: '' |
||||||
|
t.integer :account_id, null: false |
||||||
|
|
||||||
|
t.timestamps null: false |
||||||
|
end |
||||||
|
|
||||||
|
add_index :users, :email, unique: true |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,12 @@ |
|||||||
|
class CreateFollows < ActiveRecord::Migration |
||||||
|
def change |
||||||
|
create_table :follows do |t| |
||||||
|
t.integer :account_id, null: false |
||||||
|
t.integer :target_account_id, null: false |
||||||
|
|
||||||
|
t.timestamps null: false |
||||||
|
end |
||||||
|
|
||||||
|
add_index :follows, [:account_id, :target_account_id], unique: true |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,11 @@ |
|||||||
|
class CreateStreamEntries < ActiveRecord::Migration |
||||||
|
def change |
||||||
|
create_table :stream_entries do |t| |
||||||
|
t.integer :account_id |
||||||
|
t.integer :activity_id |
||||||
|
t.string :activity_type |
||||||
|
|
||||||
|
t.timestamps null: false |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,7 @@ |
|||||||
|
class AddProfileFieldsToAccounts < ActiveRecord::Migration |
||||||
|
def change |
||||||
|
add_column :accounts, :note, :text, null: false, default: '' |
||||||
|
add_column :accounts, :display_name, :string, null: false, default: '' |
||||||
|
add_column :accounts, :uri, :string, null: false, default: '' |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe AtomController, type: :controller do |
||||||
|
|
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe HomeController, type: :controller do |
||||||
|
|
||||||
|
end |
@ -0,0 +1,12 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe ProfileController, type: :controller do |
||||||
|
|
||||||
|
describe "GET #show" do |
||||||
|
it "returns http success" do |
||||||
|
get :show |
||||||
|
expect(response).to have_http_status(:success) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe XrdController, type: :controller do |
||||||
|
|
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes |
||||||
|
# the AtomHelper. For example: |
||||||
|
# |
||||||
|
# describe AtomHelper do |
||||||
|
# describe "string concat" do |
||||||
|
# it "concats two strings with spaces" do |
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that") |
||||||
|
# end |
||||||
|
# end |
||||||
|
# end |
||||||
|
RSpec.describe AtomHelper, type: :helper do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes |
||||||
|
# the HomeHelper. For example: |
||||||
|
# |
||||||
|
# describe HomeHelper do |
||||||
|
# describe "string concat" do |
||||||
|
# it "concats two strings with spaces" do |
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that") |
||||||
|
# end |
||||||
|
# end |
||||||
|
# end |
||||||
|
RSpec.describe HomeHelper, type: :helper do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes |
||||||
|
# the ProfileHelper. For example: |
||||||
|
# |
||||||
|
# describe ProfileHelper do |
||||||
|
# describe "string concat" do |
||||||
|
# it "concats two strings with spaces" do |
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that") |
||||||
|
# end |
||||||
|
# end |
||||||
|
# end |
||||||
|
RSpec.describe ProfileHelper, type: :helper do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes |
||||||
|
# the XrdHelper. For example: |
||||||
|
# |
||||||
|
# describe XrdHelper do |
||||||
|
# describe "string concat" do |
||||||
|
# it "concats two strings with spaces" do |
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that") |
||||||
|
# end |
||||||
|
# end |
||||||
|
# end |
||||||
|
RSpec.describe XrdHelper, type: :helper do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe Follow, type: :model do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe Stream, type: :model do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe User, type: :model do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
@ -0,0 +1,5 @@ |
|||||||
|
require 'rails_helper' |
||||||
|
|
||||||
|
RSpec.describe "profile/show.html.haml", type: :view do |
||||||
|
pending "add some examples to (or delete) #{__FILE__}" |
||||||
|
end |
Loading…
Reference in new issue