Commit 0e8f59c1 authored by Eugen Rochko's avatar Eugen Rochko

Refactoring Grape API methods into normal controllers & other things

parent 11ff92c9
......@@ -22,6 +22,7 @@ gem 'grape-entity'
gem 'hashie-forbidden_attributes'
gem 'paranoia', '~> 2.0'
gem 'paperclip', '~> 4.3'
gem 'backport_new_renderer'
gem 'http'
gem 'addressable'
......
......@@ -43,6 +43,8 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
backport_new_renderer (1.0.0)
rails
better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
......@@ -320,6 +322,7 @@ PLATFORMS
DEPENDENCIES
addressable
backport_new_renderer
better_errors
binding_of_caller
coffee-rails (~> 4.1.0)
......
module Mastodon
class API < Grape::API
rescue_from :all
mount Mastodon::Ostatus
mount Mastodon::Rest
end
end
module Mastodon
module Entities
class Account < Grape::Entity
include ApplicationHelper
expose :id
expose :username
expose :domain do |account|
account.local? ? LOCAL_DOMAIN : account.domain
end
expose :display_name
expose :note
expose :url do |account|
account.local? ? profile_url(name: account.username) : account.url
end
end
class Status < Grape::Entity
include ApplicationHelper
format_with(:iso_timestamp) { |dt| dt.iso8601 }
expose :id
expose :uri do |status|
status.local? ? unique_tag(status.stream_entry.created_at, status.stream_entry.activity_id, status.stream_entry.activity_type) : status.uri
end
expose :url do |status|
status.local? ? status_url(name: status.account.username, id: status.id) : status.url
end
expose :text
expose :in_reply_to_id
expose :reblog_of_id
expose :reblog, using: Mastodon::Entities::Status
expose :account, using: Mastodon::Entities::Account
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
class StreamEntry < Grape::Entity
expose :activity, using: Mastodon::Entities::Status
end
end
end
module Mastodon
class Ostatus < Grape::API
format :txt
before do
@account = Account.find(params[:id])
end
resource :subscriptions do
helpers do
include ApplicationHelper
end
desc 'Receive updates from an account'
params do
requires :id, type: String, desc: 'Account ID'
end
post ':id' do
body = request.body.read
if @account.subscription(subscription_url(@account)).verify(body, env['HTTP_X_HUB_SIGNATURE'])
ProcessFeedService.new.(body, @account)
status 201
else
status 202
end
end
desc 'Confirm PuSH subscription to an account'
params do
requires :id, type: String, desc: 'Account ID'
requires 'hub.topic', type: String, desc: 'Topic URL'
requires 'hub.verify_token', type: String, desc: 'Verification token'
requires 'hub.challenge', type: String, desc: 'Hub challenge'
end
get ':id' do
if @account.subscription(subscription_url(@account)).valid?(params['hub.topic'], params['hub.verify_token'])
params['hub.challenge']
else
error! :not_found, 404
end
end
end
resource :salmon do
desc 'Receive Salmon updates targeted to account'
params do
requires :id, type: String, desc: 'Account ID'
end
post ':id' do
ProcessInteractionService.new.(request.body.read, @account)
status 201
end
end
end
end
module Mastodon
class Rest < Grape::API
version 'v1', using: :path
format :json
helpers do
def current_user
User.first
end
end
resource :timelines do
desc 'Return a public timeline'
get :public do
# todo
end
desc 'Return the home timeline of a logged in user'
get :home do
present current_user.timeline, with: Mastodon::Entities::StreamEntry
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
# 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/
# 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/
# 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/
# 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/
.card {
display: flex;
background: $primary-color;
box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
.bio {
flex-grow: 1;
}
.name {
font-size: 20px;
line-height: 18px * 1.5;
color: $quaternary-color;
small {
display: block;
font-size: 14px;
color: $quaternary-color;
}
}
.avatar {
width: 96px;
float: left;
margin-right: 10px;
padding: 10px;
padding-right: 0;
padding-left: 9px;
margin-top: -30px;
img {
width: 94px;
height: 94px;
display: block;
border-radius: 5px;
box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
}
}
}
// Place all the styles related to the Atom controller here.
// Place all the styles related to the API::Salmon controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the XRD controller here.
// Place all the styles related to the API::Subscriptions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
......@@ -20,7 +20,7 @@ body {
}
.container {
width: 800px;
width: 700px;
margin: 0 auto;
margin-top: 40px;
}
......@@ -40,4 +40,5 @@ body {
@import 'home';
@import 'profile';
@import 'accounts';
@import 'stream_entries';
.card {
display: flex;
background: $darker-background-color;
border: 1px solid darken($darker-background-color, 15%);
box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
.bio {
flex-grow: 1;
}
.name {
font-size: 24px;
line-height: 18px * 1.5;
color: $text-color;
small {
display: block;
font-size: 14px;
color: $lighter-text-color;
}
}
.avatar {
width: 96px;
float: left;
margin-right: 10px;
padding: 10px;
padding-left: 9px;
margin-top: -30px;
img {
width: 94px;
height: 94px;
display: block;
border: 2px solid $lighter-text-color;
border-radius: 5px;
}
}
}
.activity-stream {
clear: both;
box-shadow: 4px 3px 0 rgba(0, 0, 0, 0.1);
.entry {
border-bottom: 1px solid darken($background-color, 10%);
border-bottom: 1px solid $darker-background-color;
background: $background-color;
border-left: 2px solid $primary-color;
&.entry-reblog {
border-left: 2px solid $tertiary-color;
.content {
a {
color: $tertiary-color;
}
}
}
&.entry-predecessor, &.entry-successor {
border-left: 2px solid $lighter-text-color;
background: darken($background-color, 5%);
.content {
a {
color: $lighter-text-color;
}
}
}
&.entry-follow, &.entry-favourite {
......@@ -92,9 +65,10 @@
}
.header {
margin-bottom: 10px;
margin-bottom: 5px;
padding: 10px;
padding-bottom: 0;
padding-left: 8px;
.name {
text-decoration: none;
......@@ -113,7 +87,7 @@
}
.pre-header {
border-bottom: 1px solid darken($background-color, 10%);
border-bottom: 1px solid darken($background-color, 5%);
color: $tertiary-color;
padding: 5px 10px;
padding-left: 8px;
......@@ -131,9 +105,10 @@
}
.content {
font-size: 16px;
font-size: 14px;
padding: 0 10px;
padding-left: 8px;
padding-bottom: 25px;
a {
color: $primary-color;
......@@ -153,24 +128,4 @@
text-decoration: underline;
}
}
.counters {
margin-top: 15px;
color: $lighter-text-color;
cursor: default;
padding: 10px;
padding-top: 0;
.counter {
display: inline-block;
margin-right: 10px;
color: $lighter-text-color;
}
.conversation-link {
color: $primary-color;
text-decoration: underline;
float: right;
}
}
}
class AccountsController < ApplicationController
before_action :set_account
def show
respond_to do |format|
format.html
format.atom
end
end
private
def set_account
@account = Account.find_by!(username: params[:username], domain: nil)
end
end
class Api::SalmonController < ApplicationController
before_action :set_account
def update
ProcessInteractionService.new.(request.body.read, @account)
render nothing: true, status: 201
end
private
def set_account
@account = Account.find(params[:id])
end
end
class Api::SubscriptionsController < ApplicationController
before_action :set_account
def show
if @account.subscription(api_subscription_url(@account.id)).valid?(params['hub.topic'], params['hub.verify_token'])
render text: params['hub.challenge'], status: 200
else
render nothing: true, status: 404
end
end
def update
body = request.body.read
if @account.subscription(api_subscription_url(@account.id)).verify(body, env['HTTP_X_HUB_SIGNATURE'])
ProcessFeedService.new.(body, @account)
render nothing: true, status: 201
else
render nothing: true, status: 202
end
end
private
def set_account
@account = Account.find(params[:id])
end
end
class AtomController < ApplicationController
before_filter :set_format
def user_stream
@account = Account.find_by!(id: params[:id], domain: nil)
end
def entry
@entry = StreamEntry.find(params[:id])
end
private
def set_format
request.format = 'xml'
response.headers['Content-Type'] = 'application/atom+xml'
end
end
class ProfileController < ApplicationController
before_action :set_account
def show
end
def entry
@entry = @account.stream_entries.find(params[:id])
@type = @entry.activity_type.downcase
end
private
def set_account
@account = Account.find_by!(username: params[:name], domain: nil)
end
end
class StreamEntriesController < ApplicationController
before_action :set_account
before_action :set_stream_entry
def show
@type = @stream_entry.activity_type.downcase
respond_to do |format|
format.html
format.atom
end
end
private
def set_account
@account = Account.find_by!(username: params[:account_username], domain: nil)
end
def set_stream_entry
@stream_entry = @account.stream_entries.find(params[:id])
end
end
......@@ -9,6 +9,8 @@ class XrdController < ApplicationController
@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)
rescue ActiveRecord::RecordNotFound
render nothing: true, status: 404
end
private
......
module AccountsHelper
end
module Api::SalmonHelper
end
module Api::SubscriptionsHelper
end
module ApplicationHelper
include RoutingHelper
def unique_tag(date, id, type)
"tag:#{LOCAL_DOMAIN},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
end
......@@ -13,24 +11,4 @@ module ApplicationHelper
def local_id?(id)
id.start_with?("tag:#{LOCAL_DOMAIN}")
end
def subscription_url(account)
add_base_url_prefix subscriptions_path(id: account.id, format: '')
end
def salmon_url(account)
add_base_url_prefix salmon_path(id: account.id, format: '')
end
def profile_url(account)
account.local? ? super(name: account.username) : account.url
end
def status_url(status)
status.local? ? super(name: status.account.username, id: status.stream_entry.id) : status.url
end
def add_base_url_prefix(suffix)
File.join(root_url, "api", suffix)
end
end
module AtomHelper
module AtomBuilderHelper
def stream_updated_at
@account.stream_entries.last ? (@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at) : @account.updated_at
end
......@@ -97,10 +97,10 @@ module AtomHelper
xml['thr'].send('in-reply-to', { ref: uri, href: url, type: 'text/html' })
end
def disambiguate_uri(target)
def uri_for_target(target)
if target.local?
if target.object_type == :person
profile_url(target)
account_url(target)
else
unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type)
end
......@@ -109,12 +109,12 @@ module AtomHelper
end
end
def disambiguate_url(target)
def url_for_target(target)
if target.local?
if target.object_type == :person
profile_url(target)
account_url(target)
else
status_url(target)
account_stream_entry_url(target.account, target.stream_entry)
end
else
target.url
......@@ -122,13 +122,13 @@ module AtomHelper
end
def link_mention(xml, account)
xml.link(rel: 'mentioned', href: disambiguate_uri(account))
xml.link(rel: 'mentioned', href: uri_for_target(account))
end
def link_avatar(xml, account)
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '300', 'media:height' =>'300', 'href' => asset_url(account.avatar.url(:large)))
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '96', 'media:height' =>'96', 'href' => asset_url(account.avatar.url(:medium)))
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '48', 'media:height' =>'48', 'href' => asset_url(account.avatar.url(:small)))
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '300', 'media:height' =>'300', 'href' => asset_url(account.avatar.url(:large, false)))
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '96', 'media:height' =>'96', 'href' => asset_url(account.avatar.url(:medium, false)))
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => '48', 'media:height' =>'48', 'href' => asset_url(account.avatar.url(:small, false)))
end
def logo(xml, url)
......@@ -137,10 +137,10 @@ module AtomHelper
def include_author(xml, account)
object_type xml, :person
uri xml, profile_url(account)
uri xml, url_for_target(account)
name xml, account.username
summary xml, account.note
link_alternate xml, profile_url(account)
link_alternate xml, url_for_target(account)
link_avatar xml, account
portable_contact xml, account
end
......@@ -152,20 +152,20 @@ module AtomHelper
title xml, stream_entry.title
content xml, stream_entry.content
verb xml, stream_entry.verb
link_self xml, atom_entry_url(id: stream_entry.id)
link_self xml, account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')
object_type xml, stream_entry.object_type
# Comments need thread element
if stream_entry.threaded?
in_reply_to xml, disambiguate_uri(stream_entry.thread), disambiguate_url(stream_entry.thread)
in_reply_to xml, uri_for_target(stream_entry.thread), url_for_target(stream_entry.thread)
end
if stream_entry.targeted?
target(xml) do
object_type xml, stream_entry.target.object_type
simple_id xml, disambiguate_uri(stream_entry.target)
simple_id xml, uri_for_target(stream_entry.target)
title xml, stream_entry.target.title
link_alternate xml, disambiguate_url(stream_entry.target)
link_alternate xml, url_for_target(stream_entry.target)
# People have summary and portable contacts information
if stream_entry.target.object_type == :person
......
module ProfileHelper
module StreamEntriesHelper
def display_name(account)
account.display_name.blank? ? account.username : account.display_name
end
......
......@@ -76,6 +76,10 @@ class Account < ActiveRecord::Base
@avatar_remote_url = url
end
def to_param
self.username
end
before_create do
if local?
keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048)
......
class User < ActiveRecord::Base
belongs_to :account, inverse_of: :user
validates :account, presence: true
def timeline
StreamEntry.where(account_id: self.account.following, activity_type: 'Status').order('id desc')
end