Merge branch 'master' into master

This commit is contained in:
André Lewin 2017-04-05 20:28:58 +02:00 committed by GitHub
commit bf7cefa516
117 changed files with 1783 additions and 276 deletions

2
.buildpacks Normal file
View file

@ -0,0 +1,2 @@
https://github.com/Scalingo/nodejs-buildpack
https://github.com/Scalingo/ruby-buildpack

View file

@ -22,6 +22,8 @@ OTP_SECRET=
# SINGLE_USER_MODE=true # SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains # Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc # EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# E-mail configuration # E-mail configuration
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org

2
.slugignore Normal file
View file

@ -0,0 +1,2 @@
node_modules/
.cache/

View file

@ -8,8 +8,6 @@ gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0' gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails' gem 'jquery-rails'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'puma' gem 'puma'
gem 'hamlit-rails' gem 'hamlit-rails'
@ -38,7 +36,7 @@ gem 'rqrcode'
gem 'twitter-text' gem 'twitter-text'
gem 'oj' gem 'oj'
gem 'hiredis' gem 'hiredis'
gem 'redis', '~>3.2' gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
gem 'fast_blank' gem 'fast_blank'
gem 'htmlentities' gem 'htmlentities'
gem 'simple_form' gem 'simple_form'
@ -46,6 +44,7 @@ gem 'will_paginate'
gem 'rack-attack' gem 'rack-attack'
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'sidekiq' gem 'sidekiq'
gem 'sidekiq-unique-jobs'
gem 'rails-settings-cached' gem 'rails-settings-cached'
gem 'simple-navigation' gem 'simple-navigation'
gem 'statsd-instrument' gem 'statsd-instrument'
@ -66,9 +65,10 @@ group :development, :test do
end end
group :test do group :test do
gem 'faker'
gem 'rspec-sidekiq'
gem 'simplecov', require: false gem 'simplecov', require: false
gem 'webmock' gem 'webmock'
gem 'rspec-sidekiq'
end end
group :development do group :development do

View file

@ -149,6 +149,8 @@ GEM
erubis (2.7.0) erubis (2.7.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.15.2) fabrication (2.15.2)
faker (1.6.6)
i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
font-awesome-rails (4.6.3.1) font-awesome-rails (4.6.3.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
@ -196,9 +198,6 @@ GEM
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
term-ansicolor (>= 1.3.2) term-ansicolor (>= 1.3.2)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
jbuilder (2.6.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jmespath (1.3.1) jmespath (1.3.1)
jquery-rails (4.1.1) jquery-rails (4.1.1)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
@ -229,7 +228,6 @@ GEM
mimemagic (0.3.2) mimemagic (0.3.2)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.10.1) minitest (5.10.1)
multi_json (1.12.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.0.1) net-ssh (4.0.1)
@ -308,8 +306,6 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.1.0)
rake (12.0.0) rake (12.0.0)
rdoc (4.2.2)
json (~> 1.4)
react-rails (1.10.0) react-rails (1.10.0)
babel-transpiler (>= 0.7.0) babel-transpiler (>= 0.7.0)
coffee-script-source (~> 1.8) coffee-script-source (~> 1.8)
@ -379,14 +375,14 @@ GEM
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0) sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3) tilt (>= 1.1, < 3)
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
sidekiq (4.2.7) sidekiq (4.2.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
sidekiq-unique-jobs (4.0.18)
sidekiq (>= 2.6)
thor
simple-navigation (4.0.3) simple-navigation (4.0.3)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (3.2.1) simple_form (3.2.1)
@ -467,6 +463,7 @@ DEPENDENCIES
doorkeeper doorkeeper
dotenv-rails dotenv-rails
fabrication fabrication
faker
fast_blank fast_blank
font-awesome-rails font-awesome-rails
fuubar fuubar
@ -477,7 +474,6 @@ DEPENDENCIES
http http
httplog httplog
i18n-tasks (~> 0.9.6) i18n-tasks (~> 0.9.6)
jbuilder (~> 2.0)
jquery-rails jquery-rails
letter_opener letter_opener
letter_opener_web letter_opener_web
@ -508,8 +504,8 @@ DEPENDENCIES
rubocop rubocop
ruby-oembed ruby-oembed
sass-rails (~> 5.0) sass-rails (~> 5.0)
sdoc (~> 0.4.0)
sidekiq sidekiq
sidekiq-unique-jobs
simple-navigation simple-navigation
simple_form simple_form
simplecov simplecov

5
ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,5 @@
[Issue text goes here].
* * * *
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.

View file

@ -117,6 +117,12 @@ Which will re-create the updated containers, leaving databases and data as is. D
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions. Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
## Deployment on Scalingo
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
[You can view a guide for deployment on Scalingo here.](docs/Running-Mastodon/Scalingo-guide.md)
## Deployment on Heroku (experimental) ## Deployment on Heroku (experimental)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -46,6 +46,7 @@ import fr from 'react-intl/locale-data/fr';
import pt from 'react-intl/locale-data/pt'; import pt from 'react-intl/locale-data/pt';
import hu from 'react-intl/locale-data/hu'; import hu from 'react-intl/locale-data/hu';
import uk from 'react-intl/locale-data/uk'; import uk from 'react-intl/locale-data/uk';
import fi from 'react-intl/locale-data/fi';
import eo from 'react-intl/locale-data/eo'; import eo from 'react-intl/locale-data/eo';
import getMessagesForLocale from '../locales'; import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
@ -59,7 +60,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web' basename: '/web'
}); });
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...eo]); addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
const Mastodon = React.createClass({ const Mastodon = React.createClass({

View file

@ -43,7 +43,7 @@ const GettingStarted = ({ intl, me }) => {
<div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}> <div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
<div className='static-content getting-started'> <div className='static-content getting-started'>
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p> <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
</div> </div>
</div> </div>
</Column> </Column>

View file

@ -9,7 +9,7 @@ const iconStyle = {
}; };
const ClearColumnButton = ({ onClick }) => ( const ClearColumnButton = ({ onClick }) => (
<div className='column-icon' style={iconStyle} onClick={onClick}> <div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}>
<i className='fa fa-trash' /> <i className='fa fa-trash' />
</div> </div>
); );

View file

@ -25,7 +25,7 @@ const en = {
"getting_started.heading": "Getting started", "getting_started.heading": "Getting started",
"getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.", "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
"getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.", "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.", "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
"column.home": "Home", "column.home": "Home",
"column.community": "Local timeline", "column.community": "Local timeline",
"column.public": "Federated timeline", "column.public": "Federated timeline",

View file

@ -0,0 +1,68 @@
const fi = {
"column_back_button.label": "Takaisin",
"lightbox.close": "Sulje",
"loading_indicator.label": "Ladataan...",
"status.mention": "Mainitse @{name}",
"status.delete": "Poista",
"status.reply": "Vastaa",
"status.reblog": "Buustaa",
"status.favourite": "Tykkää",
"status.reblogged_by": "{name} buustasi",
"status.sensitive_warning": "Arkaluontoista sisältöä",
"status.sensitive_toggle": "Klikkaa nähdäksesi",
"video_player.toggle_sound": "Äänet päälle/pois",
"account.mention": "Mainitse @{name}",
"account.edit_profile": "Muokkaa",
"account.unblock": "Salli @{name}",
"account.unfollow": "Lopeta seuraaminen",
"account.block": "Estä @{name}",
"account.follow": "Seuraa",
"account.posts": "Postit",
"account.follows": "Seuraa",
"account.followers": "Seuraajia",
"account.follows_you": "Seuraa sinua",
"account.requested": "Odottaa hyväksyntää",
"getting_started.heading": "Aloitus",
"getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.",
"getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi",
"getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}. {apps}.",
"column.home": "Koti",
"column.community": "Paikallinen aikajana",
"column.public": "Yleinen aikajana",
"column.notifications": "Ilmoitukset",
"tabs_bar.compose": "Luo",
"tabs_bar.home": "Koti",
"tabs_bar.mentions": "Maininnat",
"tabs_bar.public": "Yleinen aikajana",
"tabs_bar.notifications": "Ilmoitukset",
"compose_form.placeholder": "Mitä sinulla on mielessä?",
"compose_form.publish": "Toot",
"compose_form.sensitive": "Merkitse media herkäksi",
"compose_form.spoiler": "Piiloita teksti varoituksen taakse",
"compose_form.private": "Merkitse yksityiseksi",
"compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.",
"compose_form.unlisted": "Älä näytä yleisillä aikajanoilla",
"navigation_bar.edit_profile": "Muokkaa profiilia",
"navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.community_timeline": "Paikallinen aikajana",
"navigation_bar.public_timeline": "Yleinen aikajana",
"navigation_bar.logout": "Kirjaudu ulos",
"reply_indicator.cancel": "Peruuta",
"search.placeholder": "Hae",
"search.account": "Tili",
"search.hashtag": "Hashtag",
"upload_button.label": "Lisää mediaa",
"upload_form.undo": "Peru",
"notification.follow": "{name} seurasi sinua",
"notification.favourite": "{name} tykkäsi statuksestasi",
"notification.reblog": "{name} buustasi statustasi",
"notification.mention": "{name} mainitsi sinut",
"notifications.column_settings.alert": "Työpöytä ilmoitukset",
"notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.follow": "Uusia seuraajia:",
"notifications.column_settings.favourite": "Tykkäyksiä:",
"notifications.column_settings.mention": "Mainintoja:",
"notifications.column_settings.reblog": "Buusteja:",
};
export default fi;

View file

@ -5,6 +5,7 @@ import hu from './hu';
import fr from './fr'; import fr from './fr';
import pt from './pt'; import pt from './pt';
import uk from './uk'; import uk from './uk';
import fi from './fi';
import eo from './eo'; import eo from './eo';
const locales = { const locales = {
@ -15,6 +16,7 @@ const locales = {
fr, fr,
pt, pt,
uk, uk,
fi,
eo eo
}; };

View file

@ -319,7 +319,7 @@
} }
} }
.simple_form { .simple_form, .closed-registrations-message {
width: 300px; width: 300px;
flex: 0 0 auto; flex: 0 0 auto;
background: rgba(darken($color1, 7%), 0.5); background: rgba(darken($color1, 7%), 0.5);
@ -340,3 +340,11 @@
} }
} }
} }
.closed-registrations-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}

View file

@ -34,6 +34,7 @@
text-align: center; text-align: center;
position: relative; position: relative;
z-index: 2; z-index: 2;
text-shadow: 0 0 2px $color8;
small { small {
display: block; display: block;
@ -128,6 +129,7 @@
text-transform: uppercase; text-transform: uppercase;
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
text-shadow: 0 0 2px $color8;
} }
.counter-number { .counter-number {
@ -385,5 +387,6 @@
.account__header__content { .account__header__content {
font-size: 14px; font-size: 14px;
color: $color1; color: $color1;
text-shadow: 0 0 2px $color8;
} }
} }

View file

@ -4,7 +4,9 @@ class AboutController < ApplicationController
before_action :set_body_classes before_action :set_body_classes
def index def index
@description = Setting.site_description @description = Setting.site_description
@open_registrations = Setting.open_registrations
@closed_registrations_message = Setting.closed_registrations_message
@user = User.new @user = User.new
@user.build_account @user.build_account

View file

@ -9,6 +9,24 @@ class Admin::DomainBlocksController < ApplicationController
@blocks = DomainBlock.paginate(page: params[:page], per_page: 40) @blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
end end
def new
@domain_block = DomainBlock.new
end
def create def create
@domain_block = DomainBlock.new(resource_params)
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
else
render action: :new
end
end
private
def resource_params
params.require(:domain_block).permit(:domain, :severity)
end end
end end

View file

@ -16,19 +16,19 @@ class Admin::ReportsController < ApplicationController
end end
def resolve def resolve
@report.update(action_taken: true) @report.update(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def suspend def suspend
Admin::SuspensionWorker.perform_async(@report.target_account.id) Admin::SuspensionWorker.perform_async(@report.target_account.id)
@report.update(action_taken: true) Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def silence def silence
@report.target_account.update(silenced: true) @report.target_account.update(silenced: true)
@report.update(action_taken: true) Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View file

@ -11,9 +11,13 @@ class Admin::SettingsController < ApplicationController
def update def update
@setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
value = settings_params[:value]
if @setting.value != params[:setting][:value] # Special cases
@setting.value = params[:setting][:value] value = value == 'true' if @setting.var == 'open_registrations'
if @setting.value != value
@setting.value = value
@setting.save @setting.save
end end
@ -22,4 +26,10 @@ class Admin::SettingsController < ApplicationController
format.json { respond_with_bip(@setting) } format.json { respond_with_bip(@setting) }
end end
end end
private
def settings_params
params.require(:setting).permit(:value)
end
end end

View file

@ -4,6 +4,12 @@ class Api::V1::AppsController < ApiController
respond_to :json respond_to :json
def create def create
@app = Doorkeeper::Application.create!(name: params[:client_name], redirect_uri: params[:redirect_uris], scopes: (params[:scopes] || Doorkeeper.configuration.default_scopes), website: params[:website]) @app = Doorkeeper::Application.create!(name: app_params[:client_name], redirect_uri: app_params[:redirect_uris], scopes: (app_params[:scopes] || Doorkeeper.configuration.default_scopes), website: app_params[:website])
end
private
def app_params
params.permit(:client_name, :redirect_uris, :scopes, :website)
end end
end end

View file

@ -7,7 +7,7 @@ class Api::V1::FollowsController < ApiController
respond_to :json respond_to :json
def create def create
raise ActiveRecord::RecordNotFound if params[:uri].blank? raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account) @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
render action: :show render action: :show
@ -16,6 +16,10 @@ class Api::V1::FollowsController < ApiController
private private
def target_uri def target_uri
params[:uri].strip.gsub(/\A@/, '') follow_params[:uri].strip.gsub(/\A@/, '')
end
def follow_params
params.permit(:uri)
end end
end end

View file

@ -10,10 +10,16 @@ class Api::V1::MediaController < ApiController
respond_to :json respond_to :json
def create def create
@media = MediaAttachment.create!(account: current_user.account, file: params[:file]) @media = MediaAttachment.create!(account: current_user.account, file: media_params[:file])
rescue Paperclip::Errors::NotIdentifiedByImageMagickError rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: { error: 'File type of uploaded media could not be verified' }, status: 422 render json: { error: 'File type of uploaded media could not be verified' }, status: 422
rescue Paperclip::Error rescue Paperclip::Error
render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500 render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500
end end
private
def media_params
params.permit(:file)
end
end end

View file

@ -12,13 +12,19 @@ class Api::V1::ReportsController < ApiController
end end
def create def create
status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]] status_ids = report_params[:status_ids].is_a?(Enumerable) ? report_params[:status_ids] : [report_params[:status_ids]]
@report = Report.create!(account: current_account, @report = Report.create!(account: current_account,
target_account: Account.find(params[:account_id]), target_account: Account.find(report_params[:account_id]),
status_ids: Status.find(status_ids).pluck(:id), status_ids: Status.find(status_ids).pluck(:id),
comment: params[:comment]) comment: report_params[:comment])
render :show render :show
end end
private
def report_params
params.permit(:account_id, :comment, status_ids: [])
end
end end

View file

@ -62,11 +62,11 @@ class Api::V1::StatusesController < ApiController
end end
def create def create
@status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], @status = PostStatusService.new.call(current_user.account, status_params[:status], status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), media_ids: status_params[:media_ids],
sensitive: params[:sensitive], sensitive: status_params[:sensitive],
spoiler_text: params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
visibility: params[:visibility], visibility: status_params[:visibility],
application: doorkeeper_token.application) application: doorkeeper_token.application)
render action: :show render action: :show
end end
@ -111,4 +111,8 @@ class Api::V1::StatusesController < ApiController
@status = Status.find(params[:id]) @status = Status.find(params[:id])
raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account) raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
end end
def status_params
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: [])
end
end end

View file

@ -39,7 +39,14 @@ class ApplicationController < ActionController::Base
end end
def set_user_activity def set_user_activity
current_user.touch(:current_sign_in_at) if !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago) return unless !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago)
# Mark user as signed-in today
current_user.update_tracked_fields(request)
# If the sign in is after a two week break, we need to regenerate their feed
RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago
return
end end
def check_suspension def check_suspension

View file

@ -3,7 +3,7 @@
class Auth::RegistrationsController < Devise::RegistrationsController class Auth::RegistrationsController < Devise::RegistrationsController
layout :determine_layout layout :determine_layout
before_action :check_single_user_mode before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
protected protected
@ -27,8 +27,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
new_user_session_path new_user_session_path
end end
def check_single_user_mode def check_enabled_registrations
redirect_to root_path if Rails.configuration.x.single_user_mode redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
end end
private private

View file

@ -3,6 +3,7 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
skip_before_action :authenticate_resource_owner! skip_before_action :authenticate_resource_owner!
before_action :set_locale
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
@ -11,4 +12,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
rescue I18n::InvalidLocale
I18n.locale = I18n.default_locale
end
end end

View file

@ -8,6 +8,7 @@ class RemoteFollowController < ApplicationController
def new def new
@remote_follow = RemoteFollow.new @remote_follow = RemoteFollow.new
@remote_follow.acct = session[:remote_follow] if session.key?(:remote_follow)
end end
def create def create
@ -22,6 +23,8 @@ class RemoteFollowController < ApplicationController
render(:new) && return render(:new) && return
end end
session[:remote_follow] = @remote_follow.acct
redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
else else
render :new render :new

View file

@ -10,6 +10,7 @@ module SettingsHelper
hu: 'Magyar', hu: 'Magyar',
uk: 'Українська', uk: 'Українська',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
fi: 'Suomi',
eo: 'Esperanto', eo: 'Esperanto',
}.freeze }.freeze

View file

@ -2,17 +2,30 @@
class EmailValidator < ActiveModel::EachValidator class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
return if Rails.configuration.x.email_domains_blacklist.empty?
record.errors.add(attribute, I18n.t('users.invalid_email')) if blocked_email?(value) record.errors.add(attribute, I18n.t('users.invalid_email')) if blocked_email?(value)
end end
private private
def blocked_email?(value) def blocked_email?(value)
on_blacklist?(value) || not_on_whitelist?(value)
end
def on_blacklist?(value)
return false if Rails.configuration.x.email_domains_blacklist.blank?
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.') domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
regexp = Regexp.new("@(.+\\.)?(#{domains})", true) regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
value =~ regexp value =~ regexp
end end
def not_on_whitelist?(value)
return false if Rails.configuration.x.email_domains_whitelist.blank?
domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.')
regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
value !~ regexp
end
end end

View file

@ -4,4 +4,5 @@ module Mastodon
class Error < StandardError; end class Error < StandardError; end
class NotPermittedError < Error; end class NotPermittedError < Error; end
class ValidationError < Error; end class ValidationError < Error; end
class RaceConditionError < Error; end
end end

View file

@ -5,17 +5,17 @@ require 'singleton'
class FeedManager class FeedManager
include Singleton include Singleton
MAX_ITEMS = 800 MAX_ITEMS = 400
def key(type, id) def key(type, id)
"feed:#{type}:#{id}" "feed:#{type}:#{id}"
end end
def filter?(timeline_type, status, receiver) def filter?(timeline_type, status, receiver_id)
if timeline_type == :home if timeline_type == :home
filter_from_home?(status, receiver) filter_from_home?(status, receiver_id)
elsif timeline_type == :mentions elsif timeline_type == :mentions
filter_from_mentions?(status, receiver) filter_from_mentions?(status, receiver_id)
else else
false false
end end
@ -34,12 +34,7 @@ class FeedManager
trim(timeline_type, account.id) trim(timeline_type, account.id)
end end
broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) PushUpdateWorker.perform_async(account.id, status.id)
end
def broadcast(timeline_id, options = {})
options[:queued_at] = (Time.now.to_f * 1000.0).to_i
ActionCable.server.broadcast("timeline:#{timeline_id}", options)
end end
def trim(type, account_id) def trim(type, account_id)
@ -50,10 +45,18 @@ class FeedManager
def merge_into_timeline(from_account, into_account) def merge_into_timeline(from_account, into_account)
timeline_key = key(:home, into_account.id) timeline_key = key(:home, into_account.id)
query = from_account.statuses.limit(FeedManager::MAX_ITEMS / 4)
from_account.statuses.limit(MAX_ITEMS).each do |status| if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
next if status.direct_visibility? || filter?(:home, status, into_account) oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
redis.zadd(timeline_key, status.id, status.id) query = query.where('id > ?', oldest_home_score)
end
redis.pipelined do
query.each do |status|
next if status.direct_visibility? || filter?(:home, status, into_account)
redis.zadd(timeline_key, status.id, status.id)
end
end end
trim(:home, into_account.id) trim(:home, into_account.id)
@ -61,31 +64,16 @@ class FeedManager
def unmerge_from_timeline(from_account, into_account) def unmerge_from_timeline(from_account, into_account)
timeline_key = key(:home, into_account.id) timeline_key = key(:home, into_account.id)
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
from_account.statuses.select('id').find_each do |status| from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses|
redis.zrem(timeline_key, status.id) redis.pipelined do
redis.zremrangebyscore(timeline_key, status.id, status.id) statuses.each do |status|
end redis.zrem(timeline_key, status.id)
end redis.zremrangebyscore(timeline_key, status.id, status.id)
end
def inline_render(target_account, template, object)
rabl_scope = Class.new do
include RoutingHelper
def initialize(account)
@account = account
end
def current_user
@account.try(:user)
end
def current_account
@account
end end
end end
Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
end end
private private
@ -94,37 +82,39 @@ class FeedManager
Redis.current Redis.current
end end
def filter_from_home?(status, receiver) def filter_from_home?(status, receiver_id)
return true if receiver.muting?(status.account) return true if status.reply? && status.in_reply_to_id.nil?
should_filter = false check_for_mutes = [status.account_id]
check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
if status.reply? && status.in_reply_to_id.nil? return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
should_filter = true
elsif status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply check_for_blocks = status.mentions.map(&:account_id)
should_filter = !receiver.following?(status.in_reply_to_account) # and I'm not following the person it's a reply to check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
should_filter &&= !(receiver.id == status.in_reply_to_account_id) # and it's not a reply to me
should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
elsif status.reblog? # Filter out a reblog
should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply
should_filter ||= receiver.muting?(status.reblog.account) # or muting that person should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to
should_filter ||= status.reblog.account.blocking?(receiver) # or if the author of the reblogged status is blocking me should_filter &&= !(receiver_id == status.in_reply_to_account_id) # and it's not a reply to me
should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply
return should_filter
elsif status.reblog? # Filter out a reblog
return Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me
end end
should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked false
should_filter
end end
def filter_from_mentions?(status, receiver) def filter_from_mentions?(status, receiver_id)
should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself check_for_blocks = [status.account_id]
should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked check_for_blocks.concat(status.mentions.pluck(:account_id))
should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
should_filter ||= (status.account.silenced? && !receiver.following?(status.account)) # of if the account is silenced and I'm not following them
if status.reply? && !status.in_reply_to_account_id.nil? # or it's a reply should_filter = receiver_id == status.account_id # Filter if I'm mentioning myself
should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked should_filter ||= Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # or it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
end should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
should_filter should_filter
end end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class InlineRablScope
include RoutingHelper
def initialize(account)
@account = account
end
def current_user
@account.try(:user)
end
def current_account
@account
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class InlineRenderer
def self.render(status, current_account, template)
Rabl::Renderer.new(
template,
status,
view_path: 'app/views',
format: :json,
scope: InlineRablScope.new(current_account)
).render
end
end

View file

@ -3,9 +3,8 @@
class Block < ApplicationRecord class Block < ApplicationRecord
include Paginable include Paginable
belongs_to :account belongs_to :account, required: true
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account', required: true
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
end end

View file

@ -10,17 +10,9 @@ class Feed
max_id = '+inf' if max_id.blank? max_id = '+inf' if max_id.blank?
since_id = '-inf' if since_id.blank? since_id = '-inf' if since_id.blank?
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i) unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)
status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
# If we're after most recent items and none are there, we need to precompute the feed unhydrated.map { |id| status_map[id] }.compact
if unhydrated.empty? && max_id == '+inf' && since_id == '-inf'
RegenerationWorker.perform_async(@account.id, @type)
@statuses = Status.send("as_#{@type}_timeline", @account).cache_ids.paginate_by_max_id(limit, nil, nil)
else
status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
@statuses = unhydrated.map { |id| status_map[id] }.compact
end
@statuses
end end
private private

View file

@ -3,11 +3,14 @@
class Follow < ApplicationRecord class Follow < ApplicationRecord
include Paginable include Paginable
belongs_to :account, counter_cache: :following_count belongs_to :account, counter_cache: :following_count, required: true
belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count
belongs_to :target_account,
class_name: 'Account',
counter_cache: :followers_count,
required: true
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
end end

View file

@ -3,12 +3,11 @@
class FollowRequest < ApplicationRecord class FollowRequest < ApplicationRecord
include Paginable include Paginable
belongs_to :account belongs_to :account, required: true
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account', required: true
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
def authorize! def authorize!

View file

@ -1,11 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Mention < ApplicationRecord class Mention < ApplicationRecord
belongs_to :account, inverse_of: :mentions belongs_to :account, inverse_of: :mentions, required: true
belongs_to :status belongs_to :status, required: true
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
validates :account, :status, presence: true
validates :account, uniqueness: { scope: :status } validates :account, uniqueness: { scope: :status }
end end

View file

@ -3,6 +3,7 @@
class Report < ApplicationRecord class Report < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'
belongs_to :action_taken_by_account, class_name: 'Account'
scope :unresolved, -> { where(action_taken: false) } scope :unresolved, -> { where(action_taken: false) }
scope :resolved, -> { where(action_taken: true) } scope :resolved, -> { where(action_taken: true) }

View file

@ -161,9 +161,9 @@ class Status < ApplicationRecord
return where.not(visibility: [:private, :direct]) if account.nil? return where.not(visibility: [:private, :direct]) if account.nil?
if target_account.blocking?(account) # get rid of blocked peeps if target_account.blocking?(account) # get rid of blocked peeps
where('1 = 0') none
elsif account.id == target_account.id # author can see own stuff elsif account.id == target_account.id # author can see own stuff
where('1 = 1') all
elsif account.following?(target_account) # followers can see followers-only stuff, but also things they are mentioned in elsif account.following?(target_account) # followers can see followers-only stuff, but also things they are mentioned in
joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s) joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s)
.where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct]) .where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct])
@ -188,7 +188,7 @@ class Status < ApplicationRecord
end end
before_validation do before_validation do
text.strip! text&.strip!
spoiler_text&.strip! spoiler_text&.strip!
self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply

View file

@ -1,13 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class BlockDomainService < BaseService class BlockDomainService < BaseService
def call(domain, severity) def call(domain_block)
DomainBlock.where(domain: domain).first_or_create!(domain: domain, severity: severity) if domain_block.silence?
Account.where(domain: domain_block.domain).update_all(silenced: true)
if severity == :silence
Account.where(domain: domain).update_all(silenced: true)
else else
Account.where(domain: domain).find_each do |account| Account.where(domain: domain_block.domain).find_each do |account|
account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed? account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
SuspendAccountService.new.call(account) SuspendAccountService.new.call(account)
end end

View file

@ -4,6 +4,8 @@ class FanOutOnWriteService < BaseService
# Push a status into home and mentions feeds # Push a status into home and mentions feeds
# @param [Status] status # @param [Status] status
def call(status) def call(status)
raise Mastodon::RaceConditionError if status.visibility.nil?
deliver_to_self(status) if status.account.local? deliver_to_self(status) if status.account.local?
if status.direct_visibility? if status.direct_visibility?
@ -14,6 +16,7 @@ class FanOutOnWriteService < BaseService
return if status.account.silenced? || !status.public_visibility? || status.reblog? return if status.account.silenced? || !status.public_visibility? || status.reblog?
render_anonymous_payload(status)
deliver_to_hashtags(status) deliver_to_hashtags(status)
return if status.reply? && status.in_reply_to_account_id != status.account_id return if status.reply? && status.in_reply_to_account_id != status.account_id
@ -31,9 +34,8 @@ class FanOutOnWriteService < BaseService
def deliver_to_followers(status) def deliver_to_followers(status)
Rails.logger.debug "Delivering status #{status.id} to followers" Rails.logger.debug "Delivering status #{status.id} to followers"
status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).find_each do |follower| status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).find_each do |follower|
next if FeedManager.instance.filter?(:home, status, follower) FeedInsertWorker.perform_async(status.id, follower.id)
FeedManager.instance.push(:home, follower, status)
end end
end end
@ -42,28 +44,28 @@ class FanOutOnWriteService < BaseService
status.mentions.includes(:account).each do |mention| status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account mentioned_account = mention.account
next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mentioned_account) next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mention.account_id)
FeedManager.instance.push(:home, mentioned_account, status) FeedManager.instance.push(:home, mentioned_account, status)
end end
end end
def render_anonymous_payload(status)
@payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show')
end
def deliver_to_hashtags(status) def deliver_to_hashtags(status)
Rails.logger.debug "Delivering status #{status.id} to hashtags" Rails.logger.debug "Delivering status #{status.id} to hashtags"
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status) status.tags.pluck(:name).each do |hashtag|
Redis.current.publish("hashtag:#{hashtag}", Oj.dump(event: :update, payload: @payload))
status.tags.find_each do |tag| Redis.current.publish("hashtag:#{hashtag}:local", Oj.dump(event: :update, payload: @payload)) if status.account.local?
FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'update', payload: payload)
FeedManager.instance.broadcast("hashtag:#{tag.name}:local", event: 'update', payload: payload) if status.account.local?
end end
end end
def deliver_to_public(status) def deliver_to_public(status)
Rails.logger.debug "Delivering status #{status.id} to public timeline" Rails.logger.debug "Delivering status #{status.id} to public timeline"
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status) Redis.current.publish('public', Oj.dump(event: 'update', payload: @payload))
Redis.current.publish('public:local', Oj.dump(event: 'update', payload: @payload)) if status.account.local?
FeedManager.instance.broadcast(:public, event: 'update', payload: payload)
FeedManager.instance.broadcast('public:local', event: 'update', payload: payload) if status.account.local?
end end
end end

View file

@ -17,7 +17,7 @@ class NotifyService < BaseService
private private
def blocked_mention? def blocked_mention?
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient) FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
end end
def blocked_favourite? def blocked_favourite?
@ -50,7 +50,7 @@ class NotifyService < BaseService
def create_notification def create_notification
@notification.save! @notification.save!
return unless @notification.browserable? return unless @notification.browserable?
FeedManager.instance.broadcast(@recipient.id, event: 'notification', payload: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification)) Redis.current.publish(@recipient.id, Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
end end
def send_email def send_email

View file

@ -5,9 +5,11 @@ class PrecomputeFeedService < BaseService
# @param [Symbol] type :home or :mentions # @param [Symbol] type :home or :mentions
# @param [Account] account # @param [Account] account
def call(_, account) def call(_, account)
Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status| redis.pipelined do
next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account) Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS / 4).each do |status|
redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account.id)
redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
end
end end
end end

View file

@ -65,17 +65,17 @@ class RemoveStatusService < BaseService
redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id) redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
end end
FeedManager.instance.broadcast(receiver.id, event: 'delete', payload: status.id) Redis.current.publish(receiver.id, Oj.dump(event: :delete, payload: status.id))
end end
def remove_from_hashtags(status) def remove_from_hashtags(status)
status.tags.each do |tag| status.tags.each do |tag|
FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'delete', payload: status.id) Redis.current.publish("hashtag:#{tag.name}", Oj.dump(event: :delete, payload: status.id))
end end
end end
def remove_from_public(status) def remove_from_public(status)
FeedManager.instance.broadcast(:public, event: 'delete', payload: status.id) Redis.current.publish('public', Oj.dump(event: :delete, payload: status.id))
end end
def redis def redis

View file

@ -24,21 +24,34 @@
.screenshot-with-signup .screenshot-with-signup
.mascot= image_tag 'fluffy-elephant-friend.png' .mascot= image_tag 'fluffy-elephant-friend.png'
= simple_form_for(@user, url: user_registration_path) do |f| - if @open_registrations
= f.simple_fields_for :account do |ff| = simple_form_for(@user, url: user_registration_path) do |f|
= ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } = f.simple_fields_for :account do |ff|
= ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
= f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
= f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
.actions .actions
= f.button :button, t('about.get_started'), type: :submit = f.button :button, t('about.get_started'), type: :submit
.info .info
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
· ·
= link_to t('about.about_this'), about_more_path = link_to t('about.about_this'), about_more_path
- else
.closed-registrations-message
- if @closed_registrations_message.blank?
%p= t('about.closed_registrations')
- else
= @closed_registrations_message.html_safe
.info
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
·
= link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
·
= link_to t('about.about_this'), about_more_path
%h3= t('about.features_headline') %h3= t('about.features_headline')

View file

@ -14,3 +14,4 @@
%td= block.severity %td= block.severity
= will_paginate @blocks, pagination_options = will_paginate @blocks, pagination_options
= link_to 'Add new', new_admin_domain_block_path, class: 'button'

View file

@ -0,0 +1,18 @@
- content_for :page_title do
New domain block
= simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
= render 'shared/error_messages', object: @domain_block
%p.hint The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.
= f.input :domain, placeholder: 'Domain'
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false
%p.hint
%strong Silence
will make the account's posts invisible to anyone who isn't following them.
%strong Suspend
will remove all of the account's content, media, and profile data.
.actions
= f.button :button, 'Create block', type: :submit

View file

@ -8,20 +8,25 @@
%li= filter_link_to 'Unresolved', action_taken: nil %li= filter_link_to 'Unresolved', action_taken: nil
%li= filter_link_to 'Resolved', action_taken: '1' %li= filter_link_to 'Resolved', action_taken: '1'
%table.table = form_tag do
%thead
%tr %table.table
%th ID %thead
%th Target
%th Reported by
%th Comment
%th
%tbody
- @reports.each do |report|
%tr %tr
%td= "##{report.id}" %th
%td= link_to report.target_account.acct, admin_account_path(report.target_account.id) %th ID
%td= link_to report.account.acct, admin_account_path(report.account.id) %th Target
%td= truncate(report.comment, length: 30, separator: ' ') %th Reported by
%td= table_link_to 'circle', 'View', admin_report_path(report) %th Comment
%th
%tbody
- @reports.each do |report|
%tr
%td= check_box_tag 'select', report.id
%td= "##{report.id}"
%td= link_to report.target_account.acct, admin_account_path(report.target_account.id)
%td= link_to report.account.acct, admin_account_path(report.account.id)
%td= truncate(report.comment, length: 30, separator: ' ')
%td= table_link_to 'circle', 'View', admin_report_path(report)
= will_paginate @reports, pagination_options = will_paginate @reports, pagination_options

View file

@ -27,7 +27,7 @@
= link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: 'Delete' do = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: 'Delete' do
= fa_icon 'trash' = fa_icon 'trash'
- unless @report.action_taken? - if !@report.action_taken?
%hr/ %hr/
%div{ style: 'overflow: hidden' } %div{ style: 'overflow: hidden' }
@ -36,3 +36,9 @@
= link_to 'Suspend account', suspend_admin_report_path(@report), method: :post, class: 'button' = link_to 'Suspend account', suspend_admin_report_path(@report), method: :post, class: 'button'
%div{ style: 'float: left' } %div{ style: 'float: left' }
= link_to 'Mark as resolved', resolve_admin_report_path(@report), method: :post, class: 'button' = link_to 'Mark as resolved', resolve_admin_report_path(@report), method: :post, class: 'button'
- elsif !@report.action_taken_by_account.nil?
%hr/
%p
%strong Action taken by:
= @report.action_taken_by_account.acct

View file

@ -38,3 +38,15 @@
%br/ %br/
You can use HTML tags You can use HTML tags
%td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description']) %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
%tr
%td
%strong Open registration
%td= best_in_place @settings['open_registrations'], :value, as: :checkbox, collection: { false: 'Disabled', true: 'Enabled'}, url: admin_setting_path(@settings['open_registrations'])
%tr
%td
%strong Closed registration message
%br/
Displayed on frontpage when registrations are closed
%br/
You can use HTML tags
%td= best_in_place @settings['closed_registrations_message'], :value, as: :textarea, url: admin_setting_path(@settings['closed_registrations_message'])

View file

@ -3,7 +3,7 @@
class AfterRemoteFollowRequestWorker class AfterRemoteFollowRequestWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options retry: 5 sidekiq_options queue: 'pull', retry: 5
def perform(follow_request_id) def perform(follow_request_id)
follow_request = FollowRequest.find(follow_request_id) follow_request = FollowRequest.find(follow_request_id)
@ -13,5 +13,7 @@ class AfterRemoteFollowRequestWorker
follow_request.destroy follow_request.destroy
FollowService.new.call(follow_request.account, updated_account.acct) FollowService.new.call(follow_request.account, updated_account.acct)
rescue ActiveRecord::RecordNotFound
true
end end
end end

View file

@ -3,7 +3,7 @@
class AfterRemoteFollowWorker class AfterRemoteFollowWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options retry: 5 sidekiq_options queue: 'pull', retry: 5
def perform(follow_id) def perform(follow_id)
follow = Follow.find(follow_id) follow = Follow.find(follow_id)
@ -13,5 +13,7 @@ class AfterRemoteFollowWorker
follow.destroy follow.destroy
FollowService.new.call(follow.account, updated_account.acct) FollowService.new.call(follow.account, updated_account.acct)
rescue ActiveRecord::RecordNotFound
true
end end
end end

View file

@ -0,0 +1,5 @@
class ApplicationWorker
def info(message)
Rails.logger.info("#{self.class.name} - #{message}")
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class DistributionWorker class DistributionWorker < ApplicationWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform(status_id) def perform(status_id)
@ -9,6 +9,6 @@ class DistributionWorker
FanOutOnWriteService.new.call(status) FanOutOnWriteService.new.call(status)
WarmCacheService.new.call(status) WarmCacheService.new.call(status)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true info("Couldn't find the status")
end end
end end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class DomainBlockWorker
include Sidekiq::Worker
def perform(domain_block_id)
BlockDomainService.new.call(DomainBlock.find(domain_block_id))
rescue ActiveRecord::RecordNotFound
true
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class FeedInsertWorker
include Sidekiq::Worker
def perform(status_id, follower_id)
status = Status.find(status_id)
follower = Account.find(follower_id)
return if FeedManager.instance.filter?(:home, status, follower.id)
FeedManager.instance.push(:home, follower, status)
rescue ActiveRecord::RecordNotFound
true
end
end

View file

@ -5,7 +5,7 @@ require 'csv'
class ImportWorker class ImportWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options retry: false sidekiq_options queue: 'pull', retry: false
def perform(import_id) def perform(import_id)
import = Import.find(import_id) import = Import.find(import_id)

View file

@ -3,7 +3,7 @@
class LinkCrawlWorker class LinkCrawlWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options retry: false sidekiq_options queue: 'pull', retry: false
def perform(status_id) def perform(status_id)
FetchLinkCardService.new.call(Status.find(status_id)) FetchLinkCardService.new.call(Status.find(status_id))

View file

@ -3,6 +3,8 @@
class MergeWorker class MergeWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(from_account_id, into_account_id) def perform(from_account_id, into_account_id)
FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id)) FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
end end

View file

@ -3,7 +3,7 @@
class NotificationWorker class NotificationWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options retry: 5 sidekiq_options queue: 'push', retry: 5
def perform(xml, source_account_id, target_account_id) def perform(xml, source_account_id, target_account_id)
SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id)) SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))

View file

@ -22,6 +22,7 @@ class Pubsubhubbub::DeliveryWorker
.headers(headers) .headers(headers)
.post(subscription.callback_url, body: payload) .post(subscription.callback_url, body: payload)
return subscription.destroy! if response.code > 299 && response.code < 500 && response.code != 429 # HTTP 4xx means error is not temporary, except for 429 (throttling)
raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300 raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300
subscription.touch(:last_successful_delivery_at) subscription.touch(:last_successful_delivery_at)

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class PushUpdateWorker
include Sidekiq::Worker
def perform(account_id, status_id)
account = Account.find(account_id)
status = Status.find(status_id)
message = InlineRenderer.render(status, account, 'api/v1/statuses/show')
Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
rescue ActiveRecord::RecordNotFound
true
end
end

View file

@ -3,7 +3,9 @@
class RegenerationWorker class RegenerationWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform(account_id, timeline_type) sidekiq_options queue: 'pull', backtrace: true, unique: :until_executed
PrecomputeFeedService.new.call(timeline_type, Account.find(account_id))
def perform(account_id, _ = :home)
PrecomputeFeedService.new.call(:home, Account.find(account_id))
end end
end end

View file

@ -3,7 +3,7 @@
class ThreadResolveWorker class ThreadResolveWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options retry: false sidekiq_options queue: 'pull', retry: false
def perform(child_status_id, parent_url) def perform(child_status_id, parent_url)
child_status = Status.find(child_status_id) child_status = Status.find(child_status_id)

View file

@ -3,6 +3,8 @@
class UnmergeWorker class UnmergeWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(from_account_id, into_account_id) def perform(from_account_id, into_account_id)
FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id)) FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
end end

View file

@ -24,7 +24,7 @@ module Mastodon
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :eo] config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo]
config.i18n.default_locale = :en config.i18n.default_locale = :en
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')

View file

@ -2,4 +2,5 @@
Rails.application.configure do Rails.application.configure do
config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' } config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' }
config.x.email_domains_whitelist = ENV.fetch('EMAIL_DOMAIN_WHITELIST') { '' }
end end

View file

@ -1,4 +1,5 @@
Rabl.configure do |config| Rabl.configure do |config|
config.json_engine = Oj
config.cache_all_output = false config.cache_all_output = false
config.cache_sources = Rails.env.production? config.cache_sources = Rails.env.production?
config.include_json_root = false config.include_json_root = false

View file

@ -1,14 +1,14 @@
--- ---
de: de:
about: about:
about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Eine <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen, verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen. about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen.
get_started: Erste Schritte get_started: Erste Schritte
source_code: Quellcode source_code: Quellcode
terms: AGB terms: AGB
accounts: accounts:
follow: Folgen follow: Folgen
followers: Folger followers: Follower
following: Folgt following: Gefolgt
nothing_here: Hier gibt es nichts! nothing_here: Hier gibt es nichts!
people_followed_by: Nutzer, denen %{name} folgt people_followed_by: Nutzer, denen %{name} folgt
people_who_follow: Nutzer, die %{name} folgen people_who_follow: Nutzer, die %{name} folgen
@ -27,7 +27,7 @@ de:
reset_password: Passwort zurücksetzen reset_password: Passwort zurücksetzen
set_new_password: Neues Passwort setzen set_new_password: Neues Passwort setzen
authorize_follow: authorize_follow:
error: Das entfernte Profil konnte nicht geladen werden error: Das Profil konnte nicht geladen werden
follow: Folgen follow: Folgen
prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:' prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:'
title: "%{acct} folgen" title: "%{acct} folgen"
@ -55,25 +55,25 @@ de:
notification_mailer: notification_mailer:
favourite: favourite:
body: 'Dein Beitrag wurde von %{name} favorisiert:' body: 'Dein Beitrag wurde von %{name} favorisiert:'
subject: "%{name} hat deinen Beitrag favorisiert" subject: "%{name} hat deinen Beitrag favorisiert."
follow: follow:
body: "%{name} folgt dir jetzt!" body: "%{name} folgt dir jetzt!"
subject: "%{name} folgt dir nun" subject: "%{name} folgt dir jetzt."
follow_request: follow_request:
body: "%{name} möchte dir folgen:" body: "%{name} möchte dir folgen:"
subject: "%{name} möchte dir folgen" subject: "%{name} möchte dir folgen."
mention: mention:
body: "%{name} hat dich erwähnt:" body: "%{name} hat dich erwähnt:"
subject: "%{name} hat dich erwähnt" subject: "%{name} hat dich erwähnt."
reblog: reblog:
body: 'Dein Beitrag wurde von %{name} geteilt:' body: 'Dein Beitrag wurde von %{name} geteilt:'
subject: "%{name} teilte deinen Beitrag" subject: "%{name} teilte deinen Beitrag."
pagination: pagination:
next: Vorwärts next: Vorwärts
prev: Zurück prev: Zurück
remote_follow: remote_follow:
acct: Dein Nutzername@Domain, von dem du dieser Person folgen möchtest acct: Dein Nutzername@Domain, von dem aus du dieser Person folgen möchtest.
missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden.
proceed: Weiter proceed: Weiter
prompt: 'Du wirst dieser Person folgen:' prompt: 'Du wirst dieser Person folgen:'
settings: settings:

View file

@ -2,59 +2,59 @@
de: de:
devise: devise:
confirmations: confirmations:
confirmed: "Vielen Dank für Deine Registrierung. Bitte melde dich jetzt an." confirmed: "Vielen Dank für deine Registrierung. Bitte melde dich jetzt an."
send_instructions: "Du erhältst in wenigen Minuten eine E-Mail, mit der Du Deine Registrierung bestätigen kannst." send_instructions: "Du erhältst in wenigen Minuten eine E-Mail, mit der du deine Registrierung bestätigen kannst."
send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Du Deine Registrierung bestätigen kannst." send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert, erhältst Du in wenigen Minuten eine E-Mail mit der du deine Registrierung bestätigen kannst."
failure: failure:
already_authenticated: "Du bist bereits angemeldet." already_authenticated: "Du bist bereits angemeldet."
inactive: "Dein Account ist nicht aktiv." inactive: "Dein Account ist nicht aktiv."
invalid: "Ungültige Anmeldedaten." invalid: "Ungültige Anmeldedaten."
last_attempt: "Du hast noch einen Versuch bevor dein Account gesperrt wird" last_attempt: "Du hast noch einen Versuch bevor dein Account gesperrt wird."
locked: "Dein Account ist gesperrt." locked: "Dein Account ist gesperrt."
not_found_in_database: "E-Mail-Adresse oder Passwort ungültig." not_found_in_database: "E-Mail-Adresse oder Passwort ungültig."
timeout: "Deine Sitzung ist abgelaufen, bitte melde Dich erneut an." timeout: "Deine Sitzung ist abgelaufen, bitte melde dich erneut an."
unauthenticated: "Du musst Dich anmelden oder registrieren, bevor Du fortfahren kannst." unauthenticated: "Du musst Dich anmelden oder registrieren, bevor du fortfahren kannst."
unconfirmed: "Du musst Deinen Account bestätigen, bevor Du fortfahren kannst." unconfirmed: "Du musst deinen Account bestätigen, bevor du fortfahren kannst."
mailer: mailer:
confirmation_instructions: confirmation_instructions:
subject: "Mastodon: Anleitung zur Bestätigung Deines Accounts" subject: "Mastodon: Anleitung zur Bestätigung deines Accounts"
password_change: password_change:
subject: 'Mastodon: Passwort wurde geändert' subject: 'Mastodon: Passwort wurde geändert'
reset_password_instructions: reset_password_instructions:
subject: "Mastodon: Anleitung um Dein Passwort zurückzusetzen" subject: "Mastodon: Anleitung um dein Passwort zurückzusetzen"
unlock_instructions: unlock_instructions:
subject: "Mastodon: Anleitung um Deinen Account freizuschalten" subject: "Mastodon: Anleitung um deinen Account freizuschalten"
omniauth_callbacks: omniauth_callbacks:
failure: "Du konntest nicht Deinem %{kind}-Account angemeldet werden, weil '%{reason}'." failure: "Du konntest nicht mit deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
success: "Du hast Dich erfolgreich mit Deinem %{kind}-Account angemeldet." success: "Du hast dich erfolgreich mit Deinem %{kind}-Account angemeldet."
passwords: passwords:
no_token: "Du kannst diese Seite nur von dem Link aus einer E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast stelle bitte sicher, dass du die vollständige Adresse aufrufst." no_token: "Du kannst diese Seite nur über den Link aus der E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast, stelle bitte sicher, dass du die vollständige Adresse aufrufst."
send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Dein Passwort zurücksetzen kannst." send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst."
send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Dein Passwort zurücksetzen können." send_paranoid_instructions: "Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst."
updated: "Dein Passwort wurde geändert. Du bist jetzt angemeldet." updated: "Dein Passwort wurde geändert. Du bist jetzt angemeldet."
updated_not_active: "Dein Passwort wurde geändert." updated_not_active: "Dein Passwort wurde geändert."
registrations: registrations:
destroyed: "Dein Account wurde gelöscht." destroyed: "Dein Account wurde gelöscht."
signed_up: "Du hast dich erfolgreich registriert." signed_up: "Du hast dich erfolgreich registriert."
signed_up_but_inactive: "Du hast dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account inaktiv ist." signed_up_but_inactive: "Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account inaktiv ist."
signed_up_but_locked: "Du hast dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account gesperrt ist." signed_up_but_locked: "Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account gesperrt ist."
signed_up_but_unconfirmed: "Du hast Dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst." signed_up_but_unconfirmed: "Du hast Dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst."
update_needs_confirmation: "Deine Daten wurden aktualisiert, aber Du musst Deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der Du die Änderung Deiner E-Mail-Adresse abschließen kannst." update_needs_confirmation: "Deine Daten wurden aktualisiert, aber du musst deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der du die Änderung deiner E-Mail-Adresse abschließen kannst."
updated: "Deine Daten wurden aktualisiert." updated: "Deine Daten wurden aktualisiert."
sessions: sessions:
already_signed_out: "Erfolgreich abgemeldet." already_signed_out: "Erfolgreich abgemeldet."
signed_in: "Erfolgreich angemeldet." signed_in: "Erfolgreich angemeldet."
signed_out: "Erfolgreich abgemeldet." signed_out: "Erfolgreich abgemeldet."
unlocks: unlocks:
send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren können." send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren können."
send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren kannst." send_paranoid_instructions: "Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren kannst."
unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet." unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet."
errors: errors:
messages: messages:
already_confirmed: "wurde bereits bestätigt" already_confirmed: "wurde bereits bestätigt."
confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an" confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an."
expired: "ist abgelaufen, bitte neu anfordern" expired: "ist abgelaufen, bitte neu anfordern."
not_found: "nicht gefunden" not_found: "wurde nicht gefunden."
not_locked: "ist nicht gesperrt" not_locked: "ist nicht gesperrt"
not_saved: not_saved:
one: "Konnte %{resource} nicht speichern: ein Fehler." one: "Konnte %{resource} nicht speichern: ein Fehler."

View file

@ -0,0 +1,61 @@
---
fi:
devise:
confirmations:
confirmed: Sähköpostisi on onnistuneesti vahvistettu.
send_instructions: Saat kohta sähköpostiisi ohjeet kuinka voit aktivoida tilisi.
send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet sen varmentamiseen.
failure:
already_authenticated: Olet jo kirjautunut sisään.
inactive: Tiliäsi ei ole viellä aktivoitu.
invalid: Virheellinen %{authentication_keys} tai salasana.
last_attempt: Sinulla on yksi yritys jäljellä tai tili lukitaan.
locked: Tili on lukittu.
not_found_in_database: Virheellinen %{authentication_keys} tai salasana.
timeout: Sessiosi on umpeutunut. Kirjaudu sisään jatkaaksesi.
unauthenticated: Sinun tarvitsee kirjautua sisään tai rekisteröityä jatkaaksesi.
unconfirmed: Sinun tarvitsee varmentaa sähköpostisi jatkaaksesi.
mailer:
confirmation_instructions:
subject: 'Mastodon: Varmistus ohjeet'
password_change:
subject: 'Mastodon: Salasana vaihdettu'
reset_password_instructions:
subject: 'Mastodon: Salasanan vaihto ohjeet'
unlock_instructions:
subject: 'Mastodon: Avauksen ohjeet'
omniauth_callbacks:
failure: Varmennus %{kind} epäonnistui koska "%{reason}".
success: Onnistuneesti varmennettu %{kind} tilillä.
passwords:
no_token: Et pääse tälle sivulle ilman salasanan vaihto sähköpostia. Jos tulet tämmöisestä postista, varmista että sinulla on täydellinen URL.
send_instructions: Saat sähköpostitse ohjeet salasanan palautukseen muutaman minuutin kuluessa.
send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
updated: Salasanasi vaihdettu onnistuneesti. Olet nyt kirjautunut sisään.
updated_not_active: Salasanasi vaihdettu onnistuneesti.
registrations:
destroyed: Näkemiin! Tilisi on onnistuneesti peruttu. Toivottavasti näemme joskus uudestaan.
signed_up: Tervetuloa! Rekisteröitymisesi onnistu.
signed_up_but_inactive: Olet onnistuneesti rekisteröitynyt, mutta emme voi kirjata sinua sisään koska tiliäsi ei ole viellä aktivoitu.
signed_up_but_locked: Olet onnistuneesti rekisteröitynyt, mutta emme voi kirjata sinua sisään koska tilisi on lukittu.
signed_up_but_unconfirmed: Varmistuslinkki on lähetty sähköpostiisi. Seuraa sitä jotta tilisi voidaan aktivoida.
update_needs_confirmation: Tilisi on onnistuneesti päivitetty, mutta meidän tarvitsee vahvistaa sinun uusi sähköpostisi. Tarkista sähköpostisi ja seuraa viestissä tullutta linkkiä varmistaaksesi uuden osoitteen..
updated: Tilisi on onnistuneesti päivitetty.
sessions:
already_signed_out: Ulos kirjautuminen onnistui.
signed_in: Sisäänkirjautuminen onnistui.
signed_out: Ulos kirjautuminen onnistui.
unlocks:
send_instructions: Saat sähköpostiisi pian ohjeet, jolla voit avata tilisi uudestaan.
send_paranoid_instructions: Jos tilisi on olemassa, saat sähköpostiisi pian ohjeet tilisi avaamiseen.
unlocked: Tilisi on avattu onnistuneesti. Kirjaudu normaalisti sisään.
errors:
messages:
already_confirmed: on jo varmistettu. Yritä kirjautua sisään
confirmation_period_expired: pitää varmistaa %{period} sisällä, ole hyvä ja pyydä uusi
expired: on erääntynyt, ole hyvä ja pyydä uusi
not_found: ei löydy
not_locked: ei ollut lukittu
not_saved:
one: '1 virhe esti %{resource} tallennuksen:'
other: "%{count} virhettä esti %{resource} tallennuksen:"

View file

@ -0,0 +1,61 @@
---
'no':
devise:
confirmations:
confirmed: Epostaddressen din er blitt bekreftet.
send_instructions: Du vil motta en epost med instruksjoner for hvordan bekrefte din epostaddresse om noen få minutter.
send_paranoid_instructions: Hvis din epostaddresse finnes i vår database vil du motta en epost med instruksjoner for hvordan bekrefte din epost om noen få minutter.
failure:
already_authenticated: Du er allerede innlogget.
inactive: Din konto er ikke blitt aktivert ennå.
invalid: Ugyldig %{authentication_keys} eller passord.
last_attempt: Du har ett forsøk igjen før kontoen din bli låst.
locked: Din konto er låst.
not_found_in_database: Ugyldig %{authentication_keys} eller passord.
timeout: Sesjonen din løp ut på tid. Logg inn på nytt for å fortsette.
unauthenticated: Du må logge inn eller registrere deg før du kan fortsette.
unconfirmed: Du må bekrefte epostadressen din før du kan fortsette.
mailer:
confirmation_instructions:
subject: 'Mastodon: Instruksjoner for å bekrefte epostadresse'
password_change:
subject: 'Mastodon: Passord endret'
reset_password_instructions:
subject: 'Mastodon: Hvordan nullstille passord?'
unlock_instructions:
subject: 'Mastodon: Instruksjoner for å gjenåpne konto'
omniauth_callbacks:
failure: Kunne ikke autentisere deg fra %{kind} fordi "%{reason}".
success: Vellykket autentisering fra %{kind}.
passwords:
no_token: Du har ingen tilgang til denne siden så lenge du ikke kommer fra en epost om nullstilling av passord. Hvis du kommer fra en passordnullstilling epost, dobbelsjekk at du brukte hele URLen.
send_instructions: Du vil motta en epost med instruksjoner for å nullstille passordet ditt om noen få minutter.
send_paranoid_instructions: Hvis epostadressen din finnes i databasen vår vil du motta en instruksjonsmail om passord nullstilling om noen få minutter.
updated: Passordet ditt har blitt endret. Du er nå logget inn.
updated_not_active: Passordet ditt har blitt endret.
registrations:
destroyed: Adjø! Kontoen din har blitt avsluttet. Vi håper at vi ser deg igjen snart.
signed_up: Velkommen! Registrasjonen var vellykket.
signed_up_but_inactive: Registrasjonen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din ennå ikke har blitt aktivert.
signed_up_but_locked: Registrasjonen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din har blitt låst.
signed_up_but_unconfirmed: En epostmelding med en bekreftelseslink har blitt sendt til din adresse. Klikk på linken i eposten for å aktivere kontoen din.
update_needs_confirmation: Du har oppdatert kontoen din, men vi må bekrefte din nye epostadresse. Sjekk eposten din og følg bekreftelseslinken for å bekrefte din nye epostadresse.
updated: Kontoen din ble oppdatert.
sessions:
already_signed_out: Logget ut.
signed_in: Logget inn.
signed_out: Logget ut.
unlocks:
send_instructions: Du vil motta en epost med instruksjoner for å åpne kontoen din om noen få minutter.
send_paranoid_instructions: Hvis kontoen din eksisterer vil du motta en epost med instruksjoner for å åpne kontoen din om noen få minutter.
unlocked: Kontoen din ble åpnet uten problemer. Logg på for å fortsette.
errors:
messages:
already_confirmed: har allerede blitt bekreftet, prøv å logg på istedet.
confirmation_period_expired: må bekreftes innen %{period}. Spør om en ny bekreftelsesmail istedet.
expired: har utløpt, spør om en ny en istedet
not_found: ikke funnet
not_locked: var ikke låst
not_saved:
one: '1 feil hindret denne %{resource} fra å bli lagret:'
other: "%{count} feil hindret denne %{resource} fra å bli lagret:"

View file

@ -0,0 +1,113 @@
---
fi:
activerecord:
attributes:
doorkeeper/application:
name: Nimi
redirect_uri: Uudelleenohjaus URI
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: ei voi sisältää osia.
invalid_uri: pitää olla validi URI.
relative_uri: pitää olla täydellinen URI.
secured_uri: pitää olla HTTPS/SSL URI.
doorkeeper:
applications:
buttons:
authorize: Valtuuta
cancel: Peruuta
destroy: Tuhoa
edit: Muokkaa
submit: Lähetä
confirmations:
destroy: Oletko varma?
edit:
title: Muokkaa applikaatiota
form:
error: Whoops! Tarkista lomakkeesi mahdollisten virheiden varalta
help:
native_redirect_uri: Käytä %{native_redirect_uri} paikallisiin testeihin
redirect_uri: Käytä yhtä riviä per URI
scopes: Erota scopet välilyönnein. Jätä tyhjäksi käyteksi oletus scopeja.
index:
callback_url: Callback URL
name: Nimi
new: Uusi applikaatio
title: Sinun applikaatiosi
new:
title: Uusi applikaatio
show:
actions: Toiminnot
application_id: Applikaation Id
callback_urls: Callback urls
scopes: Scopet
secret: Salainen avain
title: 'Applikaatio: %{name}'
authorizations:
buttons:
authorize: Valtuuta
deny: Evää
error:
title: Virhe on tapahtunut
new:
able_to: Se voi
prompt: Applikaatio %{client_name} pyytää lupaa tilillesi
title: Valtuutus vaaditaan
show:
title: Valtuutus koodi
authorized_applications:
buttons:
revoke: Evää
confirmations:
revoke: Oletko varma?
index:
application: Applikaatio
created_at: Valtuutettu
date_format: "%Y-%m-%d %H:%M:%S"
scopes: Scopet
title: Valtuuttamasi applikaatiot
errors:
messages:
access_denied: Resurssin omistaja tai valtuutus palvelin hylkäsi pyynnönr.
credential_flow_not_configured: Resurssin omistajan salasana epäonnistui koska Doorkeeper.configure.resource_owner_from_credentials ei ole konfiguroitu.
invalid_client: Asiakkaan valtuutus epäonnistui koska tuntematon asiakas, asiakas ei sisältänyt valtuutusta, tai tukematon valtuutus tapa
invalid_grant: Antamasi valtuutus lupa on joko väärä, erääntynyt, peruttu, ei vastaa uudelleenohjaus URI jota käytetään valtuutus pyynnössä, tai se myönnettin toiselle asiakkaalle.
invalid_redirect_uri: Uudelleenohjaus uri ei ole oikein.
invalid_request: Pyynnöstä puutti parametri, sisältää tukemattoman parametri arvonn, tai on korruptoitunut.
invalid_resource_owner: Annetut resurssin omistajan tunnnukset ovat väärät, tai resurssin omistajaa ei löydy
invalid_scope: Pyydetty scope on väärä, tuntemat, tai korruptoitunut.
invalid_token:
expired: Access token vanhentunut
revoked: Access token evätty
unknown: Access token väärä
resource_owner_authenticator_not_configured: Resurssin omistajan etsiminen epäonnistui koska Doorkeeper.configure.resource_owner_authenticator ei ole konfiguroitu.
server_error: Valtuutus palvelin kohtasi odottamattoman virheen joka esti sitä täyttämästä pyyntöä.
temporarily_unavailable: Valtuutus palvelin ei voi tällä hetkellä käsitellä pyyntöäsi joko väliaikaisen ruuhkan tai huollon takia.
unauthorized_client: Asiakas ei ole valtuutettu tekemään tätä pyyntöä käyttäen tätä metodia.
unsupported_grant_type: Valtuutus grant type ei ole tuettu valtuutus palvelimella.
unsupported_response_type: Valtuutus palvelin ei tue tätä vastaus tyyppiä.
flash:
applications:
create:
notice: Applikaatio luotu.
destroy:
notice: Applikaatio poistettu.
update:
notice: Applikaatio päivitetty.
authorized_applications:
destroy:
notice: Applikaatio tuhottu.
layouts:
admin:
nav:
applications: Applikaatiot
oauth2_provider: OAuth2 Provider
application:
title: OAuth valtuutus tarvitaan
scopes:
follow: seuraa, estä, peru esto ja lopeta tilien seuraaminen
read: lukea tilin dataa
write: julkaista puolestasi

View file

@ -62,7 +62,7 @@ fr:
buttons: buttons:
revoke: Annuler revoke: Annuler
confirmations: confirmations:
revoke: Êtes-vous certain? revoke: Êtes-vous certain ?
index: index:
application: Application application: Application
created_at: Créé le created_at: Créé le
@ -72,19 +72,19 @@ fr:
errors: errors:
messages: messages:
access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la demande. access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la demande.
credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué en raison de Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré. credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré.
invalid_client: L'authentification du client a échoué à cause d'un client inconnu, d'aucune authentification de client incluse, ou d'une méthode d'authentification non prise en charge. invalid_client: L'authentification du client a échoué à cause d'un client inconnu, d'aucune authentification de client incluse, ou d'une méthode d'authentification non prise en charge.
invalid_grant: Le consentement d'autorisation accordé n'est pas valide, a expiré, est annulé, ne concorde pas avec l'URL de redirection utilisée dans la demande d'autorisation, ou a été émis à un autre client. invalid_grant: Le consentement d'autorisation accordé n'est pas valide, a expiré, est annulé, ne concorde pas avec l'URL de redirection utilisée dans la demande d'autorisation, ou a été émis à un autre client.
invalid_redirect_uri: L'URL de redirection n'est pas valide. invalid_redirect_uri: L'URL de redirection n'est pas valide.
invalid_request: La demande manque un paramètre requis, inclut une valeur de paramètre non prise en charge, ou est autrement mal formée. invalid_request: La demande manque un paramètre requis, inclut une valeur de paramètre non prise en charge, ou est autrement mal formée.
invalid_resource_owner: Les identifiants fournis du propriétaire de la ressource ne sont pas valides, ou le propriétaire de la ressource ne peut être trouvé invalid_resource_owner: Les identifiants fournis par le propriétaire de la ressource ne sont pas valides, ou le propriétaire de la ressource ne peut être trouvé
invalid_scope: La portée demandée n'est pas valide, est inconnue, ou est mal formée. invalid_scope: La portée demandée n'est pas valide, est inconnue, ou est mal formée.
invalid_token: invalid_token:
expired: Le jeton d'accès a expiré expired: Le jeton d'accès a expiré
revoked: Le jeton d'accès a été révoqué revoked: Le jeton d'accès a été révoqué
unknown: Le jeton d'accès n'est pas valide unknown: Le jeton d'accès n'est pas valide
resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué en raison de Doorkeeper.configure.resource_owner_authenticator n'est pas configuré. resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n'est pas configuré.
server_error: Le serveur d'autorisation a rencontré une condition inattendue qui l'a empêché de remplir la demande. server_error: Le serveur d'autorisation a rencontré une condition inattendue l'empêchant de remplir la demande.
temporarily_unavailable: Le serveur d'autorisation est actuellement incapable de traiter la demande à cause d'une surcharge ou d'un entretien temporaire du serveur. temporarily_unavailable: Le serveur d'autorisation est actuellement incapable de traiter la demande à cause d'une surcharge ou d'un entretien temporaire du serveur.
unauthorized_client: Le client n'est pas autorisé à effectuer cette demande à l'aide de cette méthode. unauthorized_client: Le client n'est pas autorisé à effectuer cette demande à l'aide de cette méthode.
unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation. unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation.

View file

@ -0,0 +1,113 @@
---
'no':
activerecord:
attributes:
doorkeeper/application:
name: Navn
redirect_uri: Omdirigerings-URI
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: kan ikke inneholde ett fragment.
invalid_uri: må være en gyldig URI.
relative_uri: må være en absolutt URI.
secured_uri: må være en HTTPS/SSL URI.
doorkeeper:
applications:
buttons:
authorize: Autoriser
cancel: Avbryt
destroy: Ødelegg
edit: Endre
submit: Send inn
confirmations:
destroy: Er du sikker?
edit:
title: Endre applikasjon
form:
error: Whoops! Sjekk skjemaet ditt for mulige feil
help:
native_redirect_uri: Bruk %{native_redirect_uri} for lokale tester
redirect_uri: Bruk en linje per URI
scopes: Adskill omfang med mellomrom. La det være blankt for å bruke standard omfang.
index:
callback_url: Callback URL
name: Navn
new: Ny Applikasjon
title: Dine applikasjoner
new:
title: Ny Applikasjoner
show:
actions: Operasjoner
application_id: Applikasjon Id
callback_urls: Callback urls
scopes: Omfang
secret: Hemmelighet
title: 'Applikasjon: %{name}'
authorizations:
buttons:
authorize: Autoriser
deny: Avvis
error:
title: En feil oppsto
new:
able_to: Den vil ha mulighet til
prompt: Applikasjon %{client_name} spør om tilgang til din konto
title: Autorisasjon påkrevd
show:
title: Autoriserings kode
authorized_applications:
buttons:
revoke: Opphev
confirmations:
revoke: Opphev?
index:
application: Applikasjon
created_at: Autorisert
date_format: "%Y-%m-%d %H:%M:%S"
scopes: Omfang
title: Dine autoriserte applikasjoner
errors:
messages:
access_denied: Ressurseieren eller autoriserings tjeneren avviste forespørslen.
credential_flow_not_configured: Ressurseiers passord flyt feilet på grunn av at Doorkeeper.configure.resource_owner_from_credentials ikke var konfigurert.
invalid_client: Klient autentisering feilet på grunn av ukjent klient, ingen autentisering inkludert eller autentiserings metode som ikke er støttet.
invalid_grant: Autoriseringen er ugyldig, utløpt, opphevet, stemmer ikke overens med omdirigerings-URIen eller var utstedt til en annen klient.
invalid_redirect_uri: redirect urien som var inkludert er ikke gyldig.
invalid_request: Forespørslen mangler ett eller flere parametere, inkluderte ett parameter som ikke støttes eller har feil struktur.
invalid_resource_owner: Ressurseierens detaljer er ikke gyldig, eller så kan ikke eieren finnes.
invalid_scope: Det etterspurte omfanget er ugyldig, ukjent eller har feil struktur.
invalid_token:
expired: Tilgangsbeviset har utløpt
revoked: Tilgangsbeviset har blitt opphevet
unknown: Tilgangsbeviset er ugyldig
resource_owner_authenticator_not_configured: Ressurseier kunne ikke finnes fordi Doorkeeper.configure.resource_owner_authenticator ikke er konfigurert.
server_error: Autoriserings tjeneren støtte på en uventet hendelse som hindret den i å svare på forespørslen.
temporarily_unavailable: Autoriserings tjeneren kan ikke håndtere forespørslen grunnet en midlertidig overbelastning eller tjenervedlikehold.
unauthorized_client: Klienten har ikke autorisasjon for å utføre denne forespørslen med denne metoden.
unsupported_grant_type: Autorisasjons tildelings typen er ikke støttet av denne autoriserings tjeneren.
unsupported_response_type: Autorisasjons serveren støtter ikke denne typen av forespørsler.
flash:
applications:
create:
notice: Applikasjon opprettet.
destroy:
notice: Applikasjon slettet.
update:
notice: Applikasjon oppdatert.
authorized_applications:
destroy:
notice: Applikasjon opphevet.
layouts:
admin:
nav:
applications: Applikasjoner
oauth2_provider: OAuth2 tilbyder
application:
title: OAuth autorisering påkrevet
scopes:
follow: følg, blokker, avblokker, avfølg kontoer
read: lese dine data
write: poste på dine vegne

View file

@ -5,6 +5,7 @@ en:
about_this: About this instance about_this: About this instance
apps: Apps apps: Apps
business_email: 'Business e-mail:' business_email: 'Business e-mail:'
closed_registrations: Registrations are currently closed on this instance.
contact: Contact contact: Contact
description_headline: What is %{domain}? description_headline: What is %{domain}?
domain_count_after: other instances domain_count_after: other instances

164
config/locales/fi.yml Normal file
View file

@ -0,0 +1,164 @@
---
fi:
about:
about_mastodon: Mastodon on <em>ilmainen, avoimeen lähdekoodiin perustuva</em> sosiaalinen verkosto. <em>Hajautettu</em> vaihtoehto kaupallisille alustoille, se välttää eiskit yhden yrityksen monopolisoinnin sinun viestinnässäsi. Valitse palvelin mihin luotat &mdash; minkä tahansa valitset, voit vuorovaikuttaa muiden kanssa. Kuka tahansa voi luoda Mastodon palvelimen ja ottaa osaa <em>sosiaaliseen verkkoon</em> saumattomasti.
about_this: Tietoja tästä palvelimesta
apps: Ohjelmat
business_email: 'Business e-mail:'
contact: Ota yhteyttä
description_headline: Mikä on %{domain}?
domain_count_after: muut palvelimet
domain_count_before: Yhdistyneenä
features:
api: Avoin API ohjelmille ja palveluille
blocks: Rikkaat esto ja hiljennys työkalut
characters: 500 kirjainta per viesti
chronology: Aikajana on kronologisessa järjestyksessä
ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa'
gifv: GIFV settejä ja lyhyitä videoita
privacy: Julkaisu kohtainen yksityisyys aseuts
public: Julkiset aikajanat
features_headline: Mikä erottaa Mastodonin muista
get_started: Aloita käyttö
links: Linkit
other_instances: muuhun palvelimeen
source_code: Lähdekoodi
status_count_after: statusta
status_count_before: Ovat luoneet
terms: Ehdot
user_count_after: käyttäjää
user_count_before: Koti käyttäjälle
accounts:
follow: Seuraa
followers: Seuraajat
following: Seuratut
nothing_here: Täällä ei ole mitään!
people_followed_by: Henkilöitä joita %{name} seuraa
people_who_follow: Henkilöt jotka seuraa %{name}
posts: Postaukset
remote_follow: Etäseuranta
unfollow: Lopeta seuraaminen
application_mailer:
settings: 'Muokkaa sähköposti asetuksia: %{link}'
signature: Mastodon ilmoituksia palvelimelta %{instance}
view: 'Katso:'
applications:
invalid_url: Annettu URL on väärä
auth:
change_password: Tunnukset
didnt_get_confirmation: Etkö saanut varmennus ohjeita?
forgot_password: Unohditko salasanasi?
login: Kirjaudu sisään
logout: Kirjaudu ulos
register: Rekisteröidy
resend_confirmation: Lähetä varmennus ohjeet uudestaan
reset_password: Palauta Salasana
set_new_password: Aseta uusi salasana
authorize_follow:
error: Valitettavasti tapahtui virhe etätilin haussa
follow: Seuraa
prompt_html: 'Sinä (<strong>%{self}</strong>) olet pyytänyt lupaa seurata:'
title: Seuraa %{acct}
datetime:
distance_in_words:
about_x_hours: "%{count}t"
about_x_months: "%{count}kk"
about_x_years: "%{count}v"
almost_x_years: "%{count}v"
half_a_minute: Juuri nyt
less_than_x_minutes: "%{count}m"
less_than_x_seconds: Juuri nyt
over_x_years: "%{count}v"
x_days: "%{count}pv"
x_minutes: "%{count}m"
x_months: "%{count}kk"
x_seconds: "%{count}s"
exports:
blocks: Estosi
csv: CSV
follows: Seurattavat
storage: Mediasi
generic:
changes_saved_msg: Muutokset onnistuneesti tallenettu!
powered_by: powered by %{link}
save_changes: Tallenna muutokset
validation_errors:
one: Jokin ei ole viellä oikein! Katso virhe alapuolelta
other: Jokin ei ole viellä oikein! Katso %{count} virhettä alapuolelta
imports:
preface: Voit tuoda tiettyä dataa kaikista ihmisistä joita seuraat tai estät tilillesi tälle palvelimelle tiedostoista, jotka on luotu toisella palvelimella
success: Datasi on onnistuneesti ladattu ja käsitellään pian
types:
blocking: Estetyt lista
following: Seuratut lista
upload: Lähetä
landing_strip_html: <strong>%{name}</strong> on käyttäjä domainilla <strong>%{domain}</strong>. Voit seurata tai vuorovaikuttaa heidän kanssaan jos sinulla on tili yleisessä verkossa. Jos sinulla ei ole tiliä, voit <a href="%{sign_up_path}">rekisteröityä täällä</a>.
notification_mailer:
digest:
body: 'Tässä on pieni yhteenveto palvelimelta %{instance} viimeksi kun olit paikalla %{since}:'
mention: "%{name} mainitsi sinut:"
new_followers_summary:
one: Olet saanut yhden uuden seuraajan! Jee!
other: Olet saanut %{count} uutta seuraajaa! Loistavaa!
subject:
one: "1 uusi ilmoitus viimeisen käyntisi jälkeen \U0001F418"
other: "%{count} uutta ilmoitusta viimeisen käyntisi jälkeen \U0001F418"
favourite:
body: 'Statuksestasi tykkäsi %{name}:'
subject: "%{name} tykkäsi sinun statuksestasi"
follow:
body: "%{name} seuraa nyt sinua!"
subject: "%{name} seuraa nyt sinua"
follow_request:
body: "%{name} on pyytänyt seurata sinua"
subject: 'Odottava seuraus pyyntö: %{name}'
mention:
body: 'Sinut mainitsi %{name} postauksessa:'
subject: Sinut mainitsi %{name}
reblog:
body: 'Sinun statustasi boostasi %{name}:'
subject: "%{name} boostasi statustasi"
pagination:
next: Seuraava
prev: Edellinen
remote_follow:
acct: Syötä sinun käyttäjänimesi@domain jos haluat seurata palvelimelta
missing_resource: Ei löydetty tarvittavaa uudelleenohjaavaa URL-linkkiä tilillesi
proceed: Siirry seuraamiseen
prompt: 'Sinä aiot seurata:'
settings:
authorized_apps: Valtuutetut ohjelmat
back: Takaisin Mastodoniin
edit_profile: Muokkaa profiilia
export: Datan vienti
import: Datan tuonti
preferences: Ominaisuudet
settings: Asetukset
two_factor_auth: Kaksivaiheinen tunnistus
statuses:
open_in_web: Avaa webissä
over_character_limit: sallittu kirjanmäärä %{max} ylitetty
show_more: Näytä lisää
visibilities:
private: Näytä vain seuraajille
public: Julkinen
unlisted: Julkinen, mutta älä näytä julkisella aikajanalla
stream_entries:
click_to_show: Klikkaa näyttääksesi
reblogged: boosted
sensitive_content: Herkkä materiaali
time:
formats:
default: "%b %d, %Y, %H:%M"
two_factor_auth:
description_html: Jos otat käyttöön <strong>kaksivaiheisen tunnistuksen</stron>, kirjautumiseen vaaditaan puhelin, joka voi generoida tokeneita kirjautumista varten.
disable: Poista käytöstä
enable: Ota käyttöön
instructions_html: "<strong>Skannaa tämä QR-koodi Google Authenticator tai samanlaiseen sovellukseen puhelimellasi</strong>. Tästä hetkestä lähtien ohjelma generoi koodin, mikä sinun tarvitsee syöttää sisäänkirjautuessa."
plaintext_secret_html: 'Plain-text secret: <samp>%{secret}</samp>'
warning: Jos et juuri nyt voi konfiguroida authenticator-applikaatiota juuri nyt, sinun pitäisi klikata "Poista käytöstä" tai et voi kirjautua sisään.
users:
invalid_email: Virheellinen sähköposti
invalid_otp_token: Virheellinen kaksivaihe tunnistus koodi
will_paginate:
page_gap: "&hellip;"

View file

@ -5,6 +5,7 @@ fr:
about_this: À propos de cette instance about_this: À propos de cette instance
apps: Applications apps: Applications
business_email: E-mail professionnel business_email: E-mail professionnel
closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. .
description_headline: Qu'est-ce que %{domain} ? description_headline: Qu'est-ce que %{domain} ?
domain_count_after: autres instances domain_count_after: autres instances
domain_count_before: Connectés à domain_count_before: Connectés à

164
config/locales/no.yml Normal file
View file

@ -0,0 +1,164 @@
---
'no':
about:
about_mastodon: Mastodon er et <em>gratis, åpen kildekode</em> sosialt nettverk. Et <em>desentralisert</em> alternativ til kommersielle plattformer. Slik kan det unngå risikoene ved å ha et enkelt selskap med monopol på din kommunikasjon. Velg en tjener du stoler på &mdash; uansett hvilken du velger så kan du interagere med alle andre. Alle kan kjøre sin egen Mastodon og delta sømløst i det sosiale nettverket.
about_this: Om denne instansen
apps: Applikasjoner
business_email: 'Bedriftsepost:'
contact: Kontakt
description_headline: Hva er %{domain}?
domain_count_after: andre instanser
domain_count_before: Koblet til
features:
api: Åpent api for applikasjoner og tjenester
blocks: Rikholdige blokkerings verktøy
characters: 500 tegn per post
chronology: Tidslinjer er kronologiske
ethics: 'Etisk design: Ingen reklame, ingen sporing'
gifv: GIFV sett og korte videoer
privacy: Finmaskete personvernsinnstillinger
public: Offentlige tidslinjer
features_headline: Hva skiller Mastodon fra andre sosiale nettverk
get_started: Kom i gang
links: Lenker
other_instances: Andre instanser
source_code: Kildekode
status_count_after: statuser
status_count_before: Hvem skrev
terms: Betingelser
user_count_after: brukere
user_count_before: Hjem til
accounts:
follow: Følg
followers: Følgere
following: Følger
nothing_here: Det er ingenting her!
people_followed_by: Folk som %{name} følger
people_who_follow: Folk som følger %{name}
posts: Poster
remote_follow: Følg fra andre instanser
unfollow: Avfølg
application_mailer:
settings: 'Endre foretrukne epost innstillinger: %{link}'
signature: Mastodon notiser fra %{instance}
view: 'Se:'
applications:
invalid_url: Den oppgitte URLen er ugyldig
auth:
change_password: Brukerdetaljer
didnt_get_confirmation: Fikk du ikke bekreftelsesmailen din?
forgot_password: Har du glemt passordet ditt?
login: Innlogging
logout: Logg ut
register: Bli med
resend_confirmation: Send bekreftelsesinstruksjoner på nytt
reset_password: Nullstill passord
set_new_password: Sett nytt passord
authorize_follow:
error: Uheldigvis så skjedde det en feil når vi prøvde å få tak i en konto fra en annen instans.
follow: Følg
prompt_html: 'Du (<strong>%{self}</strong>) har spurt om å følge:'
title: Følg %{acct}
datetime:
distance_in_words:
about_x_hours: "%{count}t"
about_x_months: "%{count}m"
about_x_years: "%{count}å"
almost_x_years: "%{count}å"
half_a_minute: Nylig
less_than_x_minutes: "%{count}min"
less_than_x_seconds: Nylig
over_x_years: "%{count}å"
x_days: "%{count}d"
x_minutes: "%{count}min"
x_months: "%{count}mo"
x_seconds: "%{count}s"
exports:
blocks: Du blokkerer
csv: CSV
follows: Du følger
storage: Media lagring
generic:
changes_saved_msg: Vellykket lagring av endringer!
powered_by: drevet av %{link}
save_changes: Lagre endringer
validation_errors:
one: Noe er ikke helt riktig ennå. Vær snill å se etter en gang til
other: Noe er ikke helt riktig ennå. Det er ennå %{count} feil å rette på
imports:
preface: Du kan importere data om mennesker du følger eller blokkerer inn til kontoen din på denne instansen, fra filer opprettet av eksporter fra andre instanser.
success: Din data ble mottatt og vil bli prosessert så fort som mulig.
types:
blocking: Blokkeringsliste
following: Følgeliste
upload: Opplastning
landing_strip_html: <strong>%{name}</strong> er en bruker på <strong>%{domain}</strong>. Du kan følge dem eller interagere med dem hvis du har en konto hvor som helst i fediverset. Hvis du ikke har en konto så kan du <a href="%{sign_up_path}">registrere deg her</a>.
notification_mailer:
digest:
body: 'Her er en kort oppsummering av hva du har gått glipp av på %{instance} siden du logget deg inn sist den %{since}:'
mention: "%{name} nevnte deg i:"
new_followers_summary:
one: Du har fått en ny følger. Jippi!
other: Du har fått %{count} nye følgere! Imponerende!
subject:
one: "1 ny hendelse siden ditt siste besøk \U0001F418"
other: "%{count} nye hendelser siden ditt siste besøk \U0001F418"
favourite:
body: 'Din status ble satt som favoritt av %{name}'
subject: "%{name} satte din status som favoritt."
follow:
body: "%{name} følger deg!"
subject: "%{name} følger deg"
follow_request:
body: "%{name} har spurt om å få lov til å følge deg"
subject: 'Ventende følger: %{name}'
mention:
body: 'Du ble nevnt av %{name} i:'
subject: Du ble nevnt av %{name}
reblog:
body: 'Din status fikk en boost av %{name}:'
subject: "%{name} ga din status en boost"
pagination:
next: Neste
prev: Forrige
remote_follow:
acct: Tast inn brukernavn@domene som du vil følge fra
missing_resource: Kunne ikke finne URLen for din konto
proceed: Fortsett med følging
prompt: 'Du kommer til å følge:'
settings:
authorized_apps: Autoriserte applikasjoner
back: Tilbake til Mastodon
edit_profile: Endre profil
export: Data eksport
import: Importer
preferences: Foretrukne valg
settings: Innstillinger
two_factor_auth: To-faktor autentisering
statuses:
open_in_web: Åpne i nettleser
over_character_limit: tegngrense på %{max} overskredet
show_more: Vis mer
visibilities:
private: Vis kun til følgere
public: Offentlig
unlisted: Offentlig, men vis ikke på offentlig tidslinje
stream_entries:
click_to_show: Klikk for å vise
reblogged: boostet
sensitive_content: Sensitivt innhold
time:
formats:
default: "%d, %b %Y, %H:%M"
two_factor_auth:
description_html: Hvis du skru på <strong>tofaktor autentisering</strong> vil innlogging kreve at du har telefonen din, som vil generere koder som du må taste inn.
disable: Skru av
enable: Skru på
instructions_html: "<strong>Scan denne QR-koden i Google Authenticator eller en lignende app på telefonen din</strong>. Fra nå av så vil denne applikasjonen generere koder for deg som skal brukes under innlogging"
plaintext_secret_html: 'Plain-text secret: <samp>%{secret}</samp>'
warning: Hvis du ikke kan konfigurere en autentikatorapp nå, så bør du trykke "Skru av"; ellers vil du ikke kunne logge inn.
users:
invalid_email: E-post addressen er ugyldig
invalid_otp_token: Ugyldig two-faktor kode
will_paginate:
page_gap: "&hellip;"

View file

@ -38,7 +38,7 @@ en:
follow: Send e-mail when someone follows you follow: Send e-mail when someone follows you
follow_request: Send e-mail when someone requests to follow you follow_request: Send e-mail when someone requests to follow you
mention: Send e-mail when someone mentions you mention: Send e-mail when someone mentions you
reblog: Send e-mail when someone reblogs your status reblog: Send e-mail when someone boosts your status
'no': 'No' 'no': 'No'
required: required:
mark: "*" mark: "*"

View file

@ -0,0 +1,46 @@
---
fi:
simple_form:
hints:
defaults:
avatar: PNG, GIF tai JPG. Korkeintaan 2MB. Skaalataan kokoon 120x120px
display_name: Korkeintaan 30 merkkiä
header: PNG, GIF tai JPG. Korkeintaan 2MB. Skaalataan kokoon 700x335px
locked: Vaatii sinun manuaalisesti hyväksymään seuraajat ja asettaa julkaisujen yksityisyyden vain seuraajille
note: Korkeintaan 160 merkkiä
imports:
data: CSV tiedosto tuotu toiselta Mastodon palvelimelta
labels:
defaults:
avatar: Avatar
confirm_new_password: Varmista uusi salasana
confirm_password: Varmista salasana
current_password: Nykyinen salasana
data: Data
display_name: Näykyvä nimi
email: Sähköpostiosoite
header: Header
locale: Kieli
locked: Tee tilistä yksityinen
new_password: Uusi salasana
note: Bio
otp_attempt: Kaksivaiheinen koodi
password: Salasana
setting_default_privacy: Julkaisun yksityisyys
type: Tuonti tyyppi
username: Käyttäjänimi
interactions:
must_be_follower: Estä ilmoitukset käyttäjiltä jotka eivät seuraa sinua
must_be_following: Estä ilmoitukset käyttäjiltä joita et seuraa
notification_emails:
digest: Send digest e-mails
favourite: Lähetä s-posti kun joku tykkää statuksestasi
follow: Lähetä s-posti kun joku seuraa sinua
follow_request: Lähetä s-posti kun joku pyytää seurata sinua
mention: Lähetä s-posti kun joku mainitsee sinut
reblog: Lähetä s-posti kun joku buustaa julkaisusi
'no': 'Ei'
required:
mark: "*"
text: vaaditaan
'yes': 'Kyllä'

View file

@ -0,0 +1,46 @@
---
'no':
simple_form:
hints:
defaults:
avatar: PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 120x120px
display_name: Maksimalt 30 tegn
header: PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 700x335px
locked: Krever at du manuelt godkjenner følgere og setter standard beskyttelse av poster til kun-følgere
note: Maksimalt 160 tegn
imports:
data: CSV fil eksportert fra en annen Mastodon instans
labels:
defaults:
avatar: Avatar
confirm_new_password: Bekreft nytt passord
confirm_password: Bekreft passord
current_password: Nåværende passord
data: Data
display_name: Visningsnavn
email: E-post adresse
header: Header
locale: Språk
locked: Endre konto til privat
new_password: Nytt passord
note: Biografi
otp_attempt: To-faktor kode
password: Passord
setting_default_privacy: Leserettigheter for poster
type: Importeringstype
username: Brukernavn
interactions:
must_be_follower: Blokker meldinger fra ikke-følgere
must_be_following: Blokker meldinger fra folk du ikke følger
notification_emails:
digest: Send oppsummerings eposter
favourite: Send e-post når noen setter din status som favoritt
follow: Send e-post når noen følger deg
follow_request: Send e-post når noen spør om å få følge deg
mention: Send e-post når noen nevner deg
reblog: Send e-post når noen reblogger din status
'no': 'Nei'
required:
mark: "*"
text: påkrevd
'yes': 'Ja'

View file

@ -14,11 +14,11 @@ SimpleNavigation::Configuration.run do |navigation|
settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
end end
primary.item :admin, safe_join([fa_icon('cogs fw'), 'Administration']), admin_accounts_url, if: proc { current_user.admin? } do |admin| primary.item :admin, safe_join([fa_icon('cogs fw'), 'Administration']), admin_reports_url, if: proc { current_user.admin? } do |admin|
admin.item :reports, safe_join([fa_icon('flag fw'), 'Reports']), admin_reports_url, highlights_on: %r{/admin/reports} admin.item :reports, safe_join([fa_icon('flag fw'), 'Reports']), admin_reports_url, highlights_on: %r{/admin/reports}
admin.item :accounts, safe_join([fa_icon('users fw'), 'Accounts']), admin_accounts_url, highlights_on: %r{/admin/accounts} admin.item :accounts, safe_join([fa_icon('users fw'), 'Accounts']), admin_accounts_url, highlights_on: %r{/admin/accounts}
admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), 'PubSubHubbub']), admin_pubsubhubbub_index_url admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), 'PubSubHubbub']), admin_pubsubhubbub_index_url
admin.item :domain_blocks, safe_join([fa_icon('lock fw'), 'Domain Blocks']), admin_domain_blocks_url admin.item :domain_blocks, safe_join([fa_icon('lock fw'), 'Domain Blocks']), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}
admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url
admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url
admin.item :settings, safe_join([fa_icon('cogs fw'), 'Site Settings']), admin_settings_url admin.item :settings, safe_join([fa_icon('cogs fw'), 'Site Settings']), admin_settings_url

View file

@ -77,7 +77,7 @@ Rails.application.routes.draw do
namespace :admin do namespace :admin do
resources :pubsubhubbub, only: [:index] resources :pubsubhubbub, only: [:index]
resources :domain_blocks, only: [:index, :create] resources :domain_blocks, only: [:index, :new, :create]
resources :settings, only: [:index, :update] resources :settings, only: [:index, :update]
resources :reports, only: [:index, :show] do resources :reports, only: [:index, :show] do

View file

@ -5,6 +5,8 @@ defaults: &defaults
site_extended_description: '' site_extended_description: ''
site_contact_username: '' site_contact_username: ''
site_contact_email: '' site_contact_email: ''
open_registrations: true
closed_registrations_message: ''
notification_emails: notification_emails:
follow: false follow: false
reblog: false reblog: false
@ -15,6 +17,7 @@ defaults: &defaults
interactions: interactions:
must_be_follower: false must_be_follower: false
must_be_following: false must_be_following: false
development: development:
<<: *defaults <<: *defaults

View file

@ -0,0 +1,5 @@
class AddActionTakenByAccountIdToReports < ActiveRecord::Migration[5.0]
def change
add_column :reports, :action_taken_by_account_id, :integer
end
end

View file

@ -0,0 +1,5 @@
class AddIndexOnMentionsStatusId < ActiveRecord::Migration[5.0]
def change
add_index :mentions, :status_id
end
end

View file

@ -10,7 +10,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: 20170330164118) do ActiveRecord::Schema.define(version: 20170405112956) 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"
@ -127,6 +127,7 @@ ActiveRecord::Schema.define(version: 20170330164118) do
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.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree
t.index ["status_id"], name: "index_mentions_on_status_id", using: :btree
end end
create_table "mutes", force: :cascade do |t| create_table "mutes", force: :cascade do |t|
@ -201,13 +202,14 @@ ActiveRecord::Schema.define(version: 20170330164118) do
end end
create_table "reports", force: :cascade do |t| create_table "reports", force: :cascade do |t|
t.integer "account_id", null: false t.integer "account_id", null: false
t.integer "target_account_id", null: false t.integer "target_account_id", null: false
t.bigint "status_ids", default: [], null: false, array: true t.bigint "status_ids", default: [], null: false, array: true
t.text "comment", default: "", null: false t.text "comment", default: "", null: false
t.boolean "action_taken", default: false, null: false t.boolean "action_taken", default: false, 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.integer "action_taken_by_account_id"
end end
create_table "settings", force: :cascade do |t| create_table "settings", force: :cascade do |t|

View file

@ -33,7 +33,7 @@ services:
restart: always restart: always
build: . build: .
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq -q default -q mailers -q push command: bundle exec sidekiq -q default -q mailers -q pull -q push
depends_on: depends_on:
- db - db
- redis - redis

View file

@ -7,7 +7,7 @@ So, you have a working Mastodon instance... now what?
The following rake task: The following rake task:
rails mastodon:make_admin USERNAME=alice rake mastodon:make_admin USERNAME=alice
Would turn the local user "alice" into an admin. Would turn the local user "alice" into an admin.

View file

@ -8,6 +8,8 @@ Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.co
1. Click the above button. 1. Click the above button.
2. Fill in the options requested. 2. Fill in the options requested.
* You can use a .herokuapp.com domain, which will be simple to set up, or you can use a custom domain. If you want a custom domain and HTTPS, you will need to upgrade to a paid plan (to use Heroku's SSL features), or set up [CloudFlare](https://cloudflare.com) who offer free "Flexible SSL" (note: CloudFlare have some undefined limits on WebSockets. So far, no one has reported hitting concurrent connection limits). * You can use a .herokuapp.com domain, which will be simple to set up, or you can use a custom domain. If you want a custom domain and HTTPS, you will need to upgrade to a paid plan (to use Heroku's SSL features), or set up [CloudFlare](https://cloudflare.com) who offer free "Flexible SSL" (note: CloudFlare have some undefined limits on WebSockets. So far, no one has reported hitting concurrent connection limits).
* You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saaved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details. * You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details.
* If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests. * If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests.
3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard. 3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard.
You may need to use the `heroku` CLI application to run `USERNAME=yourUsername rails mastodon:make_admin` to make yourself an admin.

View file

@ -11,10 +11,23 @@ map $http_upgrade $connection_upgrade {
'' close; '' close;
} }
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
server { server {
listen 443 ssl; listen 443 ssl;
server_name example.com; server_name example.com;
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
@ -75,8 +88,9 @@ It is recommended to create a special user for mastodon on the server (you could
## General dependencies ## General dependencies
sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash - curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs apt-get intall nodejs
sudo npm install -g yarn sudo npm install -g yarn
## Redis ## Redis
@ -112,7 +126,7 @@ Then once `rbenv` is ready, run `rbenv install 2.3.1` to install the Ruby versio
You need the `git-core` package installed on your system. If it is so, from the `mastodon` user: You need the `git-core` package installed on your system. If it is so, from the `mastodon` user:
cd ~ cd ~
git clone https://github.com/Gargron/mastodon.git live git clone https://github.com/tootsuite/mastodon.git live
cd live cd live
Then you can proceed to install project dependencies: Then you can proceed to install project dependencies:
@ -132,7 +146,7 @@ Fill in the important data, like host/port of the redis database, host/port/user
rake secret rake secret
To get a random string. If you are setting up on one single server (most likely), then REDIS_HOST is localhost and `DB_HOST` is `/var/run/postgresql`, `DB_USER` is `mastodon` and `DB_NAME` is `mastodon_production` while `DB_PASS` is empty because this setup will use the ident authentication method (system user "mastodon" maps to postgres user "mastodon"). To get a random string. If you are setting up on one single server (most likely), then `REDIS_HOST` is localhost and `DB_HOST` is `/var/run/postgresql`, `DB_USER` is `mastodon` and `DB_NAME` is `mastodon_production` while `DB_PASS` is empty because this setup will use the ident authentication method (system user "mastodon" maps to postgres user "mastodon").
## Setup ## Setup
@ -180,7 +194,7 @@ User=mastodon
WorkingDirectory=/home/mastodon/live WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production" Environment="RAILS_ENV=production"
Environment="DB_POOL=5" Environment="DB_POOL=5"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q push ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
TimeoutSec=15 TimeoutSec=15
Restart=always Restart=always
@ -221,7 +235,7 @@ I recommend creating a couple cronjobs for the following tasks:
You may want to run `which bundle` first and copypaste that full path instead of simply `bundle` in the above commands because cronjobs usually don't have all the paths set. The time and intervals of when to run these jobs are up to you, but once every day should be enough for all. You may want to run `which bundle` first and copypaste that full path instead of simply `bundle` in the above commands because cronjobs usually don't have all the paths set. The time and intervals of when to run these jobs are up to you, but once every day should be enough for all.
You can edit the cronjob file for the `mastodon` user by running `sudo crontab -e mastodon` (outside of the mastodon user). You can edit the cronjob file for the `mastodon` user by running `sudo crontab -e -u mastodon` (outside of the mastodon user).
## Things to look out for when upgrading Mastodon ## Things to look out for when upgrading Mastodon

View file

@ -0,0 +1,13 @@
Scalingo guide
==============
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
1. Click the above button.
2. Fill in the options requested.
* You can use a .scalingo.io domain, which will be simple to set up, or you can use a custom domain.
* You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details.
* If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests.
3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard.
You may need to use the `scalingo` CLI application to run `USERNAME=yourUsername rails mastodon:make_admin` to make yourself an admin.

View file

@ -5,21 +5,48 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
| Name | Theme/Notes, if applicable | Open Registrations | IPv6 | | Name | Theme/Notes, if applicable | Open Registrations | IPv6 |
| -------------|-------------|---|---| | -------------|-------------|---|---|
| [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|No| | [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No|
| [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes|
| [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes|
| [mastodon.network](https://mastodon.network) |N/A|Yes|Yes|
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No| | [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
| [social.tchncs.de](https://social.tchncs.de)|N/A|Yes|No|
| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No| | [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No|
| [socially.constructed.space](https://socially.constructed.space) |Single user|No|No| | [socially.constructed.space](https://socially.constructed.space) |Single user|No|No|
| [epiktistes.com](https://epiktistes.com) |N/A|Yes|No| | [epiktistes.com](https://epiktistes.com) |N/A|Yes|No|
| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|Yes|No| | [fern.surgeplay.com](https://fern.surgeplay.com) |Federates everywhere, Minecraft-focused|Yes|No
| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|No|No|
| [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No| | [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No|
| [memetastic.space](https://memetastic.space) |Memes|Yes|No| | [memetastic.space](https://memetastic.space) |Memes|Yes|No|
| [social.diskseven.com](https://social.diskseven.com) |Single user|No|No (DNS entry but no response)| | [social.diskseven.com](https://social.diskseven.com) |Single user|No|Yes|
| [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No| | [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No|
| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes| | [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes|
| [social.targaryen.house](https://social.targaryen.house) |N/A|Yes|No| | [mastodon.partipirate.org](https://mastodon.partipirate.org) |French Pirate Party Instance - Politics and stuff|Yes|No|
| [social.mashek.net](https://social.mashek.net) |Themed and customised for Mashekstein Labs community. Selectively federates.|Yes|No| | [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes|
| [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes| | [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes|
| [mstdn.io](https://mstdn.io) |N/A|Yes|Yes|
| [social.imirhil.fr](https://social.imirhil.fr) |N/A|No|Yes| | [social.imirhil.fr](https://social.imirhil.fr) |N/A|No|Yes|
| [social.wxcafe.net](https://social.wxcafe.net) |Open registrations, queer people, activists, safe as much as possible |Yes|Yes|
| [octodon.social](https://octodon.social) |Open registrations, federates everywhere, cutest instance yet|Yes|Yes|
| [mastodon.club](https://mastodon.club)|Open Registration, Open Federation, Mostly Canadians|Yes|No|
| [mastodon.irish](https://mastodon.irish)|Open Registration|Yes|No|
| [hostux.social](https://hostux.social) |N/A|Yes|Yes|
| [social.alex73630.xyz](https://social.alex73630.xyz) |Francophones|Yes|Yes|
| [oc.todon.fr](https://oc.todon.fr) |Modérée et principalement francophone, pas de tolérances pour misogynie/LGBTphobies/validisme/etc.|Yes|Yes|
| [maly.io](https://maly.io) |N/A|Yes|No|
| [social.lou.lt](https://social.lou.lt) |N/A|Yes|No|
| [mastodon.ninetailed.uk](https://mastodon.ninetailed.uk) |N/A|Yes|No|
| [soc.louiz.org](https://soc.louiz.org) |"Coucou"|Yes|No|
| [7nw.eu](https://7nw.eu) |N/A|Yes|No|
| [mastodon.gougere.fr](https://mastodon.gougere.fr)|N/A|Yes|No|
| [aleph.land](https://aleph.land)|N/A|Yes|No|
| [share.elouworld.org](https://share.elouworld.org)|N/A|No|No|
| [social.lkw.tf](https://social.lkw.tf)|N/A|No|No|
| [manowar.social](https://manowar.social)|N/A|No|No|
| [social.ballpointcarrot.net](https://social.ballpointcarrot.net)|N/A|No|No|
| [social.nasqueron.org](https://social.nasqueron.org) |Dreamers, open source developers, free culture|Yes|Yes|
| [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes|
| [mastodon.cc](https://mastodon.cc)|Art|Yes|No|
| [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No|
| [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes|No|
Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request). Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).

View file

@ -26,17 +26,17 @@ Mastodon User's Guide
## Intro ## Intro
Mastodon is a social network application based on the GNU Social protocol. It behaves a lot like other social networks, especially Twitter, with one key difference - it is open-source and anyone can start their own server (also called an "instance"), and users of any instance can interact freely with those of other instances (called "federation"). Thus, it is possible for small communities to set up their own servers to use amongst themselves while also allowing interaction with other communities. Mastodon is a social network application based on the GNU Social protocol. It behaves a lot like other social networks, especially Twitter, with one key difference - it is open-source and anyone can start their own server (also called an "*instance*"), and users of any instance can interact freely with those of other instances (called "*federation*"). Thus, it is possible for small communities to set up their own servers to use amongst themselves while also allowing interaction with other communities.
#### Decentralization and Federation #### Decentralization and Federation
Mastodon is a system decentralized through a concept called "federation" - rather than depending on a single person or organization to run its infrastructure, anyone can download and run the software and run their own server. Federation means different Mastodon servers can interact with each other seamlessly, similar to e.g. e-mail. Mastodon is a system decentralized through a concept called "*federation*" - rather than depending on a single person or organization to run its infrastructure, anyone can download and run the software and run their own server. Federation means different Mastodon servers can interact with each other seamlessly, similar to e.g. e-mail.
As such, anyone can download Mastodon and e.g. run it for a small community of people, but any user registered on that instance can follow and send and read posts from other Mastodon instances (as well as servers running other GNU Social-compatible services). This means that not only is users' data not inherently owned by a company with an interest in selling it to advertisers, but also that if any given server shuts down its users can set up a new one or migrate to another instance, rather than the entire service being lost. As such, anyone can download Mastodon and e.g. run it for a small community of people, but any user registered on that instance can follow and send and read posts from other Mastodon instances (as well as servers running other GNU Social-compatible services). This means that not only is users' data not inherently owned by a company with an interest in selling it to advertisers, but also that if any given server shuts down its users can set up a new one or migrate to another instance, rather than the entire service being lost.
Within each Mastodon instance, usernames just appear as `@username`, similar to other services such as Twitter. Users from other instances appear, and can be searched for and followed, as `@user@servername.ext` - so e.g. `@gargron` on the `mastodon.social` instance can be followed from other instances as `@gargron@mastodon.social`). Within each Mastodon instance, usernames just appear as `@username`, similar to other services such as Twitter. Users from other instances appear, and can be searched for and followed, as `@user@servername.ext` - so e.g. `@gargron` on the `mastodon.social` instance can be followed from other instances as `@gargron@mastodon.social`).
Posts from users on external instances are "federated" into the local one, i.e. if `user1@mastodon1` follows `user2@gnusocial2`, any posts `user2@gnusocial2` makes appear in both `user1@mastodon`'s Home feed and the public timeline on the `mastodon1` server. Mastodon server administrators have some control over this and can exclude users' posts from appearing on the public timeline; post privacy settings from users on Mastodon instances also affect this, see below in the [Toot Privacy](User-guide.md#toot-privacy) section. Posts from users on external instances are "*federated*" into the local one, i.e. if `user1@mastodon1` follows `user2@gnusocial2`, any posts `user2@gnusocial2` makes appear in both `user1@mastodon`'s Home feed and the public timeline on the `mastodon1` server. Mastodon server administrators have some control over this and can exclude users' posts from appearing on the public timeline; post privacy settings from users on Mastodon instances also affect this, see below in the [Toot Privacy](User-guide.md#toot-privacy) section.
## Getting Started ## Getting Started
@ -160,13 +160,13 @@ Toot privacy is handled independently of account privacy, and individually for e
**Unlisted** toots are toggled with the "Do not display in public timeline" option in the Compose pane. They are visible to anyone following you and appear on your profile page to the public even without a Mastodon login, but do *not* appear to anyone viewing the Public Timeline while logged into Mastodon. **Unlisted** toots are toggled with the "Do not display in public timeline" option in the Compose pane. They are visible to anyone following you and appear on your profile page to the public even without a Mastodon login, but do *not* appear to anyone viewing the Public Timeline while logged into Mastodon.
**Private** toots, finally, are toggled with the "Mark as private" switch. Private toots do not appear in the public timeline nor on your profile page to anyone viewing it unless they are on your Followers list. This means the option is of very limited use if your account is not also set to be private (as anyone can follow you without confirmation and thus see your private toots). However the separation of this means that if you *do* set your entire account to private, you can switch this option off on a toot to make unlisted or even public toots from your otherwise private account. **Private** toots, finally, are toggled with the "Mark as private" switch. Private toots do not appear in the public timeline nor on your profile page to anyone viewing it unless they are on your Followers list. This means the option is of very limited use if your account is not also set to be private (as anyone can follow you without confirmation and thus see your private toots). However the separation of this means that if you *do* set your entire account to private, you can switch this option off on a toot to make unlisted or even public toots from your otherwise private account. Private posts are not encrypted. Make sure you trust your instance admin not to just read your private posts on the back-end.
Private toots do not federate to other instances, unless you @mention a remote user. In this case, they will federate to their instance *and may appear there PUBLICLY*. A warning will be displayed if you're composing a private toot that will federate to another instance. Private toots do not federate to other instances, unless you @mention a remote user. In this case, they will federate to their instance *and may appear there PUBLICLY*. A warning will be displayed if you're composing a private toot that will federate to another instance.
Private toots cannot be boosted. If someone you follow makes a private toot, it will appear in your timeline with a padlock icon in place of the Boost icon. **NOTE** that remote instances may not respect this. Private toots cannot be boosted. If someone you follow makes a private toot, it will appear in your timeline with a padlock icon in place of the Boost icon. **NOTE** that remote instances may not respect this.
**Direct** messages are only visible to users you have @mentioned in them. This does *not* federate to protect your privacy (as other instances may ignore the "Direct" status and display the messages as public if they were to receive them), even if you have @mentioned a remote user. **Direct** posts are only visible to users you have @mentioned in them and cannot be boosted. Like with private posts, you should be mindful that the remote instance may not respect this protocol. If you are discussing a sensitive matter you should move the conversation off of Mastodon.
To summarise: To summarise:
@ -175,7 +175,7 @@ Toot Privacy | Visible on Profile | Visible on Public Timeline | Federates to ot
Public | Anyone incl. anonymous viewers | Yes | Yes Public | Anyone incl. anonymous viewers | Yes | Yes
Unlisted | Anyone incl. anonymous viewers | No | Yes Unlisted | Anyone incl. anonymous viewers | No | Yes
Private | Followers only | No | Only remote @mentions Private | Followers only | No | Only remote @mentions
Direct | No | No | No Direct | No | No | Only remote @mentions
#### Blocking #### Blocking

View file

@ -310,7 +310,7 @@ Returns a [Status](#status).
#### Getting status context: #### Getting status context:
GET /api/v1/statuses/:id/contexts GET /api/v1/statuses/:id/context
Returns a [Context](#context). Returns a [Context](#context).

87
scalingo.json Normal file
View file

@ -0,0 +1,87 @@
{
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/johnsudaar/mastodon",
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png",
"env": {
"LOCAL_DOMAIN": {
"description": "The domain that your Mastodon instance will run on (this can be appname.scalingo.io or a custom domain)",
"required": true
},
"LOCAL_HTTPS": {
"description": "Will your domain support HTTPS? (Automatic for *.scalingo.io, requires manual configuration for custom domains)",
"value": "true",
"required": true
},
"PAPERCLIP_SECRET": {
"description": "The secret key for storing media files",
"generator": "secret"
},
"SECRET_KEY_BASE": {
"description": "The secret key base",
"generator": "secret"
},
"SINGLE_USER_MODE": {
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
"value": "false",
"required": true
},
"S3_ENABLED": {
"description": "Should Mastodon use Amazon S3 for storage? This is highly recommended, as Scalingo does not have persistent file storage (files will be lost).",
"value": "true",
"required": false
},
"S3_BUCKET": {
"description": "Amazon S3 Bucket",
"required": false
},
"S3_REGION": {
"description": "Amazon S3 region that the bucket is located in",
"required": false
},
"AWS_ACCESS_KEY_ID": {
"description": "Amazon S3 Access Key",
"required": false
},
"AWS_SECRET_ACCESS_KEY": {
"description": "Amazon S3 Secret Key",
"required": false
},
"SMTP_SERVER": {
"description": "Hostname for SMTP server, if you want to enable email",
"required": false
},
"SMTP_PORT": {
"description": "Port for SMTP server",
"required": false
},
"SMTP_LOGIN": {
"description": "Username for SMTP server",
"required": false
},
"SMTP_PASSWORD": {
"description": "Password for SMTP server",
"required": false
},
"SMTP_DOMAIN": {
"description": "Domain for SMTP server. Will default to instance domain if blank.",
"required": false
},
"SMTP_FROM_ADDRESS": {
"description": "Address to send emails from",
"required": false
},
"BUILDPACK_URL": {
"description": "Internal scalingo configuration",
"required": true,
"value": "https://github.com/Scalingo/multi-buildpack.git"
}
},
"scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
},
"addons": [
"scalingo-postgresql",
"scalingo-redis"
]
}

Some files were not shown because too many files have changed in this diff Show more