10 Commits

19 changed files with 125 additions and 26 deletions

View File

@ -1,5 +1,23 @@
# Service dependencies
REDIS_HOST=redis REDIS_HOST=redis
REDIS_PORT=6379
DB_HOST=db
DB_USER=postgres
DB_NAME=postgres
DB_PASS=
DB_PORT=5432
# Federation
LOCAL_DOMAIN=example.com LOCAL_DOMAIN=example.com
LOCAL_HTTPS=true LOCAL_HTTPS=true
# Application secrets
PAPERCLIP_SECRET= PAPERCLIP_SECRET=
SECRET_KEY_BASE= SECRET_KEY_BASE=
# E-mail configuration
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com

View File

@ -2,7 +2,7 @@ FROM ruby:2.2.4
ENV RAILS_ENV=production ENV RAILS_ENV=production
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev RUN apt-get update -qq && apt-get install -y build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
RUN mkdir /mastodon RUN mkdir /mastodon
WORKDIR /mastodon WORKDIR /mastodon
@ -13,3 +13,5 @@ ADD Gemfile.lock /mastodon/Gemfile.lock
RUN bundle install --deployment --without test --without development RUN bundle install --deployment --without test --without development
ADD . /mastodon ADD . /mastodon
VOLUME ["/mastodon/public/system", "/mastodon/public/assets"]

View File

@ -21,8 +21,8 @@ Mastodon is a federated microblogging engine. An alternative implementation of t
Missing: Missing:
- Media attachments (photos, videos) - Media attachments (photos, videos)
- UI to post, reblog, favourite, follow and unfollow
- Streaming API - Streaming API
- Blocking users, blocking remote instances
## Configuration ## Configuration
@ -30,6 +30,8 @@ Missing:
- `LOCAL_HTTPS` set it to `true` if HTTPS works on your website. This is used to generate canonical URLs, which is also important when generating and parsing federation-related IDs - `LOCAL_HTTPS` set it to `true` if HTTPS works on your website. This is used to generate canonical URLs, which is also important when generating and parsing federation-related IDs
- `HUB_URL` should be the URL of the PubsubHubbub service that your instance is going to use. By default it is the open service of Superfeedr - `HUB_URL` should be the URL of the PubsubHubbub service that your instance is going to use. By default it is the open service of Superfeedr
Consult the example configuration file, `.env.production.sample` for the full list.
## Requirements ## Requirements
- PostgreSQL - PostgreSQL
@ -37,7 +39,7 @@ Missing:
## Running with Docker and Docker-Compose ## Running with Docker and Docker-Compose
The project now includes a Dockerfile and a docker-compose.yml. You need to turn .env.production sample into .env.production with all the variables set before you can: The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
docker-compose build docker-compose build
@ -48,3 +50,9 @@ And finally
As usual, the first thing you would need to do would be to run migrations: As usual, the first thing you would need to do would be to run migrations:
docker-compose run web rake db:migrate docker-compose run web rake db:migrate
And since the instance running in the container will be running in production mode, you need to pre-compile assets:
docker-compose run web rake assets:precompile
The container has two volumes, for the assets and for user uploads. The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.

View File

@ -214,6 +214,12 @@
text-align: center; text-align: center;
} }
.error_notification {
color: #df405a;
font-weight: 500;
margin-bottom: 15px;
}
.input { .input {
margin-bottom: 15px; margin-bottom: 15px;
@ -238,7 +244,7 @@
font-size: 14px; font-size: 14px;
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
&:focus { &:focus, &:active {
border-bottom: 2px solid #2b90d9; border-bottom: 2px solid #2b90d9;
padding-bottom: 5px; padding-bottom: 5px;
} }
@ -253,6 +259,24 @@
margin-top: 5px; margin-top: 5px;
color: lighten(#282c37, 25%); color: lighten(#282c37, 25%);
} }
&.field_with_errors {
input[type=text], input[type=email], input[type=password], textarea {
border-bottom: 2px solid #df405a;
padding-bottom: 5px;
&:focus, &:active {
border-bottom: 2px solid #2b90d9;
padding-bottom: 5px;
}
}
.error {
display: block;
margin-top: 5px;
color: #df405a;
}
}
} }
} }

View File

@ -6,7 +6,7 @@ class XrdController < ApplicationController
end end
def webfinger def webfinger
@account = Account.find_by!(username: username_from_resource, domain: nil) @account = Account.find_local!(username_from_resource)
@canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}" @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}"
@magic_key = pem_to_magic_key(@account.keypair.public_key) @magic_key = pem_to_magic_key(@account.keypair.public_key)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
@ -21,10 +21,10 @@ class XrdController < ApplicationController
end end
def username_from_resource def username_from_resource
if params[:resource].start_with?('acct:') if resource_param.start_with?('acct:')
params[:resource].split('@').first.gsub('acct:', '') resource_param.split('@').first.gsub('acct:', '')
else else
url = Addressable::URI.parse(params[:resource]) url = Addressable::URI.parse(resource_param)
url.path.gsub('/users/', '') url.path.gsub('/users/', '')
end end
end end
@ -43,4 +43,8 @@ class XrdController < ApplicationController
(["RSA"] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.') (["RSA"] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
end end
def resource_param
params.require(:resource)
end
end end

View File

@ -25,7 +25,7 @@ module StreamEntriesHelper
status.mentions.each { |m| mention_hash[m.acct] = m } status.mentions.each { |m| mention_hash[m.acct] = m }
coder = HTMLEntities.new coder = HTMLEntities.new
auto_link(coder.encode(status.text), link: :urls, html: { target: '_blank', rel: 'nofollow' }).gsub(Account::MENTION_RE) do |m| auto_link(coder.encode(status.text), link: :urls, html: { rel: 'nofollow noopener' }).gsub(Account::MENTION_RE) do |m|
account = mention_hash[Account::MENTION_RE.match(m)[1]] account = mention_hash[Account::MENTION_RE.match(m)[1]]
"#{m.split('@').first}<a href=\"#{url_for_target(account)}\" class=\"mention\">@<span>#{account.acct}</span></a>" "#{m.split('@').first}<a href=\"#{url_for_target(account)}\" class=\"mention\">@<span>#{account.acct}</span></a>"
end.html_safe end.html_safe

View File

@ -1,7 +1,7 @@
class Account < ActiveRecord::Base class Account < ActiveRecord::Base
# Local users # Local users
has_one :user, inverse_of: :account has_one :user, inverse_of: :account
validates :username, uniqueness: { scope: :domain } validates :username, uniqueness: { scope: :domain, case_sensitive: false }
# Avatar upload # Avatar upload
attr_reader :avatar_remote_url attr_reader :avatar_remote_url
@ -12,6 +12,10 @@ class Account < ActiveRecord::Base
has_attached_file :header, styles: { medium: '700x335#' } has_attached_file :header, styles: { medium: '700x335#' }
validates_attachment_content_type :header, content_type: /\Aimage\/.*\Z/ validates_attachment_content_type :header, content_type: /\Aimage\/.*\Z/
# Local user profile validations
validates :display_name, length: { maximum: 30 }, if: 'local?'
validates :note, length: { maximum: 124 }, if: 'local?'
# Timelines # Timelines
has_many :stream_entries, inverse_of: :account has_many :stream_entries, inverse_of: :account
has_many :statuses, inverse_of: :account has_many :statuses, inverse_of: :account
@ -32,7 +36,8 @@ class Account < ActiveRecord::Base
end end
def unfollow!(other_account) def unfollow!(other_account)
self.active_relationships.find_by(target_account: other_account).destroy follow = self.active_relationships.find_by(target_account: other_account)
follow.destroy unless follow.nil?
end end
def following?(other_account) def following?(other_account)
@ -93,6 +98,11 @@ class Account < ActiveRecord::Base
self.username self.username
end end
def self.find_local!(username)
table = self.arel_table
self.where(table[:username].matches(username)).where(domain: nil).take!
end
before_create do before_create do
if local? if local?
keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048) keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048)

View File

@ -2,7 +2,7 @@ class Favourite < ActiveRecord::Base
belongs_to :account, inverse_of: :favourites belongs_to :account, inverse_of: :favourites
belongs_to :status, inverse_of: :favourites belongs_to :status, inverse_of: :favourites
has_one :stream_entry, as: :activity, dependent: :destroy has_one :stream_entry, as: :activity
def verb def verb
:favorite :favorite

View File

@ -2,7 +2,7 @@ class Follow < ActiveRecord::Base
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'
has_one :stream_entry, as: :activity, dependent: :destroy has_one :stream_entry, as: :activity
validates :account, :target_account, presence: true validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }

View File

@ -98,7 +98,7 @@ class ProcessFeedService < BaseService
account = Account.find_by(username: username, domain: domain) account = Account.find_by(username: username, domain: domain)
if account.nil? if account.nil?
account = follow_remote_account_service.("acct:#{username}@#{domain}", false) account = follow_remote_account_service.("#{username}@#{domain}", false)
return nil if account.nil? return nil if account.nil?
end end

View File

@ -14,7 +14,7 @@ class ProcessInteractionService < BaseService
account = Account.find_by(username: username, domain: domain) account = Account.find_by(username: username, domain: domain)
if account.nil? if account.nil?
account = follow_remote_account_service.("acct:#{username}@#{domain}", false) account = follow_remote_account_service.("#{username}@#{domain}", false)
return if account.nil? return if account.nil?
end end
@ -48,7 +48,7 @@ class ProcessInteractionService < BaseService
end end
def verb(xml) def verb(xml)
xml.at_xpath('//activity:verb').content.gsub('http://activitystrea.ms/schema/1.0/', '').to_sym xml.at_xpath('//activity:verb').content.gsub('http://activitystrea.ms/schema/1.0/', '').gsub('http://ostatus.org/schema/1.0/', '').to_sym
rescue rescue
:post :post
end end

View File

@ -11,7 +11,7 @@ class ProcessMentionsService < BaseService
mentioned_account = Account.find_by(username: username, domain: domain) mentioned_account = Account.find_by(username: username, domain: domain)
if mentioned_account.nil? if mentioned_account.nil?
mentioned_account = follow_remote_account_service.("acct:#{match.first}") mentioned_account = follow_remote_account_service.("#{match.first}")
end end
mentioned_account.mentions.first_or_create(status: status) mentioned_account.mentions.first_or_create(status: status)

View File

@ -17,7 +17,8 @@ test:
production: production:
<<: *default <<: *default
database: postgres database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
username: postgres username: <%= ENV['DB_USER'] || 'mastodon' %>
password: password: <%= ENV['DB_PASS'] || '' %>
host: db host: <%= ENV['DB_HOST'] || 'localhost' %>
port: <%= ENV['DB_PORT'] || 5432 %>

View File

@ -22,7 +22,7 @@ Rails.application.configure do
# Disable serving static files from the `/public` folder by default since # Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this. # Apache or NGINX already handles this.
config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? config.serve_static_files = true
# Compress JavaScripts and CSS. # Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier config.assets.js_compressor = :uglifier
@ -42,7 +42,7 @@ Rails.application.configure do
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true config.force_ssl = ENV['LOCAL_HTTPS'] == 'true'
# Use the lowest log level to ensure availability of diagnostic information # Use the lowest log level to ensure availability of diagnostic information
# when problems arise. # when problems arise.
@ -76,4 +76,16 @@ Rails.application.configure do
# Do not dump schema after migrations. # Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false config.active_record.dump_schema_after_migration = false
# E-mails
config.action_mailer.smtp_settings = {
:port => ENV['SMTP_PORT'],
:address => ENV['SMTP_SERVER'],
:user_name => ENV['SMTP_LOGIN'],
:password => ENV['SMTP_PASSWORD'],
:domain => config.x.local_domain,
:authentication => :plain,
}
config.action_mailer.delivery_method = :smtp
end end

View File

@ -12,7 +12,7 @@ Devise.setup do |config|
# Configure the e-mail address which will be shown in Devise::Mailer, # Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class # note that it will be overwritten if you use your own mailer class
# with default "from" parameter. # with default "from" parameter.
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' config.mailer_sender = ENV['SMTP_FROM_ADDRESS'] || 'notifications@localhost'
# Configure the class responsible to send e-mails. # Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer' # config.mailer = 'Devise::Mailer'

View File

@ -23,7 +23,7 @@ Doorkeeper.configure do
# Access token expiration time (default 2 hours). # Access token expiration time (default 2 hours).
# If you want to disable expiration, set this to nil. # If you want to disable expiration, set this to nil.
# access_token_expires_in nil access_token_expires_in nil
# Assign a custom TTL for implicit grants. # Assign a custom TTL for implicit grants.
# custom_access_token_expires_in do |oauth_client| # custom_access_token_expires_in do |oauth_client|

View File

@ -0,0 +1,10 @@
class AddMissingIndices < ActiveRecord::Migration
def change
add_index :users, :account_id
add_index :statuses, :account_id
add_index :statuses, :in_reply_to_id
add_index :statuses, :reblog_of_id
add_index :stream_entries, :account_id
add_index :stream_entries, [:activity_id, :activity_type]
end
end

View File

@ -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: 20160314164231) do ActiveRecord::Schema.define(version: 20160316103650) 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"
@ -125,6 +125,9 @@ ActiveRecord::Schema.define(version: 20160314164231) do
t.string "url" t.string "url"
end end
add_index "statuses", ["account_id"], name: "index_statuses_on_account_id", using: :btree
add_index "statuses", ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", using: :btree
add_index "statuses", ["reblog_of_id"], name: "index_statuses_on_reblog_of_id", using: :btree
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| create_table "stream_entries", force: :cascade do |t|
@ -135,6 +138,9 @@ ActiveRecord::Schema.define(version: 20160314164231) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "stream_entries", ["account_id"], name: "index_stream_entries_on_account_id", using: :btree
add_index "stream_entries", ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type", using: :btree
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false t.string "email", default: "", null: false
t.integer "account_id", null: false t.integer "account_id", null: false
@ -151,6 +157,7 @@ ActiveRecord::Schema.define(version: 20160314164231) do
t.inet "last_sign_in_ip" t.inet "last_sign_in_ip"
end end
add_index "users", ["account_id"], name: "index_users_on_account_id", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree

View File

@ -12,4 +12,7 @@ services:
depends_on: depends_on:
- db - db
- redis - redis
volumes:
- ./public/assets:/mastodon/public/assets
- ./public/system:/mastodon/public/system
env_file: .env.production env_file: .env.production