From 709c6685a90bb819696566cc9e42e587546d72dc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 22 Feb 2016 16:00:20 +0100 Subject: Made some progress --- Gemfile | 2 + Gemfile.lock | 6 +++ app/api/mastodon/entities.rb | 2 + app/api/mastodon/ostatus.rb | 15 +++--- app/api/mastodon/rest.rb | 25 +++++++++ app/assets/javascripts/application.js | 1 - app/assets/javascripts/atom.coffee | 3 ++ app/assets/javascripts/home.coffee | 3 ++ app/assets/javascripts/profile.coffee | 3 ++ app/assets/javascripts/xrd.coffee | 3 ++ app/assets/stylesheets/atom.scss | 3 ++ app/assets/stylesheets/home.scss | 3 ++ app/assets/stylesheets/profile.scss | 3 ++ app/assets/stylesheets/xrd.scss | 3 ++ app/controllers/atom_controller.rb | 14 +++++ app/controllers/home_controller.rb | 4 ++ app/controllers/profile_controller.rb | 4 ++ app/controllers/xrd_controller.rb | 39 ++++++++++++++ app/helpers/application_helper.rb | 17 ++++++ app/helpers/atom_helper.rb | 5 ++ app/helpers/home_helper.rb | 2 + app/helpers/profile_helper.rb | 2 + app/helpers/xrd_helper.rb | 2 + app/models/account.rb | 32 ++++++++++++ app/models/follow.rb | 8 +++ app/models/status.rb | 4 ++ app/models/stream_entry.rb | 33 ++++++++++++ app/models/user.rb | 3 ++ app/services/fetch_feed_service.rb | 12 ++++- app/services/follow_remote_account_service.rb | 57 ++++++++++++++++++++ app/services/follow_remote_user_service.rb | 60 ---------------------- app/services/follow_service.rb | 12 +++++ app/services/process_feed_service.rb | 20 ++++++++ app/services/process_feed_update_service.rb | 20 -------- app/services/process_interaction_service.rb | 38 ++++++++++++++ app/services/setup_local_account_service.rb | 14 +++++ app/views/atom/user_stream.xml.ruby | 35 +++++++++++++ app/views/home/index.html.haml | 1 + app/views/layouts/application.html.erb | 14 ----- app/views/layouts/application.html.haml | 10 ++++ app/views/profile/show.html.haml | 2 + app/views/xrd/host_meta.xml.ruby | 5 ++ app/views/xrd/webfinger.xml.ruby | 8 +++ config/environments/development.rb | 2 + config/initializers/ostatus.rb | 1 + config/routes.rb | 8 +++ db/migrate/20160221003140_create_users.rb | 12 +++++ db/migrate/20160221003621_create_follows.rb | 12 +++++ db/migrate/20160222122600_create_stream_entries.rb | 11 ++++ ...0160222143943_add_profile_fields_to_accounts.rb | 7 +++ db/schema.rb | 31 ++++++++++- spec/controllers/atom_controller_spec.rb | 5 ++ spec/controllers/home_controller_spec.rb | 5 ++ spec/controllers/profile_controller_spec.rb | 12 +++++ spec/controllers/xrd_controller_spec.rb | 5 ++ spec/helpers/atom_helper_spec.rb | 15 ++++++ spec/helpers/home_helper_spec.rb | 15 ++++++ spec/helpers/profile_helper_spec.rb | 15 ++++++ spec/helpers/xrd_helper_spec.rb | 15 ++++++ spec/models/follow_spec.rb | 5 ++ spec/models/stream_spec.rb | 5 ++ spec/models/user_spec.rb | 5 ++ spec/views/profile/show.html.haml_spec.rb | 5 ++ 63 files changed, 638 insertions(+), 105 deletions(-) create mode 100644 app/assets/javascripts/atom.coffee create mode 100644 app/assets/javascripts/home.coffee create mode 100644 app/assets/javascripts/profile.coffee create mode 100644 app/assets/javascripts/xrd.coffee create mode 100644 app/assets/stylesheets/atom.scss create mode 100644 app/assets/stylesheets/home.scss create mode 100644 app/assets/stylesheets/profile.scss create mode 100644 app/assets/stylesheets/xrd.scss create mode 100644 app/controllers/atom_controller.rb create mode 100644 app/controllers/home_controller.rb create mode 100644 app/controllers/profile_controller.rb create mode 100644 app/controllers/xrd_controller.rb create mode 100644 app/helpers/atom_helper.rb create mode 100644 app/helpers/home_helper.rb create mode 100644 app/helpers/profile_helper.rb create mode 100644 app/helpers/xrd_helper.rb create mode 100644 app/models/follow.rb create mode 100644 app/models/stream_entry.rb create mode 100644 app/models/user.rb create mode 100644 app/services/follow_remote_account_service.rb delete mode 100644 app/services/follow_remote_user_service.rb create mode 100644 app/services/follow_service.rb create mode 100644 app/services/process_feed_service.rb delete mode 100644 app/services/process_feed_update_service.rb create mode 100644 app/services/process_interaction_service.rb create mode 100644 app/services/setup_local_account_service.rb create mode 100644 app/views/atom/user_stream.xml.ruby create mode 100644 app/views/home/index.html.haml delete mode 100644 app/views/layouts/application.html.erb create mode 100644 app/views/layouts/application.html.haml create mode 100644 app/views/profile/show.html.haml create mode 100644 app/views/xrd/host_meta.xml.ruby create mode 100644 app/views/xrd/webfinger.xml.ruby create mode 100644 config/initializers/ostatus.rb create mode 100644 db/migrate/20160221003140_create_users.rb create mode 100644 db/migrate/20160221003621_create_follows.rb create mode 100644 db/migrate/20160222122600_create_stream_entries.rb create mode 100644 db/migrate/20160222143943_add_profile_fields_to_accounts.rb create mode 100644 spec/controllers/atom_controller_spec.rb create mode 100644 spec/controllers/home_controller_spec.rb create mode 100644 spec/controllers/profile_controller_spec.rb create mode 100644 spec/controllers/xrd_controller_spec.rb create mode 100644 spec/helpers/atom_helper_spec.rb create mode 100644 spec/helpers/home_helper_spec.rb create mode 100644 spec/helpers/profile_helper_spec.rb create mode 100644 spec/helpers/xrd_helper_spec.rb create mode 100644 spec/models/follow_spec.rb create mode 100644 spec/models/stream_spec.rb create mode 100644 spec/models/user_spec.rb create mode 100644 spec/views/profile/show.html.haml_spec.rb diff --git a/Gemfile b/Gemfile index 59eb08814..f0a77f7b6 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,8 @@ group :development do gem 'web-console', '~> 2.0' gem 'spring' gem 'rubocop', require: false + gem 'better_errors' + gem 'binding_of_caller' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index ff72f4ddf..fad87d191 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,6 +43,10 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + better_errors (2.1.1) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) @@ -284,6 +288,8 @@ PLATFORMS DEPENDENCIES addressable + better_errors + binding_of_caller byebug coffee-rails (~> 4.1.0) dotenv-rails diff --git a/app/api/mastodon/entities.rb b/app/api/mastodon/entities.rb index a3f40ec48..2e56a67df 100644 --- a/app/api/mastodon/entities.rb +++ b/app/api/mastodon/entities.rb @@ -3,6 +3,8 @@ module Mastodon class Account < Grape::Entity expose :username expose :domain + expose :display_name + expose :note end class Status < Grape::Entity diff --git a/app/api/mastodon/ostatus.rb b/app/api/mastodon/ostatus.rb index fcde980f7..4676bc429 100644 --- a/app/api/mastodon/ostatus.rb +++ b/app/api/mastodon/ostatus.rb @@ -8,12 +8,10 @@ module Mastodon resource :subscriptions do helpers do - def subscription_url(account) - "https://649841dc.ngrok.io/api#{subscriptions_path(id: account.id)}" - end + include ApplicationHelper end - desc 'Receive updates from a feed' + desc 'Receive updates from an account' params do requires :id, type: String, desc: 'Account ID' @@ -23,14 +21,14 @@ module Mastodon body = request.body.read if @account.subscription(subscription_url(@account)).verify(body, env['HTTP_X_HUB_SIGNATURE']) - ProcessFeedUpdateService.new.(body, @account) + ProcessFeedService.new.(body, @account) status 201 else status 202 end end - desc 'Confirm PuSH subscription to a feed' + desc 'Confirm PuSH subscription to an account' params do requires :id, type: String, desc: 'Account ID' @@ -49,14 +47,15 @@ module Mastodon end resource :salmon do - desc 'Receive Salmon updates' + desc 'Receive Salmon updates targeted to account' params do requires :id, type: String, desc: 'Account ID' end post ':id' do - # todo + ProcessInteractionService.new.(request.body.read, @account) + status 201 end end end diff --git a/app/api/mastodon/rest.rb b/app/api/mastodon/rest.rb index e011ab34d..eaf337938 100644 --- a/app/api/mastodon/rest.rb +++ b/app/api/mastodon/rest.rb @@ -5,9 +5,34 @@ module Mastodon resource :statuses do desc 'Return a public timeline' + get :all do present Status.all, with: Mastodon::Entities::Status end + + desc 'Return the home timeline of a logged in user' + + get :home do + # todo + end + + desc 'Return the notifications timeline of a logged in user' + + get :notifications do + # todo + end + end + + resource :accounts do + desc 'Return a user profile' + + params do + requires :id, type: String, desc: 'Account ID' + end + + get ':id' do + present Account.find(params[:id]), with: Mastodon::Entities::Account + end end end end diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e07c5a830..646c5aba4 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,5 +12,4 @@ // //= require jquery //= require jquery_ujs -//= require turbolinks //= require_tree . diff --git a/app/assets/javascripts/atom.coffee b/app/assets/javascripts/atom.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/atom.coffee @@ -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/ diff --git a/app/assets/javascripts/home.coffee b/app/assets/javascripts/home.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/home.coffee @@ -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/ diff --git a/app/assets/javascripts/profile.coffee b/app/assets/javascripts/profile.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/profile.coffee @@ -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/ diff --git a/app/assets/javascripts/xrd.coffee b/app/assets/javascripts/xrd.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/xrd.coffee @@ -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/ diff --git a/app/assets/stylesheets/atom.scss b/app/assets/stylesheets/atom.scss new file mode 100644 index 000000000..888698db3 --- /dev/null +++ b/app/assets/stylesheets/atom.scss @@ -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/ diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss new file mode 100644 index 000000000..7131aac4d --- /dev/null +++ b/app/assets/stylesheets/home.scss @@ -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/ diff --git a/app/assets/stylesheets/profile.scss b/app/assets/stylesheets/profile.scss new file mode 100644 index 000000000..22ee50876 --- /dev/null +++ b/app/assets/stylesheets/profile.scss @@ -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/ diff --git a/app/assets/stylesheets/xrd.scss b/app/assets/stylesheets/xrd.scss new file mode 100644 index 000000000..62391c7d3 --- /dev/null +++ b/app/assets/stylesheets/xrd.scss @@ -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/ diff --git a/app/controllers/atom_controller.rb b/app/controllers/atom_controller.rb new file mode 100644 index 000000000..e0b45c580 --- /dev/null +++ b/app/controllers/atom_controller.rb @@ -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 diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 000000000..95f29929c --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,4 @@ +class HomeController < ApplicationController + def index + end +end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb new file mode 100644 index 000000000..2374318eb --- /dev/null +++ b/app/controllers/profile_controller.rb @@ -0,0 +1,4 @@ +class ProfileController < ApplicationController + def show + end +end diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb new file mode 100644 index 000000000..4c8e958e6 --- /dev/null +++ b/app/controllers/xrd_controller.rb @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be7945..29e444a32 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,19 @@ 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 diff --git a/app/helpers/atom_helper.rb b/app/helpers/atom_helper.rb new file mode 100644 index 000000000..a42a49946 --- /dev/null +++ b/app/helpers/atom_helper.rb @@ -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 diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 000000000..23de56ac6 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb new file mode 100644 index 000000000..5a0d6b31f --- /dev/null +++ b/app/helpers/profile_helper.rb @@ -0,0 +1,2 @@ +module ProfileHelper +end diff --git a/app/helpers/xrd_helper.rb b/app/helpers/xrd_helper.rb new file mode 100644 index 000000000..6b273e122 --- /dev/null +++ b/app/helpers/xrd_helper.rb @@ -0,0 +1,2 @@ +module XrdHelper +end diff --git a/app/models/account.rb b/app/models/account.rb index c0b153794..90e8d7610 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,6 +1,38 @@ class Account < ActiveRecord::Base + # Local users + has_one :user, inverse_of: :account + + # Timelines + has_many :stream_entries, inverse_of: :account has_many :statuses, inverse_of: :account + # Follow relations + has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy + has_many :passive_relationships, class_name: 'Follow', foreign_key: 'target_account_id', dependent: :destroy + + has_many :following, through: :active_relationships, source: :target_account + has_many :followers, through: :passive_relationships, source: :account + + def follow!(other_account) + self.active_relationships.create!(target_account: other_account) + end + + def unfollow!(other_account) + self.active_relationships.find_by(target_account: other_account).destroy + end + + def following?(other_account) + following.include?(other_account) + end + + def local? + self.domain.nil? + end + + def keypair + self.private_key.nil? ? OpenSSL::PKey::RSA.new(self.public_key) : OpenSSL::PKey::RSA.new(self.private_key) + end + def subscription(webhook_url) @subscription ||= OStatus2::Subscription.new(self.remote_url, secret: self.secret, token: self.verify_token, webhook: webhook_url, hub: self.hub_url) end diff --git a/app/models/follow.rb b/app/models/follow.rb new file mode 100644 index 000000000..eec01b9ba --- /dev/null +++ b/app/models/follow.rb @@ -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 diff --git a/app/models/status.rb b/app/models/status.rb index a1278ccaa..d98297643 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -1,3 +1,7 @@ class Status < ActiveRecord::Base belongs_to :account, inverse_of: :statuses + + after_create do + self.account.stream_entries.create!(activity: self) + end end diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb new file mode 100644 index 000000000..cee151a07 --- /dev/null +++ b/app/models/stream_entry.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..ccfa54e4f --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,3 @@ +class User < ActiveRecord::Base + belongs_to :account, inverse_of: :user +end diff --git a/app/services/fetch_feed_service.rb b/app/services/fetch_feed_service.rb index 3b8efbe3b..059d65925 100644 --- a/app/services/fetch_feed_service.rb +++ b/app/services/fetch_feed_service.rb @@ -1,5 +1,15 @@ class FetchFeedService 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 diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb new file mode 100644 index 000000000..41f8fa4a0 --- /dev/null +++ b/app/services/follow_remote_account_service.rb @@ -0,0 +1,57 @@ +class FollowRemoteAccountService + include ApplicationHelper + + def call(uri) + username, domain = uri.split('@') + account = Account.where(username: username, domain: domain).first + + return account unless account.nil? + + account = Account.new(username: username, domain: domain) + data = Goldfinger.finger("acct:#{uri}") + + account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href + account.salmon_url = data.link('salmon').href + account.public_key = magic_key_to_pem(data.link('magic-public-key').href) + account.private_key = nil + + account.secret = SecureRandom.hex + account.verify_token = SecureRandom.hex + + feed = get_feed(account.remote_url) + hubs = feed.xpath('//xmlns:link[@rel="hub"]') + + return false if hubs.empty? || hubs.first.attribute('href').nil? || feed.at_xpath('/xmlns:author/xmlns:uri').nil? + + account.uri = feed.at_xpath('/xmlns:author/xmlns:uri').content + account.hub_url = hubs.first.attribute('href').value + account.save! + + subscription = account.subscription(subscription_url(account)) + subscription.subscribe + rescue Goldfinger::Error, HTTP::Error => e + false + end + + private + + def get_feed(url) + response = http_client.get(Addressable::URI.parse(url)) + Nokogiri::XML(response) + end + + def magic_key_to_pem(magic_key) + _, modulus, exponent = magic_key.split('.') + modulus, exponent = [modulus, exponent].map { |n| Base64.urlsafe_decode64(n).bytes.inject(0) { |num, byte| (num << 8) | byte } } + + key = OpenSSL::PKey::RSA.new + key.n = modulus + key.e = exponent + + key.to_pem + end + + def http_client + HTTP + end +end diff --git a/app/services/follow_remote_user_service.rb b/app/services/follow_remote_user_service.rb deleted file mode 100644 index f3c0e68df..000000000 --- a/app/services/follow_remote_user_service.rb +++ /dev/null @@ -1,60 +0,0 @@ -class FollowRemoteUserService - include GrapeRouteHelpers::NamedRouteMatcher - - def call(user) - username, domain = user.split('@') - account = Account.where(username: username, domain: domain).first - - return account unless account.nil? - - account = Account.new(username: username, domain: domain) - data = Goldfinger.finger("acct:#{user}") - - account.remote_url = data.link('http://schemas.google.com/g/2010#updates-from').href - account.salmon_url = data.link('salmon').href - account.public_key = magic_key_to_pem(data.link('magic-public-key').href) - account.private_key = nil - - account.secret = SecureRandom.hex - account.verify_token = SecureRandom.hex - - feed = get_feed(account.remote_url) - hubs = feed.xpath('//xmlns:link[@rel="hub"]') - - return false if hubs.empty? || hubs.first.attribute('href').nil? - - account.hub_url = hubs.first.attribute('href').value - account.save! - - subscription = account.subscription(subscription_url(account)) - subscription.subscribe - rescue Goldfinger::Error, HTTP::Error => e - false - end - - private - - def get_feed(url) - response = http_client.get(Addressable::URI.parse(url)) - Nokogiri::XML(response) - end - - def magic_key_to_pem(magic_key) - _, modulus, exponent = magic_key.split('.') - modulus, exponent = [modulus, exponent].map { |n| Base64.urlsafe_decode64(n).bytes.inject(0) { |num, byte| (num << 8) | byte } } - - key = OpenSSL::PKey::RSA.new - key.n = modulus - key.d = exponent - - key.to_pem - end - - def http_client - HTTP - end - - def subscription_url(account) - "https://649841dc.ngrok.io/api#{subscriptions_path(id: account.id)}" - end -end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb new file mode 100644 index 000000000..fc606730b --- /dev/null +++ b/app/services/follow_service.rb @@ -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 diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb new file mode 100644 index 000000000..f2523a313 --- /dev/null +++ b/app/services/process_feed_service.rb @@ -0,0 +1,20 @@ +class ProcessFeedService + def call(body, account) + xml = Nokogiri::XML(body) + + xml.xpath('/xmlns:feed/xmlns:entry').each do |entry| + uri = entry.at_xpath('./xmlns:id').content + status = Status.find_by(uri: uri) + + next unless status.nil? + + status = Status.new + status.account = account + status.uri = uri + status.text = entry.at_xpath('./xmlns:content').content + status.created_at = entry.at_xpath('./xmlns:published').content + status.updated_at = entry.at_xpath('./xmlns:updated').content + status.save! + end + end +end diff --git a/app/services/process_feed_update_service.rb b/app/services/process_feed_update_service.rb deleted file mode 100644 index 0585fad7a..000000000 --- a/app/services/process_feed_update_service.rb +++ /dev/null @@ -1,20 +0,0 @@ -class ProcessFeedUpdateService - def call(body, account) - xml = Nokogiri::XML(body) - - xml.xpath('/xmlns:feed/xmlns:entry').each do |entry| - uri = entry.at_xpath('./xmlns:id').content - status = Status.find_by(uri: uri) - - next unless status.nil? - - status = Status.new - status.account = account - status.uri = uri - status.text = entry.at_xpath('./xmlns:content').content - status.created_at = entry.at_xpath('./xmlns:published').content - status.updated_at = entry.at_xpath('./xmlns:updated').content - status.save! - end - end -end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb new file mode 100644 index 000000000..8262ead8f --- /dev/null +++ b/app/services/process_interaction_service.rb @@ -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 diff --git a/app/services/setup_local_account_service.rb b/app/services/setup_local_account_service.rb new file mode 100644 index 000000000..c40e51855 --- /dev/null +++ b/app/services/setup_local_account_service.rb @@ -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 diff --git a/app/views/atom/user_stream.xml.ruby b/app/views/atom/user_stream.xml.ruby new file mode 100644 index 000000000..d418ea0ec --- /dev/null +++ b/app/views/atom/user_stream.xml.ruby @@ -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 diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml new file mode 100644 index 000000000..862374a98 --- /dev/null +++ b/app/views/home/index.html.haml @@ -0,0 +1 @@ +Mastodon diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb deleted file mode 100644 index ff0d4c865..000000000 --- a/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ - - - - Mastodon - <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> - <%= csrf_meta_tags %> - - - -<%= yield %> - - - diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml new file mode 100644 index 000000000..0cb4e96e8 --- /dev/null +++ b/app/views/layouts/application.html.haml @@ -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 diff --git a/app/views/profile/show.html.haml b/app/views/profile/show.html.haml new file mode 100644 index 000000000..dcb5764ec --- /dev/null +++ b/app/views/profile/show.html.haml @@ -0,0 +1,2 @@ +%h1 Profile#show +%p Find me in app/views/profile/show.html.haml diff --git a/app/views/xrd/host_meta.xml.ruby b/app/views/xrd/host_meta.xml.ruby new file mode 100644 index 000000000..07d026471 --- /dev/null +++ b/app/views/xrd/host_meta.xml.ruby @@ -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 diff --git a/app/views/xrd/webfinger.xml.ruby b/app/views/xrd/webfinger.xml.ruby new file mode 100644 index 000000000..7a1e9a1d3 --- /dev/null +++ b/app/views/xrd/webfinger.xml.ruby @@ -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 diff --git a/config/environments/development.rb b/config/environments/development.rb index b55e2144b..c3377aac8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -38,4 +38,6 @@ Rails.application.configure do # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + config.action_mailer.default_url_options = { host: ENV['NGROK_HOST'] } end diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb new file mode 100644 index 000000000..64204870b --- /dev/null +++ b/config/initializers/ostatus.rb @@ -0,0 +1 @@ +LOCAL_DOMAIN = ENV['LOCAL_DOMAIN'] || 'localhost' diff --git a/config/routes.rb b/config/routes.rb index fed31a302..9ddc00c8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,11 @@ 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/' + + root 'home#index' end diff --git a/db/migrate/20160221003140_create_users.rb b/db/migrate/20160221003140_create_users.rb new file mode 100644 index 000000000..c9750c623 --- /dev/null +++ b/db/migrate/20160221003140_create_users.rb @@ -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 diff --git a/db/migrate/20160221003621_create_follows.rb b/db/migrate/20160221003621_create_follows.rb new file mode 100644 index 000000000..afec3dee0 --- /dev/null +++ b/db/migrate/20160221003621_create_follows.rb @@ -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 diff --git a/db/migrate/20160222122600_create_stream_entries.rb b/db/migrate/20160222122600_create_stream_entries.rb new file mode 100644 index 000000000..10a6862d9 --- /dev/null +++ b/db/migrate/20160222122600_create_stream_entries.rb @@ -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 diff --git a/db/migrate/20160222143943_add_profile_fields_to_accounts.rb b/db/migrate/20160222143943_add_profile_fields_to_accounts.rb new file mode 100644 index 000000000..221142bdd --- /dev/null +++ b/db/migrate/20160222143943_add_profile_fields_to_accounts.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 49ba23f19..7cd7c371d 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: 20160220211917) do +ActiveRecord::Schema.define(version: 20160222143943) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -28,10 +28,22 @@ ActiveRecord::Schema.define(version: 20160220211917) do t.string "hub_url", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "note", default: "", null: false + t.string "display_name", default: "", null: false + t.string "uri", default: "", null: false end add_index "accounts", ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree + create_table "follows", force: :cascade do |t| + t.integer "account_id", null: false + t.integer "target_account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + 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 @@ -42,4 +54,21 @@ ActiveRecord::Schema.define(version: 20160220211917) do add_index "statuses", ["uri"], name: "index_statuses_on_uri", unique: true, using: :btree + create_table "stream_entries", force: :cascade do |t| + t.integer "account_id" + t.integer "activity_id" + t.string "activity_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "email", default: "", null: false + t.integer "account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + end diff --git a/spec/controllers/atom_controller_spec.rb b/spec/controllers/atom_controller_spec.rb new file mode 100644 index 000000000..ec14db007 --- /dev/null +++ b/spec/controllers/atom_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AtomController, type: :controller do + +end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb new file mode 100644 index 000000000..e672b25e4 --- /dev/null +++ b/spec/controllers/home_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe HomeController, type: :controller do + +end diff --git a/spec/controllers/profile_controller_spec.rb b/spec/controllers/profile_controller_spec.rb new file mode 100644 index 000000000..5904f5140 --- /dev/null +++ b/spec/controllers/profile_controller_spec.rb @@ -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 diff --git a/spec/controllers/xrd_controller_spec.rb b/spec/controllers/xrd_controller_spec.rb new file mode 100644 index 000000000..03a4b5800 --- /dev/null +++ b/spec/controllers/xrd_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe XrdController, type: :controller do + +end diff --git a/spec/helpers/atom_helper_spec.rb b/spec/helpers/atom_helper_spec.rb new file mode 100644 index 000000000..57b12de67 --- /dev/null +++ b/spec/helpers/atom_helper_spec.rb @@ -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 diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb new file mode 100644 index 000000000..e537d8d9a --- /dev/null +++ b/spec/helpers/home_helper_spec.rb @@ -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 diff --git a/spec/helpers/profile_helper_spec.rb b/spec/helpers/profile_helper_spec.rb new file mode 100644 index 000000000..154c7dfb7 --- /dev/null +++ b/spec/helpers/profile_helper_spec.rb @@ -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 diff --git a/spec/helpers/xrd_helper_spec.rb b/spec/helpers/xrd_helper_spec.rb new file mode 100644 index 000000000..63ca2268a --- /dev/null +++ b/spec/helpers/xrd_helper_spec.rb @@ -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 diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb new file mode 100644 index 000000000..9b76332f6 --- /dev/null +++ b/spec/models/follow_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Follow, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/stream_spec.rb b/spec/models/stream_spec.rb new file mode 100644 index 000000000..7fc775652 --- /dev/null +++ b/spec/models/stream_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Stream, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 000000000..47a31bb43 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/profile/show.html.haml_spec.rb b/spec/views/profile/show.html.haml_spec.rb new file mode 100644 index 000000000..778dcff32 --- /dev/null +++ b/spec/views/profile/show.html.haml_spec.rb @@ -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 -- cgit