forked from treehouse/mastodon
Made some progress
parent
9c4856bdb1
commit
709c6685a9
2
Gemfile
2
Gemfile
|
@ -38,6 +38,8 @@ group :development do
|
||||||
gem 'web-console', '~> 2.0'
|
gem 'web-console', '~> 2.0'
|
||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
|
gem 'better_errors'
|
||||||
|
gem 'binding_of_caller'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
|
|
|
@ -43,6 +43,10 @@ GEM
|
||||||
descendants_tracker (~> 0.0.4)
|
descendants_tracker (~> 0.0.4)
|
||||||
ice_nine (~> 0.11.0)
|
ice_nine (~> 0.11.0)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
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)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
builder (3.2.2)
|
builder (3.2.2)
|
||||||
|
@ -284,6 +288,8 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
addressable
|
addressable
|
||||||
|
better_errors
|
||||||
|
binding_of_caller
|
||||||
byebug
|
byebug
|
||||||
coffee-rails (~> 4.1.0)
|
coffee-rails (~> 4.1.0)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
|
|
|
@ -3,6 +3,8 @@ module Mastodon
|
||||||
class Account < Grape::Entity
|
class Account < Grape::Entity
|
||||||
expose :username
|
expose :username
|
||||||
expose :domain
|
expose :domain
|
||||||
|
expose :display_name
|
||||||
|
expose :note
|
||||||
end
|
end
|
||||||
|
|
||||||
class Status < Grape::Entity
|
class Status < Grape::Entity
|
||||||
|
|
|
@ -8,12 +8,10 @@ module Mastodon
|
||||||
|
|
||||||
resource :subscriptions do
|
resource :subscriptions do
|
||||||
helpers do
|
helpers do
|
||||||
def subscription_url(account)
|
include ApplicationHelper
|
||||||
"https://649841dc.ngrok.io/api#{subscriptions_path(id: account.id)}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Receive updates from a feed'
|
desc 'Receive updates from an account'
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, type: String, desc: 'Account ID'
|
requires :id, type: String, desc: 'Account ID'
|
||||||
|
@ -23,14 +21,14 @@ module Mastodon
|
||||||
body = request.body.read
|
body = request.body.read
|
||||||
|
|
||||||
if @account.subscription(subscription_url(@account)).verify(body, env['HTTP_X_HUB_SIGNATURE'])
|
if @account.subscription(subscription_url(@account)).verify(body, env['HTTP_X_HUB_SIGNATURE'])
|
||||||
ProcessFeedUpdateService.new.(body, @account)
|
ProcessFeedService.new.(body, @account)
|
||||||
status 201
|
status 201
|
||||||
else
|
else
|
||||||
status 202
|
status 202
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Confirm PuSH subscription to a feed'
|
desc 'Confirm PuSH subscription to an account'
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, type: String, desc: 'Account ID'
|
requires :id, type: String, desc: 'Account ID'
|
||||||
|
@ -49,14 +47,15 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :salmon do
|
resource :salmon do
|
||||||
desc 'Receive Salmon updates'
|
desc 'Receive Salmon updates targeted to account'
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, type: String, desc: 'Account ID'
|
requires :id, type: String, desc: 'Account ID'
|
||||||
end
|
end
|
||||||
|
|
||||||
post ':id' do
|
post ':id' do
|
||||||
# todo
|
ProcessInteractionService.new.(request.body.read, @account)
|
||||||
|
status 201
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,9 +5,34 @@ module Mastodon
|
||||||
|
|
||||||
resource :statuses do
|
resource :statuses do
|
||||||
desc 'Return a public timeline'
|
desc 'Return a public timeline'
|
||||||
|
|
||||||
get :all do
|
get :all do
|
||||||
present Status.all, with: Mastodon::Entities::Status
|
present Status.all, with: Mastodon::Entities::Status
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,5 +12,4 @@
|
||||||
//
|
//
|
||||||
//= require jquery
|
//= require jquery
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
//= require turbolinks
|
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|
|
@ -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
|
|
@ -1,6 +1,38 @@
|
||||||
class Account < ActiveRecord::Base
|
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
|
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)
|
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)
|
@subscription ||= OStatus2::Subscription.new(self.remote_url, secret: self.secret, token: self.verify_token, webhook: webhook_url, hub: self.hub_url)
|
||||||
end
|
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
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
class FollowRemoteUserService
|
class FollowRemoteAccountService
|
||||||
include GrapeRouteHelpers::NamedRouteMatcher
|
include ApplicationHelper
|
||||||
|
|
||||||
def call(user)
|
def call(uri)
|
||||||
username, domain = user.split('@')
|
username, domain = uri.split('@')
|
||||||
account = Account.where(username: username, domain: domain).first
|
account = Account.where(username: username, domain: domain).first
|
||||||
|
|
||||||
return account unless account.nil?
|
return account unless account.nil?
|
||||||
|
|
||||||
account = Account.new(username: username, domain: domain)
|
account = Account.new(username: username, domain: domain)
|
||||||
data = Goldfinger.finger("acct:#{user}")
|
data = Goldfinger.finger("acct:#{uri}")
|
||||||
|
|
||||||
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
|
||||||
|
@ -21,8 +21,9 @@ class FollowRemoteUserService
|
||||||
feed = get_feed(account.remote_url)
|
feed = get_feed(account.remote_url)
|
||||||
hubs = feed.xpath('//xmlns:link[@rel="hub"]')
|
hubs = feed.xpath('//xmlns:link[@rel="hub"]')
|
||||||
|
|
||||||
return false if hubs.empty? || hubs.first.attribute('href').nil?
|
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.hub_url = hubs.first.attribute('href').value
|
||||||
account.save!
|
account.save!
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ class FollowRemoteUserService
|
||||||
|
|
||||||
key = OpenSSL::PKey::RSA.new
|
key = OpenSSL::PKey::RSA.new
|
||||||
key.n = modulus
|
key.n = modulus
|
||||||
key.d = exponent
|
key.e = exponent
|
||||||
|
|
||||||
key.to_pem
|
key.to_pem
|
||||||
end
|
end
|
||||||
|
@ -53,8 +54,4 @@ class FollowRemoteUserService
|
||||||
def http_client
|
def http_client
|
||||||
HTTP
|
HTTP
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscription_url(account)
|
|
||||||
"https://649841dc.ngrok.io/api#{subscriptions_path(id: account.id)}"
|
|
||||||
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
|
|
@ -38,4 +38,6 @@ Rails.application.configure do
|
||||||
|
|
||||||
# Raises error for missing translations
|
# Raises error for missing translations
|
||||||
# config.action_view.raise_on_missing_translations = true
|
# config.action_view.raise_on_missing_translations = true
|
||||||
|
|
||||||
|
config.action_mailer.default_url_options = { host: ENV['NGROK_HOST'] }
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
31
db/schema.rb
31
db/schema.rb
|
@ -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: 20160220211917) do
|
ActiveRecord::Schema.define(version: 20160222143943) 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"
|
||||||
|
@ -28,10 +28,22 @@ ActiveRecord::Schema.define(version: 20160220211917) do
|
||||||
t.string "hub_url", default: "", null: false
|
t.string "hub_url", default: "", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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
|
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 "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|
|
create_table "statuses", force: :cascade do |t|
|
||||||
t.string "uri", default: "", null: false
|
t.string "uri", default: "", null: false
|
||||||
t.integer "account_id", 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
|
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
|
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