@@ -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) | |||||
"https://649841dc.ngrok.io/api#{subscriptions_path(id: account.id)}" | |||||
end | |||||
include ApplicationHelper | |||||
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 | |||||
include GrapeRouteHelpers::NamedRouteMatcher | |||||
class FollowRemoteAccountService | |||||
include ApplicationHelper | |||||
def call(user) | |||||
username, domain = user.split('@') | |||||
def call(uri) | |||||
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 |
@@ -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 |