Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
e239fc3050 | |||
d022975444 | |||
086d487145 | |||
9cb690c706 | |||
786397e15d | |||
55ca59a66c | |||
48d66a2055 | |||
2c374cd97c | |||
8767a98fbb | |||
a9db42a956 |
@ -1,5 +1,23 @@
|
||||
# Service dependencies
|
||||
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_HTTPS=true
|
||||
|
||||
# Application secrets
|
||||
PAPERCLIP_SECRET=
|
||||
SECRET_KEY_BASE=
|
||||
|
||||
# E-mail configuration
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=notifications@example.com
|
||||
|
@ -2,7 +2,7 @@ FROM ruby:2.2.4
|
||||
|
||||
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
|
||||
|
||||
WORKDIR /mastodon
|
||||
@ -13,3 +13,5 @@ ADD Gemfile.lock /mastodon/Gemfile.lock
|
||||
RUN bundle install --deployment --without test --without development
|
||||
|
||||
ADD . /mastodon
|
||||
|
||||
VOLUME ["/mastodon/public/system", "/mastodon/public/assets"]
|
||||
|
12
README.md
12
README.md
@ -21,8 +21,8 @@ Mastodon is a federated microblogging engine. An alternative implementation of t
|
||||
Missing:
|
||||
|
||||
- Media attachments (photos, videos)
|
||||
- UI to post, reblog, favourite, follow and unfollow
|
||||
- Streaming API
|
||||
- Blocking users, blocking remote instances
|
||||
|
||||
## 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
|
||||
- `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
|
||||
|
||||
- PostgreSQL
|
||||
@ -37,7 +39,7 @@ Missing:
|
||||
|
||||
## 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
|
||||
|
||||
@ -48,3 +50,9 @@ And finally
|
||||
As usual, the first thing you would need to do would be to run migrations:
|
||||
|
||||
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.
|
||||
|
@ -214,6 +214,12 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error_notification {
|
||||
color: #df405a;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 15px;
|
||||
|
||||
@ -238,7 +244,7 @@
|
||||
font-size: 14px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
|
||||
&:focus {
|
||||
&:focus, &:active {
|
||||
border-bottom: 2px solid #2b90d9;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
@ -253,6 +259,24 @@
|
||||
margin-top: 5px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ class XrdController < ApplicationController
|
||||
end
|
||||
|
||||
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}"
|
||||
@magic_key = pem_to_magic_key(@account.keypair.public_key)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@ -21,10 +21,10 @@ class XrdController < ApplicationController
|
||||
end
|
||||
|
||||
def username_from_resource
|
||||
if params[:resource].start_with?('acct:')
|
||||
params[:resource].split('@').first.gsub('acct:', '')
|
||||
if resource_param.start_with?('acct:')
|
||||
resource_param.split('@').first.gsub('acct:', '')
|
||||
else
|
||||
url = Addressable::URI.parse(params[:resource])
|
||||
url = Addressable::URI.parse(resource_param)
|
||||
url.path.gsub('/users/', '')
|
||||
end
|
||||
end
|
||||
@ -43,4 +43,8 @@ class XrdController < ApplicationController
|
||||
|
||||
(["RSA"] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
|
||||
end
|
||||
|
||||
def resource_param
|
||||
params.require(:resource)
|
||||
end
|
||||
end
|
||||
|
@ -25,7 +25,7 @@ module StreamEntriesHelper
|
||||
status.mentions.each { |m| mention_hash[m.acct] = m }
|
||||
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]]
|
||||
"#{m.split('@').first}<a href=\"#{url_for_target(account)}\" class=\"mention\">@<span>#{account.acct}</span></a>"
|
||||
end.html_safe
|
||||
|
@ -1,7 +1,7 @@
|
||||
class Account < ActiveRecord::Base
|
||||
# Local users
|
||||
has_one :user, inverse_of: :account
|
||||
validates :username, uniqueness: { scope: :domain }
|
||||
validates :username, uniqueness: { scope: :domain, case_sensitive: false }
|
||||
|
||||
# Avatar upload
|
||||
attr_reader :avatar_remote_url
|
||||
@ -12,6 +12,10 @@ class Account < ActiveRecord::Base
|
||||
has_attached_file :header, styles: { medium: '700x335#' }
|
||||
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
|
||||
has_many :stream_entries, inverse_of: :account
|
||||
has_many :statuses, inverse_of: :account
|
||||
@ -32,7 +36,8 @@ class Account < ActiveRecord::Base
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def following?(other_account)
|
||||
@ -93,6 +98,11 @@ class Account < ActiveRecord::Base
|
||||
self.username
|
||||
end
|
||||
|
||||
def self.find_local!(username)
|
||||
table = self.arel_table
|
||||
self.where(table[:username].matches(username)).where(domain: nil).take!
|
||||
end
|
||||
|
||||
before_create do
|
||||
if local?
|
||||
keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048)
|
||||
|
@ -2,7 +2,7 @@ class Favourite < ActiveRecord::Base
|
||||
belongs_to :account, 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
|
||||
:favorite
|
||||
|
@ -2,7 +2,7 @@ class Follow < ActiveRecord::Base
|
||||
belongs_to :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_id, uniqueness: { scope: :target_account_id }
|
||||
|
@ -98,7 +98,7 @@ class ProcessFeedService < BaseService
|
||||
account = Account.find_by(username: username, domain: domain)
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
|
@ -14,7 +14,7 @@ class ProcessInteractionService < BaseService
|
||||
account = Account.find_by(username: username, domain: domain)
|
||||
|
||||
if account.nil?
|
||||
account = follow_remote_account_service.("acct:#{username}@#{domain}", false)
|
||||
account = follow_remote_account_service.("#{username}@#{domain}", false)
|
||||
return if account.nil?
|
||||
end
|
||||
|
||||
@ -48,7 +48,7 @@ class ProcessInteractionService < BaseService
|
||||
end
|
||||
|
||||
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
|
||||
:post
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class ProcessMentionsService < BaseService
|
||||
mentioned_account = Account.find_by(username: username, domain: domain)
|
||||
|
||||
if mentioned_account.nil?
|
||||
mentioned_account = follow_remote_account_service.("acct:#{match.first}")
|
||||
mentioned_account = follow_remote_account_service.("#{match.first}")
|
||||
end
|
||||
|
||||
mentioned_account.mentions.first_or_create(status: status)
|
||||
|
@ -17,7 +17,8 @@ test:
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
database: postgres
|
||||
username: postgres
|
||||
password:
|
||||
host: db
|
||||
database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
|
||||
username: <%= ENV['DB_USER'] || 'mastodon' %>
|
||||
password: <%= ENV['DB_PASS'] || '' %>
|
||||
host: <%= ENV['DB_HOST'] || 'localhost' %>
|
||||
port: <%= ENV['DB_PORT'] || 5432 %>
|
||||
|
@ -22,7 +22,7 @@ Rails.application.configure do
|
||||
|
||||
# Disable serving static files from the `/public` folder by default since
|
||||
# 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.
|
||||
config.assets.js_compressor = :uglifier
|
||||
@ -42,7 +42,7 @@ Rails.application.configure do
|
||||
# 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.
|
||||
# config.force_ssl = true
|
||||
config.force_ssl = ENV['LOCAL_HTTPS'] == 'true'
|
||||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
@ -76,4 +76,16 @@ Rails.application.configure do
|
||||
|
||||
# Do not dump schema after migrations.
|
||||
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
|
||||
|
@ -12,7 +12,7 @@ Devise.setup do |config|
|
||||
# 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
|
||||
# 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.
|
||||
# config.mailer = 'Devise::Mailer'
|
||||
|
@ -23,7 +23,7 @@ Doorkeeper.configure do
|
||||
|
||||
# Access token expiration time (default 2 hours).
|
||||
# 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.
|
||||
# custom_access_token_expires_in do |oauth_client|
|
||||
|
10
db/migrate/20160316103650_add_missing_indices.rb
Normal file
10
db/migrate/20160316103650_add_missing_indices.rb
Normal 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
|
@ -11,7 +11,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
@ -125,6 +125,9 @@ ActiveRecord::Schema.define(version: 20160314164231) do
|
||||
t.string "url"
|
||||
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
|
||||
|
||||
create_table "stream_entries", force: :cascade do |t|
|
||||
@ -135,6 +138,9 @@ ActiveRecord::Schema.define(version: 20160314164231) do
|
||||
t.datetime "updated_at", null: false
|
||||
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|
|
||||
t.string "email", default: "", null: false
|
||||
t.integer "account_id", null: false
|
||||
@ -151,6 +157,7 @@ ActiveRecord::Schema.define(version: 20160314164231) do
|
||||
t.inet "last_sign_in_ip"
|
||||
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", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
|
||||
|
||||
|
@ -12,4 +12,7 @@ services:
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
volumes:
|
||||
- ./public/assets:/mastodon/public/assets
|
||||
- ./public/system:/mastodon/public/system
|
||||
env_file: .env.production
|
||||
|
Reference in New Issue
Block a user