mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 22:15:23 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
3e4eb9c95f
128 changed files with 2013 additions and 519 deletions
2
.buildpacks
Normal file
2
.buildpacks
Normal file
|
@ -0,0 +1,2 @@
|
|||
https://github.com/Scalingo/nodejs-buildpack
|
||||
https://github.com/Scalingo/ruby-buildpack
|
|
@ -5,3 +5,4 @@ public/assets
|
|||
node_modules
|
||||
storybook
|
||||
neo4j
|
||||
vendor/bundle
|
||||
|
|
|
@ -22,8 +22,14 @@ OTP_SECRET=
|
|||
# SINGLE_USER_MODE=true
|
||||
# Prevent registrations with following e-mail domains
|
||||
# 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
|
||||
|
||||
# Optionally change default language
|
||||
# DEFAULT_LOCALE=de
|
||||
|
||||
# E-mail configuration
|
||||
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=
|
||||
|
@ -42,6 +48,16 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||
# S3_PROTOCOL=http
|
||||
# S3_HOSTNAME=192.168.1.123:9000
|
||||
|
||||
# S3 (Minio Config (optional) Please check Minio instance for details)
|
||||
# S3_ENABLED=true
|
||||
# S3_BUCKET=
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# S3_REGION=
|
||||
# S3_PROTOCOL=https
|
||||
# S3_HOSTNAME=
|
||||
# S3_ENDPOINT=
|
||||
|
||||
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
|
||||
# S3_CLOUDFRONT_HOST=
|
||||
|
||||
|
|
5
.slugignore
Normal file
5
.slugignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
docs/
|
||||
spec/
|
||||
storybook/
|
12
Dockerfile
12
Dockerfile
|
@ -1,11 +1,16 @@
|
|||
FROM ruby:2.3.1-alpine
|
||||
|
||||
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||
description="A GNU Social-compatible microblogging server"
|
||||
|
||||
ENV RAILS_ENV=production \
|
||||
NODE_ENV=production
|
||||
|
||||
EXPOSE 3000 4000
|
||||
|
||||
WORKDIR /mastodon
|
||||
|
||||
COPY . /mastodon
|
||||
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
||||
|
||||
RUN BUILD_DEPS=" \
|
||||
postgresql-dev \
|
||||
|
@ -24,8 +29,11 @@ RUN BUILD_DEPS=" \
|
|||
&& npm install -g npm@3 && npm install -g yarn \
|
||||
&& bundle install --deployment --without test development \
|
||||
&& yarn \
|
||||
&& npm cache clean \
|
||||
&& yarn cache clean \
|
||||
&& npm -g cache clean \
|
||||
&& apk del $BUILD_DEPS \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
COPY . /mastodon
|
||||
|
||||
VOLUME /mastodon/public/system /mastodon/public/assets
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -8,8 +8,6 @@ gem 'sass-rails', '~> 5.0'
|
|||
gem 'uglifier', '>= 1.3.0'
|
||||
gem 'coffee-rails', '~> 4.1.0'
|
||||
gem 'jquery-rails'
|
||||
gem 'jbuilder', '~> 2.0'
|
||||
gem 'sdoc', '~> 0.4.0', group: :doc
|
||||
gem 'puma'
|
||||
|
||||
gem 'hamlit-rails'
|
||||
|
@ -36,6 +34,7 @@ gem 'doorkeeper'
|
|||
gem 'rabl'
|
||||
gem 'rqrcode'
|
||||
gem 'twitter-text'
|
||||
gem 'ox'
|
||||
gem 'oj'
|
||||
gem 'hiredis'
|
||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||
|
@ -67,9 +66,10 @@ group :development, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'faker'
|
||||
gem 'rspec-sidekiq'
|
||||
gem 'simplecov', require: false
|
||||
gem 'webmock'
|
||||
gem 'rspec-sidekiq'
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
|
16
Gemfile.lock
16
Gemfile.lock
|
@ -149,6 +149,8 @@ GEM
|
|||
erubis (2.7.0)
|
||||
execjs (2.7.0)
|
||||
fabrication (2.15.2)
|
||||
faker (1.6.6)
|
||||
i18n (~> 0.5)
|
||||
fast_blank (1.0.0)
|
||||
font-awesome-rails (4.6.3.1)
|
||||
railties (>= 3.2, < 5.1)
|
||||
|
@ -196,9 +198,6 @@ GEM
|
|||
parser (>= 2.2.3.0)
|
||||
term-ansicolor (>= 1.3.2)
|
||||
terminal-table (>= 1.5.1)
|
||||
jbuilder (2.6.0)
|
||||
activesupport (>= 3.0.0, < 5.1)
|
||||
multi_json (~> 1.2)
|
||||
jmespath (1.3.1)
|
||||
jquery-rails (4.1.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
|
@ -229,7 +228,6 @@ GEM
|
|||
mimemagic (0.3.2)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.10.1)
|
||||
multi_json (1.12.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (4.0.1)
|
||||
|
@ -242,6 +240,7 @@ GEM
|
|||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
ox (2.4.11)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
|
@ -308,8 +307,6 @@ GEM
|
|||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.1.0)
|
||||
rake (12.0.0)
|
||||
rdoc (4.2.2)
|
||||
json (~> 1.4)
|
||||
react-rails (1.10.0)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
coffee-script-source (~> 1.8)
|
||||
|
@ -379,9 +376,6 @@ GEM
|
|||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
sdoc (0.4.1)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
rdoc (~> 4.0)
|
||||
sidekiq (4.2.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
|
@ -470,6 +464,7 @@ DEPENDENCIES
|
|||
doorkeeper
|
||||
dotenv-rails
|
||||
fabrication
|
||||
faker
|
||||
fast_blank
|
||||
font-awesome-rails
|
||||
fuubar
|
||||
|
@ -480,7 +475,6 @@ DEPENDENCIES
|
|||
http
|
||||
httplog
|
||||
i18n-tasks (~> 0.9.6)
|
||||
jbuilder (~> 2.0)
|
||||
jquery-rails
|
||||
letter_opener
|
||||
letter_opener_web
|
||||
|
@ -489,6 +483,7 @@ DEPENDENCIES
|
|||
nokogiri
|
||||
oj
|
||||
ostatus2
|
||||
ox
|
||||
paperclip (~> 5.1)
|
||||
paperclip-av-transcoder
|
||||
pg
|
||||
|
@ -511,7 +506,6 @@ DEPENDENCIES
|
|||
rubocop
|
||||
ruby-oembed
|
||||
sass-rails (~> 5.0)
|
||||
sdoc (~> 0.4.0)
|
||||
sidekiq
|
||||
sidekiq-unique-jobs
|
||||
simple-navigation
|
||||
|
|
2
Procfile
2
Procfile
|
@ -1,2 +1,2 @@
|
|||
web: bundle exec puma -C config/puma.rb
|
||||
worker: bundle exec sidekiq -q default -q mailers -q push
|
||||
worker: bundle exec sidekiq -q default -q push -q pull -q mailers
|
||||
|
|
|
@ -65,6 +65,8 @@ Consult the example configuration file, `.env.production.sample` for the full li
|
|||
|
||||
## Running with Docker and Docker-Compose
|
||||
|
||||
[![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
|
||||
|
||||
The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
|
||||
|
||||
docker-compose build
|
||||
|
@ -117,6 +119,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.
|
||||
|
||||
## 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)
|
||||
|
||||
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
|
||||
|
|
16
Vagrantfile
vendored
16
Vagrantfile
vendored
|
@ -84,6 +84,16 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
config.vm.provider :virtualbox do |vb|
|
||||
vb.name = "mastodon"
|
||||
vb.customize ["modifyvm", :id, "--memory", "1024"]
|
||||
|
||||
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
|
||||
# https://github.com/mitchellh/vagrant/issues/1172
|
||||
vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"]
|
||||
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off"]
|
||||
|
||||
# Use "virtio" network interfaces for better performance.
|
||||
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
|
||||
vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
|
||||
|
||||
end
|
||||
|
||||
config.vm.hostname = "mastodon.dev"
|
||||
|
@ -91,12 +101,14 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
# This uses the vagrant-hostsupdater plugin, and lets you
|
||||
# access the development site at http://mastodon.dev.
|
||||
# To install:
|
||||
# $ vagrant plugin install hostsupdater
|
||||
# $ vagrant plugin install vagrant-hostsupdater
|
||||
if defined?(VagrantPlugins::HostsUpdater)
|
||||
config.vm.network :private_network, ip: "192.168.42.42"
|
||||
config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio"
|
||||
config.hostsupdater.remove_on_suspend = false
|
||||
end
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
|
||||
|
||||
# Otherwise, you can access the site at http://localhost:3000
|
||||
config.vm.network :forwarded_port, guest: 80, host: 3000
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 874 KiB After Width: | Height: | Size: 209 KiB |
|
@ -47,6 +47,7 @@ import pt from 'react-intl/locale-data/pt';
|
|||
import hu from 'react-intl/locale-data/hu';
|
||||
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 getMessagesForLocale from '../locales';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import createStream from '../stream';
|
||||
|
@ -59,7 +60,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
|
|||
basename: '/web'
|
||||
});
|
||||
|
||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi]);
|
||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
|
||||
|
||||
const Mastodon = React.createClass({
|
||||
|
||||
|
|
68
app/assets/javascripts/components/locales/eo.jsx
Normal file
68
app/assets/javascripts/components/locales/eo.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
const eo = {
|
||||
"column_back_button.label": "Reveni",
|
||||
"lightbox.close": "Fermi",
|
||||
"loading_indicator.label": "Ŝarĝanta...",
|
||||
"status.mention": "Mencii @{name}",
|
||||
"status.delete": "Forigi",
|
||||
"status.reply": "Respondi",
|
||||
"status.reblog": "Diskonigi",
|
||||
"status.favourite": "Favori",
|
||||
"status.reblogged_by": "{name} diskonigita",
|
||||
"status.sensitive_warning": "Tikla enhavo",
|
||||
"status.sensitive_toggle": "Alklaki por vidi",
|
||||
"video_player.toggle_sound": "Aktivigi sonojn",
|
||||
"account.mention": "Mencii @{name}",
|
||||
"account.edit_profile": "Redakti la profilon",
|
||||
"account.unblock": "Malbloki @{name}",
|
||||
"account.unfollow": "Malsekvi",
|
||||
"account.block": "Bloki @{name}",
|
||||
"account.follow": "Sekvi",
|
||||
"account.posts": "Mesaĝoj",
|
||||
"account.follows": "Sekvatoj",
|
||||
"account.followers": "Sekvantoj",
|
||||
"account.follows_you": "Sekvas vin",
|
||||
"account.requested": "Atendas aprobon",
|
||||
"getting_started.heading": "Por komenci",
|
||||
"getting_started.about_addressing": "Vi povas sekvi homojn se vi konas la uzantnomon kaj domajnon tajpinte retpoŝtecan adreson en la serĉilon.",
|
||||
"getting_started.about_shortcuts": "Se la celita uzanto troviĝas en la sama domajno de vi, uzi nur la uzantnomon sufiĉos. La sama regulo validas por mencii aliajn uzantojn en mesaĝo.",
|
||||
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.",
|
||||
"column.home": "Hejmo",
|
||||
"column.community": "Loka tempolinio",
|
||||
"column.public": "Fratara tempolinio",
|
||||
"column.notifications": "Sciigoj",
|
||||
"tabs_bar.compose": "Ekskribi",
|
||||
"tabs_bar.home": "Hejmo",
|
||||
"tabs_bar.mentions": "Sciigoj",
|
||||
"tabs_bar.public": "Fratara tempolinio",
|
||||
"tabs_bar.notifications": "Sciigoj",
|
||||
"compose_form.placeholder": "Pri kio vi pensas?",
|
||||
"compose_form.publish": "Hup",
|
||||
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
|
||||
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
|
||||
"compose_form.private": "Marki ke la enhavo estas privata",
|
||||
"compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.",
|
||||
"compose_form.unlisted": "Ne afiŝi en publikaj tempolinioj",
|
||||
"navigation_bar.edit_profile": "Redakti la profilon",
|
||||
"navigation_bar.preferences": "Preferoj",
|
||||
"navigation_bar.community_timeline": "Loka tempolinio",
|
||||
"navigation_bar.public_timeline": "Fratara tempolinio",
|
||||
"navigation_bar.logout": "Elsaluti",
|
||||
"reply_indicator.cancel": "Rezigni",
|
||||
"search.placeholder": "Serĉi",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Kradvorto",
|
||||
"upload_button.label": "Aldoni enhavaĵon",
|
||||
"upload_form.undo": "Malfari",
|
||||
"notification.follow": "{name} sekvis vin",
|
||||
"notification.favourite": "{name} favoris vian mesaĝon",
|
||||
"notification.reblog": "{name} diskonigis vian mesaĝon",
|
||||
"notification.mention": "{name} menciis vin",
|
||||
"notifications.column_settings.alert": "Retumilaj atentigoj",
|
||||
"notifications.column_settings.show": "Montri en kolono",
|
||||
"notifications.column_settings.follow": "Novaj sekvantoj:",
|
||||
"notifications.column_settings.favourite": "Favoroj:",
|
||||
"notifications.column_settings.mention": "Mencioj:",
|
||||
"notifications.column_settings.reblog": "Diskonigoj:",
|
||||
};
|
||||
|
||||
export default eo;
|
|
@ -5,9 +5,9 @@ const fi = {
|
|||
"status.mention": "Mainitse @{name}",
|
||||
"status.delete": "Poista",
|
||||
"status.reply": "Vastaa",
|
||||
"status.reblog": "Boostaa",
|
||||
"status.reblog": "Buustaa",
|
||||
"status.favourite": "Tykkää",
|
||||
"status.reblogged_by": "{name} boostattu",
|
||||
"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",
|
||||
|
@ -22,13 +22,13 @@ const fi = {
|
|||
"account.followers": "Seuraajia",
|
||||
"account.follows_you": "Seuraa sinua",
|
||||
"account.requested": "Odottaa hyväksyntää",
|
||||
"getting_started.heading": "Päästä alkuun",
|
||||
"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 githubissa {github}. {apps}.",
|
||||
"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": "Yhdistetty aikajana",
|
||||
"column.public": "Yleinen aikajana",
|
||||
"column.notifications": "Ilmoitukset",
|
||||
"tabs_bar.compose": "Luo",
|
||||
"tabs_bar.home": "Koti",
|
||||
|
@ -41,7 +41,7 @@ const fi = {
|
|||
"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ä julkisilla aikajanoilla",
|
||||
"compose_form.unlisted": "Älä näytä yleisillä aikajanoilla",
|
||||
"navigation_bar.edit_profile": "Muokkaa profiilia",
|
||||
"navigation_bar.preferences": "Ominaisuudet",
|
||||
"navigation_bar.community_timeline": "Paikallinen aikajana",
|
||||
|
@ -55,14 +55,14 @@ const fi = {
|
|||
"upload_form.undo": "Peru",
|
||||
"notification.follow": "{name} seurasi sinua",
|
||||
"notification.favourite": "{name} tykkäsi statuksestasi",
|
||||
"notification.reblog": "{name} boostasi statustasi",
|
||||
"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": "Boosteja:",
|
||||
"notifications.column_settings.reblog": "Buusteja:",
|
||||
};
|
||||
|
||||
export default fi;
|
||||
|
|
|
@ -6,6 +6,7 @@ import fr from './fr';
|
|||
import pt from './pt';
|
||||
import uk from './uk';
|
||||
import fi from './fi';
|
||||
import eo from './eo';
|
||||
|
||||
const locales = {
|
||||
en,
|
||||
|
@ -15,7 +16,8 @@ const locales = {
|
|||
fr,
|
||||
pt,
|
||||
uk,
|
||||
fi
|
||||
fi,
|
||||
eo
|
||||
};
|
||||
|
||||
export default function getMessagesForLocale (locale) {
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-shadow: 0 0 2px $color8;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
|
@ -128,6 +129,7 @@
|
|||
text-transform: uppercase;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
text-shadow: 0 0 2px $color8;
|
||||
}
|
||||
|
||||
.counter-number {
|
||||
|
@ -385,5 +387,6 @@
|
|||
.account__header__content {
|
||||
font-size: 14px;
|
||||
color: $color1;
|
||||
text-shadow: 0 0 2px $color8;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
@import 'variables';
|
||||
|
||||
.app-body{
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: darken($color4, 3%);
|
||||
font-family: inherit;
|
||||
|
|
|
@ -16,7 +16,8 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
format.atom do
|
||||
@entries = @account.stream_entries.order('id desc').where(activity_type: 'Status').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||
@entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
|
||||
end
|
||||
|
||||
format.activitystreams2
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Localized
|
||||
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
protect_from_forgery with: :exception
|
||||
|
@ -14,7 +16,6 @@ class ApplicationController < ActionController::Base
|
|||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||
|
||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
||||
before_action :set_locale
|
||||
before_action :set_user_activity
|
||||
before_action :check_suspension, if: :user_signed_in?
|
||||
|
||||
|
@ -28,12 +29,6 @@ class ApplicationController < ActionController::Base
|
|||
store_location_for(:user, request.url)
|
||||
end
|
||||
|
||||
def set_locale
|
||||
I18n.locale = current_user.try(:locale) || I18n.default_locale
|
||||
rescue I18n::InvalidLocale
|
||||
I18n.locale = I18n.default_locale
|
||||
end
|
||||
|
||||
def require_admin!
|
||||
redirect_to root_path unless current_user&.admin?
|
||||
end
|
||||
|
|
19
app/controllers/concerns/localized.rb
Normal file
19
app/controllers/concerns/localized.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Localized
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_locale
|
||||
end
|
||||
|
||||
def set_locale
|
||||
I18n.locale = current_user.try(:locale) || default_locale
|
||||
rescue I18n::InvalidLocale
|
||||
I18n.locale = default_locale
|
||||
end
|
||||
|
||||
def default_locale
|
||||
ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale }
|
||||
end
|
||||
end
|
|
@ -1,9 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
include Localized
|
||||
|
||||
skip_before_action :authenticate_resource_owner!
|
||||
|
||||
before_action :set_locale
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
|
||||
|
@ -12,10 +13,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
def store_current_location
|
||||
store_location_for(:user, request.url)
|
||||
end
|
||||
|
||||
def set_locale
|
||||
I18n.locale = current_user.try(:locale) || I18n.default_locale
|
||||
rescue I18n::InvalidLocale
|
||||
I18n.locale = I18n.default_locale
|
||||
end
|
||||
end
|
||||
|
|
16
app/controllers/oauth/authorized_applications_controller.rb
Normal file
16
app/controllers/oauth/authorized_applications_controller.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||
include Localized
|
||||
|
||||
skip_before_action :authenticate_resource_owner!
|
||||
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
|
||||
private
|
||||
|
||||
def store_current_location
|
||||
store_location_for(:user, request.url)
|
||||
end
|
||||
end
|
|
@ -8,6 +8,7 @@ class RemoteFollowController < ApplicationController
|
|||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new
|
||||
@remote_follow.acct = session[:remote_follow] if session.key?(:remote_follow)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -22,6 +23,8 @@ class RemoteFollowController < ApplicationController
|
|||
render(:new) && return
|
||||
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
|
||||
else
|
||||
render :new
|
||||
|
|
|
@ -19,7 +19,9 @@ class StreamEntriesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
format.atom
|
||||
format.atom do
|
||||
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ module SettingsHelper
|
|||
uk: 'Українська',
|
||||
'zh-CN': '简体中文',
|
||||
fi: 'Suomi',
|
||||
eo: 'Esperanto',
|
||||
}.freeze
|
||||
|
||||
def human_locale(locale)
|
||||
|
|
|
@ -34,10 +34,6 @@ module StreamEntriesHelper
|
|||
user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
|
||||
end
|
||||
|
||||
def proper_status(status)
|
||||
status.reblog? ? status.reblog : status
|
||||
end
|
||||
|
||||
def rtl?(text)
|
||||
return false if text.empty?
|
||||
|
||||
|
|
351
app/lib/atom_serializer.rb
Normal file
351
app/lib/atom_serializer.rb
Normal file
|
@ -0,0 +1,351 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AtomSerializer
|
||||
include RoutingHelper
|
||||
|
||||
class << self
|
||||
def render(element)
|
||||
document = Ox::Document.new(version: '1.0')
|
||||
document << element
|
||||
('<?xml version="1.0"?>' + Ox.dump(element)).force_encoding('UTF-8')
|
||||
end
|
||||
end
|
||||
|
||||
def author(account)
|
||||
author = Ox::Element.new('author')
|
||||
|
||||
uri = TagManager.instance.uri_for(account)
|
||||
|
||||
append_element(author, 'id', uri)
|
||||
append_element(author, 'activity:object-type', TagManager::TYPES[:person])
|
||||
append_element(author, 'uri', uri)
|
||||
append_element(author, 'name', account.username)
|
||||
append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct)
|
||||
append_element(author, 'summary', account.note)
|
||||
append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
|
||||
append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original)))
|
||||
append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original)))
|
||||
append_element(author, 'poco:preferredUsername', account.username)
|
||||
append_element(author, 'poco:displayName', account.display_name) unless account.display_name.blank?
|
||||
append_element(author, 'poco:note', Formatter.instance.simplified_format(account).to_str) unless account.note.blank?
|
||||
append_element(author, 'mastodon:scope', account.locked? ? :private : :public)
|
||||
|
||||
author
|
||||
end
|
||||
|
||||
def feed(account, stream_entries)
|
||||
feed = Ox::Element.new('feed')
|
||||
|
||||
add_namespaces(feed)
|
||||
|
||||
append_element(feed, 'id', account_url(account, format: 'atom'))
|
||||
append_element(feed, 'title', account.display_name)
|
||||
append_element(feed, 'subtitle', account.note)
|
||||
append_element(feed, 'updated', account.updated_at.iso8601)
|
||||
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
|
||||
|
||||
feed << author(account)
|
||||
|
||||
append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
|
||||
append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
|
||||
append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20
|
||||
append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
|
||||
append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id))
|
||||
|
||||
stream_entries.each do |stream_entry|
|
||||
feed << entry(stream_entry)
|
||||
end
|
||||
|
||||
feed
|
||||
end
|
||||
|
||||
def entry(stream_entry, root = false)
|
||||
entry = Ox::Element.new('entry')
|
||||
|
||||
add_namespaces(entry) if root
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type))
|
||||
append_element(entry, 'published', stream_entry.created_at.iso8601)
|
||||
append_element(entry, 'updated', stream_entry.updated_at.iso8601)
|
||||
append_element(entry, 'title', stream_entry&.status&.title)
|
||||
|
||||
entry << author(stream_entry.account) if root
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[stream_entry.object_type])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[stream_entry.verb])
|
||||
|
||||
entry << object(stream_entry.target) if stream_entry.targeted?
|
||||
|
||||
serialize_status_attributes(entry, stream_entry.status) unless stream_entry.status.nil?
|
||||
|
||||
append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry))
|
||||
append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
|
||||
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
|
||||
|
||||
entry
|
||||
end
|
||||
|
||||
def object(status)
|
||||
object = Ox::Element.new('activity:object')
|
||||
|
||||
append_element(object, 'id', TagManager.instance.uri_for(status))
|
||||
append_element(object, 'published', status.created_at.iso8601)
|
||||
append_element(object, 'updated', status.updated_at.iso8601)
|
||||
append_element(object, 'title', status.title)
|
||||
|
||||
object << author(status.account)
|
||||
|
||||
append_element(object, 'activity:object-type', TagManager::TYPES[status.object_type])
|
||||
append_element(object, 'activity:verb', TagManager::VERBS[status.verb])
|
||||
|
||||
serialize_status_attributes(object, status)
|
||||
|
||||
append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status))
|
||||
append_element(object, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) if status.reply? && !status.thread.nil?
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
def follow_salmon(follow)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{follow.account.acct} started following #{follow.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(follow.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:follow])
|
||||
|
||||
object = author(follow.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def follow_request_salmon(follow_request)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest'))
|
||||
append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}")
|
||||
|
||||
entry << author(follow_request.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:request_friend])
|
||||
|
||||
object = author(follow_request.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def authorize_follow_request_salmon(follow_request)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
|
||||
append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}")
|
||||
|
||||
entry << author(follow_request.target_account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:authorize])
|
||||
|
||||
object = Ox::Element.new('activity:object')
|
||||
object << author(follow_request.account)
|
||||
|
||||
append_element(object, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(object, 'activity:verb', TagManager::VERBS[:request_friend])
|
||||
|
||||
inner_object = author(follow_request.target_account)
|
||||
inner_object.value = 'activity:object'
|
||||
|
||||
object << inner_object
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def reject_follow_request_salmon(follow_request)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest'))
|
||||
append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}")
|
||||
|
||||
entry << author(follow_request.target_account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:reject])
|
||||
|
||||
object = Ox::Element.new('activity:object')
|
||||
object << author(follow_request.account)
|
||||
|
||||
append_element(object, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(object, 'activity:verb', TagManager::VERBS[:request_friend])
|
||||
|
||||
inner_object = author(follow_request.target_account)
|
||||
inner_object.value = 'activity:object'
|
||||
|
||||
object << inner_object
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def unfollow_salmon(follow)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(follow.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:unfollow])
|
||||
|
||||
object = author(follow.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def block_salmon(block)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
|
||||
append_element(entry, 'title', description)
|
||||
|
||||
entry << author(block.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:block])
|
||||
|
||||
object = author(block.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def unblock_salmon(block)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{block.account.acct} no longer blocks #{block.target_account.acct}"
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block'))
|
||||
append_element(entry, 'title', description)
|
||||
|
||||
entry << author(block.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:unblock])
|
||||
|
||||
object = author(block.target_account)
|
||||
object.value = 'activity:object'
|
||||
|
||||
entry << object
|
||||
entry
|
||||
end
|
||||
|
||||
def favourite_salmon(favourite)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(favourite.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:favorite])
|
||||
|
||||
entry << object(favourite.status)
|
||||
|
||||
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
|
||||
|
||||
entry
|
||||
end
|
||||
|
||||
def unfavourite_salmon(favourite)
|
||||
entry = Ox::Element.new('entry')
|
||||
add_namespaces(entry)
|
||||
|
||||
description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
|
||||
|
||||
append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite'))
|
||||
append_element(entry, 'title', description)
|
||||
append_element(entry, 'content', description, type: :html)
|
||||
|
||||
entry << author(favourite.account)
|
||||
|
||||
append_element(entry, 'activity:object-type', TagManager::TYPES[:activity])
|
||||
append_element(entry, 'activity:verb', TagManager::VERBS[:unfavorite])
|
||||
|
||||
entry << object(favourite.status)
|
||||
|
||||
append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
|
||||
|
||||
entry
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def append_element(parent, name, content = nil, attributes = {})
|
||||
element = Ox::Element.new(name)
|
||||
attributes.each { |k, v| element[k] = v.to_s }
|
||||
element << content.to_s unless content.nil?
|
||||
parent << element
|
||||
end
|
||||
|
||||
def add_namespaces(parent)
|
||||
parent['xmlns'] = TagManager::XMLNS
|
||||
parent['xmlns:thr'] = TagManager::THR_XMLNS
|
||||
parent['xmlns:activity'] = TagManager::AS_XMLNS
|
||||
parent['xmlns:poco'] = TagManager::POCO_XMLNS
|
||||
parent['xmlns:media'] = TagManager::MEDIA_XMLNS
|
||||
parent['xmlns:ostatus'] = TagManager::OS_XMLNS
|
||||
parent['xmlns:mastodon'] = TagManager::MTDN_XMLNS
|
||||
end
|
||||
|
||||
def serialize_status_attributes(entry, status)
|
||||
append_element(entry, 'summary', status.spoiler_text) unless status.spoiler_text.blank?
|
||||
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
|
||||
|
||||
status.mentions.each do |mentioned|
|
||||
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account))
|
||||
end
|
||||
|
||||
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:collection], href: TagManager::COLLECTIONS[:public]) if status.public_visibility?
|
||||
|
||||
status.tags.each do |tag|
|
||||
append_element(entry, 'category', nil, term: tag.name)
|
||||
end
|
||||
|
||||
append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive?
|
||||
|
||||
status.media_attachments.each do |media|
|
||||
append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false)))
|
||||
end
|
||||
|
||||
append_element(entry, 'mastodon:scope', status.visibility)
|
||||
end
|
||||
end
|
|
@ -2,17 +2,30 @@
|
|||
|
||||
class EmailValidator < ActiveModel::EachValidator
|
||||
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)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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('.', '\.')
|
||||
regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
|
||||
|
||||
value =~ regexp
|
||||
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
|
||||
|
|
|
@ -34,12 +34,7 @@ class FeedManager
|
|||
trim(timeline_type, account.id)
|
||||
end
|
||||
|
||||
broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status))
|
||||
end
|
||||
|
||||
def broadcast(timeline_id, options = {})
|
||||
options[:queued_at] = (Time.now.to_f * 1000.0).to_i
|
||||
ActionCable.server.broadcast("timeline:#{timeline_id}", options)
|
||||
PushUpdateWorker.perform_async(account.id, status.id)
|
||||
end
|
||||
|
||||
def trim(type, account_id)
|
||||
|
@ -81,10 +76,6 @@ class FeedManager
|
|||
end
|
||||
end
|
||||
|
||||
def inline_render(target_account, template, object)
|
||||
Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: InlineRablScope.new(target_account)).render
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redis
|
||||
|
@ -118,7 +109,7 @@ class FeedManager
|
|||
|
||||
def filter_from_mentions?(status, receiver_id)
|
||||
check_for_blocks = [status.account_id]
|
||||
check_for_blocks.concat(status.mentions.select('account_id').map(&:account_id))
|
||||
check_for_blocks.concat(status.mentions.pluck(:account_id))
|
||||
check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
|
||||
|
||||
should_filter = receiver_id == status.account_id # Filter if I'm mentioning myself
|
||||
|
|
13
app/lib/inline_renderer.rb
Normal file
13
app/lib/inline_renderer.rb
Normal 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
|
|
@ -78,6 +78,8 @@ class TagManager
|
|||
case target.object_type
|
||||
when :person
|
||||
account_url(target)
|
||||
when :note, :comment, :activity
|
||||
unique_tag(target.created_at, target.id, 'Status')
|
||||
else
|
||||
unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type)
|
||||
end
|
||||
|
|
|
@ -125,11 +125,11 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def favourited?(status)
|
||||
(status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive?
|
||||
status.proper.favourites.where(account: self).count.positive?
|
||||
end
|
||||
|
||||
def reblogged?(status)
|
||||
(status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive?
|
||||
status.proper.reblogs.where(account: self).count.positive?
|
||||
end
|
||||
|
||||
def keypair
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
class Block < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
belongs_to :account, required: true
|
||||
belongs_to :target_account, class_name: 'Account', required: true
|
||||
|
||||
validates :account, :target_account, presence: true
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
end
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
class Follow < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
belongs_to :account, counter_cache: :following_count
|
||||
belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count
|
||||
belongs_to :account, counter_cache: :following_count, required: true
|
||||
|
||||
belongs_to :target_account,
|
||||
class_name: 'Account',
|
||||
counter_cache: :followers_count,
|
||||
required: true
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
validates :account, :target_account, presence: true
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
end
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
class FollowRequest < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
belongs_to :account, required: true
|
||||
belongs_to :target_account, class_name: 'Account', required: true
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
validates :account, :target_account, presence: true
|
||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||
|
||||
def authorize!
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mention < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :mentions
|
||||
belongs_to :status
|
||||
belongs_to :account, inverse_of: :mentions, required: true
|
||||
belongs_to :status, required: true
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
validates :account, :status, presence: true
|
||||
validates :account, uniqueness: { scope: :status }
|
||||
end
|
||||
|
|
|
@ -62,8 +62,12 @@ class Status < ApplicationRecord
|
|||
reply? ? :comment : :note
|
||||
end
|
||||
|
||||
def proper
|
||||
reblog? ? reblog : self
|
||||
end
|
||||
|
||||
def content
|
||||
reblog? ? reblog.text : text
|
||||
proper.text
|
||||
end
|
||||
|
||||
def target
|
||||
|
@ -161,9 +165,9 @@ class Status < ApplicationRecord
|
|||
return where.not(visibility: [:private, :direct]) if account.nil?
|
||||
|
||||
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
|
||||
where('1 = 1')
|
||||
all
|
||||
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)
|
||||
.where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct])
|
||||
|
|
|
@ -5,25 +5,21 @@ class StreamEntry < ApplicationRecord
|
|||
|
||||
belongs_to :account, inverse_of: :stream_entries
|
||||
belongs_to :activity, polymorphic: true
|
||||
|
||||
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry
|
||||
|
||||
validates :account, :activity, presence: true
|
||||
|
||||
STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, mentions: :account], thread: [:stream_entry, :account]].freeze
|
||||
STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze
|
||||
|
||||
default_scope { where(activity_type: 'Status') }
|
||||
scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
|
||||
|
||||
def object_type
|
||||
if orphaned?
|
||||
:activity
|
||||
else
|
||||
targeted? ? :activity : activity.object_type
|
||||
end
|
||||
orphaned? || targeted? ? :activity : status.object_type
|
||||
end
|
||||
|
||||
def verb
|
||||
orphaned? ? :delete : activity.verb
|
||||
orphaned? ? :delete : status.verb
|
||||
end
|
||||
|
||||
def targeted?
|
||||
|
@ -31,15 +27,15 @@ class StreamEntry < ApplicationRecord
|
|||
end
|
||||
|
||||
def target
|
||||
orphaned? ? nil : activity.target
|
||||
orphaned? ? nil : status.target
|
||||
end
|
||||
|
||||
def title
|
||||
orphaned? ? nil : activity.title
|
||||
orphaned? ? nil : status.title
|
||||
end
|
||||
|
||||
def content
|
||||
orphaned? ? nil : activity.content
|
||||
orphaned? ? nil : status.content
|
||||
end
|
||||
|
||||
def threaded?
|
||||
|
@ -47,20 +43,16 @@ class StreamEntry < ApplicationRecord
|
|||
end
|
||||
|
||||
def thread
|
||||
orphaned? ? nil : activity.thread
|
||||
orphaned? ? nil : status.thread
|
||||
end
|
||||
|
||||
def mentions
|
||||
activity.respond_to?(:mentions) ? activity.mentions.map(&:account) : []
|
||||
end
|
||||
|
||||
def activity
|
||||
!new_record? ? send(activity_type.underscore) || super : super
|
||||
orphaned? ? [] : status.mentions.map(&:account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def orphaned?
|
||||
activity.nil?
|
||||
status.nil?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,20 +9,20 @@ class AfterBlockService < BaseService
|
|||
private
|
||||
|
||||
def clear_timelines(account, target_account)
|
||||
mentions_key = FeedManager.instance.key(:mentions, account.id)
|
||||
home_key = FeedManager.instance.key(:home, account.id)
|
||||
home_key = FeedManager.instance.key(:home, account.id)
|
||||
|
||||
target_account.statuses.select('id').find_each do |status|
|
||||
redis.zrem(mentions_key, status.id)
|
||||
redis.zrem(home_key, status.id)
|
||||
redis.pipelined do
|
||||
target_account.statuses.select('id').find_each do |status|
|
||||
redis.zrem(home_key, status.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def clear_notifications(account, target_account)
|
||||
Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).destroy_all
|
||||
Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).destroy_all
|
||||
Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).destroy_all
|
||||
Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).destroy_all
|
||||
Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).delete_all
|
||||
Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).delete_all
|
||||
Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).delete_all
|
||||
Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).delete_all
|
||||
end
|
||||
|
||||
def redis
|
||||
|
|
|
@ -10,31 +10,6 @@ class AuthorizeFollowService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(follow_request)
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest'
|
||||
title xml, "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}"
|
||||
|
||||
author(xml) do
|
||||
include_author xml, follow_request.target_account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :authorize
|
||||
|
||||
target(xml) do
|
||||
author(xml) do
|
||||
include_author xml, follow_request.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :request_friend
|
||||
|
||||
target(xml) do
|
||||
include_author xml, follow_request.target_account
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.authorize_follow_request_salmon(follow_request))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,22 +18,6 @@ class BlockService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(block)
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, block.created_at, block.id, 'Block'
|
||||
title xml, "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
|
||||
|
||||
author(xml) do
|
||||
include_author xml, block.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :block
|
||||
|
||||
target(xml) do
|
||||
include_author xml, block.target_account
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.block_salmon(block))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
module StreamEntryRenderer
|
||||
def stream_entry_to_xml(stream_entry)
|
||||
renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
|
||||
renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom])
|
||||
AtomSerializer.render(AtomSerializer.new.entry(stream_entry, true))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,7 @@ class FanOutOnWriteService < BaseService
|
|||
|
||||
return if status.account.silenced? || !status.public_visibility? || status.reblog?
|
||||
|
||||
render_anonymous_payload(status)
|
||||
deliver_to_hashtags(status)
|
||||
|
||||
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||
|
@ -48,23 +49,24 @@ class FanOutOnWriteService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def render_anonymous_payload(status)
|
||||
@payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show')
|
||||
@payload = Oj.dump(event: :update, payload: @payload)
|
||||
end
|
||||
|
||||
def deliver_to_hashtags(status)
|
||||
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|
|
||||
FeedManager.instance.broadcast("hashtag:#{hashtag}", event: 'update', payload: payload)
|
||||
FeedManager.instance.broadcast("hashtag:#{hashtag}:local", event: 'update', payload: payload) if status.account.local?
|
||||
Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
|
||||
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
|
||||
end
|
||||
end
|
||||
|
||||
def deliver_to_public(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to public timeline"
|
||||
|
||||
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status)
|
||||
|
||||
FeedManager.instance.broadcast(:public, event: 'update', payload: payload)
|
||||
FeedManager.instance.broadcast('public:local', event: 'update', payload: payload) if status.account.local?
|
||||
Redis.current.publish('timeline:public', @payload)
|
||||
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,26 +22,6 @@ class FavouriteService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(favourite)
|
||||
description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
|
||||
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, favourite.created_at, favourite.id, 'Favourite'
|
||||
title xml, description
|
||||
content xml, description
|
||||
|
||||
author(xml) do
|
||||
include_author xml, favourite.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :favorite
|
||||
in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status)
|
||||
|
||||
target(xml) do
|
||||
include_target xml, favourite.status
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,13 +45,13 @@ class FollowRemoteAccountService < BaseService
|
|||
account.suspended = true if domain_block && domain_block.suspend?
|
||||
account.silenced = true if domain_block && domain_block.silence?
|
||||
|
||||
xml = get_feed(account.remote_url)
|
||||
hubs = get_hubs(xml)
|
||||
body, xml = get_feed(account.remote_url)
|
||||
hubs = get_hubs(xml)
|
||||
|
||||
account.uri = get_account_uri(xml)
|
||||
account.hub_url = hubs.first.attribute('href').value
|
||||
|
||||
get_profile(xml, account)
|
||||
get_profile(body, account)
|
||||
account.save!
|
||||
|
||||
account
|
||||
|
@ -61,7 +61,7 @@ class FollowRemoteAccountService < BaseService
|
|||
|
||||
def get_feed(url)
|
||||
response = http_client.get(Addressable::URI.parse(url))
|
||||
Nokogiri::XML(response)
|
||||
[response.to_s, Nokogiri::XML(response)]
|
||||
end
|
||||
|
||||
def get_hubs(xml)
|
||||
|
@ -82,12 +82,8 @@ class FollowRemoteAccountService < BaseService
|
|||
author_uri.content
|
||||
end
|
||||
|
||||
def get_profile(xml, account)
|
||||
update_remote_profile_service.call(xml.at_xpath('/xmlns:feed'), account)
|
||||
end
|
||||
|
||||
def update_remote_profile_service
|
||||
@update_remote_profile_service ||= UpdateRemoteProfileService.new
|
||||
def get_profile(body, account)
|
||||
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), false)
|
||||
end
|
||||
|
||||
def http_client
|
||||
|
|
|
@ -10,7 +10,7 @@ class FollowService < BaseService
|
|||
target_account = FollowRemoteAccountService.new.call(uri)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
||||
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
|
||||
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
|
||||
|
||||
if target_account.locked?
|
||||
request_follow(source_account, target_account)
|
||||
|
@ -55,48 +55,10 @@ class FollowService < BaseService
|
|||
end
|
||||
|
||||
def build_follow_request_xml(follow_request)
|
||||
description = "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}"
|
||||
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, follow_request.created_at, follow_request.id, 'FollowRequest'
|
||||
title xml, description
|
||||
content xml, description
|
||||
|
||||
author(xml) do
|
||||
include_author xml, follow_request.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :request_friend
|
||||
|
||||
target(xml) do
|
||||
include_author xml, follow_request.target_account
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.follow_request_salmon(follow_request))
|
||||
end
|
||||
|
||||
def build_follow_xml(follow)
|
||||
description = "#{follow.account.acct} started following #{follow.target_account.acct}"
|
||||
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, follow.created_at, follow.id, 'Follow'
|
||||
title xml, description
|
||||
content xml, description
|
||||
|
||||
author(xml) do
|
||||
include_author xml, follow.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :follow
|
||||
|
||||
target(xml) do
|
||||
include_author xml, follow.target_account
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.follow_salmon(follow))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,7 +50,7 @@ class NotifyService < BaseService
|
|||
def create_notification
|
||||
@notification.save!
|
||||
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("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
|
||||
end
|
||||
|
||||
def send_email
|
||||
|
|
|
@ -37,11 +37,11 @@ class PostStatusService < BaseService
|
|||
def validate_media!(media_ids)
|
||||
return if media_ids.nil? || !media_ids.is_a?(Enumerable)
|
||||
|
||||
raise Mastodon::ValidationError, 'Cannot attach more than 4 files' if media_ids.size > 4
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4
|
||||
|
||||
media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i))
|
||||
|
||||
raise Mastodon::ValidationError, 'Cannot attach a video to a toot that already contains images' if media.size > 1 && media.find(&:video?)
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media.size > 1 && media.find(&:video?)
|
||||
|
||||
media
|
||||
end
|
||||
|
|
|
@ -5,15 +5,15 @@ class ProcessFeedService < BaseService
|
|||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
update_author(xml, account)
|
||||
update_author(body, xml, account)
|
||||
process_entries(xml, account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_author(xml, account)
|
||||
def update_author(body, xml, account)
|
||||
return if xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS).nil?
|
||||
UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS), account, true)
|
||||
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true)
|
||||
end
|
||||
|
||||
def process_entries(xml, account)
|
||||
|
|
|
@ -24,7 +24,7 @@ class ProcessInteractionService < BaseService
|
|||
return if account.suspended?
|
||||
|
||||
if salmon.verify(envelope, account.keypair)
|
||||
update_remote_profile_service.call(xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS), account, true)
|
||||
RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true)
|
||||
|
||||
case verb(xml)
|
||||
when :follow
|
||||
|
@ -114,7 +114,7 @@ class ProcessInteractionService < BaseService
|
|||
|
||||
return if status.nil?
|
||||
|
||||
remove_status_service.call(status) if account.id == status.account_id
|
||||
RemovalWorker.perform_async(status.id) if account.id == status.account_id
|
||||
end
|
||||
|
||||
def favourite!(xml, from_account)
|
||||
|
@ -130,7 +130,7 @@ class ProcessInteractionService < BaseService
|
|||
end
|
||||
|
||||
def add_post!(body, account)
|
||||
process_feed_service.call(body, account)
|
||||
ProcessingWorker.perform_async(account.id, body.force_encoding('UTF-8'))
|
||||
end
|
||||
|
||||
def status(xml)
|
||||
|
@ -153,10 +153,6 @@ class ProcessInteractionService < BaseService
|
|||
@process_feed_service ||= ProcessFeedService.new
|
||||
end
|
||||
|
||||
def update_remote_profile_service
|
||||
@update_remote_profile_service ||= UpdateRemoteProfileService.new
|
||||
end
|
||||
|
||||
def remove_status_service
|
||||
@remove_status_service ||= RemoveStatusService.new
|
||||
end
|
||||
|
|
|
@ -10,31 +10,6 @@ class RejectFollowService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(follow_request)
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest'
|
||||
title xml, "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}"
|
||||
|
||||
author(xml) do
|
||||
include_author xml, follow_request.target_account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :reject
|
||||
|
||||
target(xml) do
|
||||
author(xml) do
|
||||
include_author xml, follow_request.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :request_friend
|
||||
|
||||
target(xml) do
|
||||
include_author xml, follow_request.target_account
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.reject_follow_request_salmon(follow_request))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ class RemoveStatusService < BaseService
|
|||
include StreamEntryRenderer
|
||||
|
||||
def call(status)
|
||||
@payload = Oj.dump(event: :delete, payload: status.id)
|
||||
|
||||
remove_from_self(status) if status.account.local?
|
||||
remove_from_followers(status)
|
||||
remove_from_mentioned(status)
|
||||
|
@ -25,25 +27,23 @@ class RemoveStatusService < BaseService
|
|||
end
|
||||
|
||||
def remove_from_followers(status)
|
||||
status.account.followers.each do |follower|
|
||||
next unless follower.local?
|
||||
status.account.followers.where(domain: nil).each do |follower|
|
||||
unpush(:home, follower, status)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_mentioned(status)
|
||||
return unless status.local?
|
||||
notified_domains = []
|
||||
|
||||
status.mentions.each do |mention|
|
||||
mentioned_account = mention.account
|
||||
|
||||
if mentioned_account.local?
|
||||
unpush(:mentions, mentioned_account, status)
|
||||
else
|
||||
next if notified_domains.include?(mentioned_account.domain)
|
||||
notified_domains << mentioned_account.domain
|
||||
send_delete_salmon(mentioned_account, status)
|
||||
end
|
||||
next if mentioned_account.local?
|
||||
next if notified_domains.include?(mentioned_account.domain)
|
||||
|
||||
notified_domains << mentioned_account.domain
|
||||
send_delete_salmon(mentioned_account, status)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -65,17 +65,19 @@ class RemoveStatusService < BaseService
|
|||
redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
|
||||
end
|
||||
|
||||
FeedManager.instance.broadcast(receiver.id, event: 'delete', payload: status.id)
|
||||
Redis.current.publish("timeline:#{receiver.id}", @payload)
|
||||
end
|
||||
|
||||
def remove_from_hashtags(status)
|
||||
status.tags.each do |tag|
|
||||
FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'delete', payload: status.id)
|
||||
status.tags.pluck(:name) do |hashtag|
|
||||
Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
|
||||
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_public(status)
|
||||
FeedManager.instance.broadcast(:public, event: 'delete', payload: status.id)
|
||||
Redis.current.publish('timeline:public', @payload)
|
||||
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||
end
|
||||
|
||||
def redis
|
||||
|
|
|
@ -11,22 +11,6 @@ class UnblockService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(block)
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, Time.now.utc, block.id, 'Block'
|
||||
title xml, "#{block.account.acct} no longer blocks #{block.target_account.acct}"
|
||||
|
||||
author(xml) do
|
||||
include_author xml, block.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :unblock
|
||||
|
||||
target(xml) do
|
||||
include_author xml, block.target_account
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.unblock_salmon(block))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,26 +13,6 @@ class UnfavouriteService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(favourite)
|
||||
description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
|
||||
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, Time.now.utc, favourite.id, 'Favourite'
|
||||
title xml, description
|
||||
content xml, description
|
||||
|
||||
author(xml) do
|
||||
include_author xml, favourite.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :unfavorite
|
||||
in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status)
|
||||
|
||||
target(xml) do
|
||||
include_target xml, favourite.status
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,25 +13,6 @@ class UnfollowService < BaseService
|
|||
private
|
||||
|
||||
def build_xml(follow)
|
||||
description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
|
||||
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
unique_id xml, Time.now.utc, follow.id, 'Follow'
|
||||
title xml, description
|
||||
content xml, description
|
||||
|
||||
author(xml) do
|
||||
include_author xml, follow.account
|
||||
end
|
||||
|
||||
object_type xml, :activity
|
||||
verb xml, :unfollow
|
||||
|
||||
target(xml) do
|
||||
include_author xml, follow.target_account
|
||||
end
|
||||
end
|
||||
end.to_xml
|
||||
AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Nokogiri::XML::Builder.new do |xml|
|
||||
feed(xml) do
|
||||
simple_id xml, account_url(@account, format: 'atom')
|
||||
title xml, @account.display_name
|
||||
subtitle xml, @account.note
|
||||
updated_at xml, stream_updated_at
|
||||
logo xml, full_asset_url(@account.avatar.url(:original))
|
||||
|
||||
author(xml) do
|
||||
include_author xml, @account
|
||||
end
|
||||
|
||||
link_alternate xml, TagManager.instance.url_for(@account)
|
||||
link_self xml, account_url(@account, format: 'atom')
|
||||
link_next xml, account_url(@account, format: 'atom', max_id: @entries.last.id) if @entries.size == 20
|
||||
link_hub xml, api_push_url
|
||||
link_salmon xml, api_salmon_url(@account.id)
|
||||
|
||||
@entries.each do |stream_entry|
|
||||
entry(xml, false) do
|
||||
include_entry xml, stream_entry
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to_xml
|
|
@ -11,8 +11,10 @@
|
|||
%meta{:name => "theme-color", :content => "#282c37"}/
|
||||
%meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/
|
||||
|
||||
%title
|
||||
= "#{yield(:page_title)} - " if content_for?(:page_title)
|
||||
%title<
|
||||
- if content_for?(:page_title)
|
||||
= yield(:page_title)
|
||||
= ' - '
|
||||
= Setting.site_title
|
||||
|
||||
= stylesheet_link_tag 'application', media: 'all'
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
%strong= display_name(status.account)
|
||||
= t('stream_entries.reblogged')
|
||||
|
||||
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) }
|
||||
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
|
||||
|
||||
- if include_threads
|
||||
= render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true }
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
Nokogiri::XML::Builder.new do |xml|
|
||||
entry(xml, true) do
|
||||
author(xml) do
|
||||
include_author xml, @stream_entry.account
|
||||
end
|
||||
|
||||
include_entry xml, @stream_entry
|
||||
end
|
||||
end.to_xml
|
|
@ -0,0 +1,5 @@
|
|||
<p>Tervetuloa <%= @resource.email %>!</p>
|
||||
|
||||
<p>Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä:</p>
|
||||
|
||||
<p><%= link_to 'Varmista tilini', confirmation_url(@resource, confirmation_token: @token) %></p>
|
|
@ -0,0 +1,5 @@
|
|||
Tervetuloa <%= @resource.email %>!
|
||||
|
||||
Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä:
|
||||
|
||||
<%= confirmation_url(@resource, confirmation_token: @token) %>
|
3
app/views/user_mailer/password_change.fi.html.erb
Normal file
3
app/views/user_mailer/password_change.fi.html.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>Hei <%= @resource.email %>!</p>
|
||||
|
||||
<p>Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.</p>
|
3
app/views/user_mailer/password_change.fi.text.erb
Normal file
3
app/views/user_mailer/password_change.fi.text.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
Hei <%= @resource.email %>!
|
||||
|
||||
Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.
|
|
@ -0,0 +1,8 @@
|
|||
<p>Hei <%= @resource.email %>!</p>
|
||||
|
||||
<p>Joku on pyytänyt salasanvaihto Mastodonissa. Voit tehdä sen allaolevassa linkissä.</p>
|
||||
|
||||
<p><%= link_to 'Vaihda salasanani', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>Jos et pyytänyt vaihtoa, poista tämä viesti.</p>
|
||||
<p>Salasanaasi ei vaihdeta ennen kuin menet ylläolevaan linkkiin ja luot uuden.</p>
|
|
@ -0,0 +1,8 @@
|
|||
Hei <%= @resource.email %>!
|
||||
|
||||
Joku on pyytänyt salasanvaihto Mastodonissa. Voit tehdä sen allaolevassa linkissä.
|
||||
|
||||
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
||||
|
||||
Jos et pyytänyt vaihtoa, poista tämä viesti.
|
||||
Salasanaasi ei vaihdeta ennen kuin menet ylläolevaan linkkiin ja luot uuden.
|
|
@ -3,6 +3,8 @@
|
|||
class Admin::SuspensionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id)
|
||||
SuspendAccountService.new.call(Account.find(account_id))
|
||||
end
|
||||
|
|
|
@ -13,5 +13,7 @@ class AfterRemoteFollowRequestWorker
|
|||
|
||||
follow_request.destroy
|
||||
FollowService.new.call(follow_request.account, updated_account.acct)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,5 +13,7 @@ class AfterRemoteFollowWorker
|
|||
|
||||
follow.destroy
|
||||
FollowService.new.call(follow.account, updated_account.acct)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
7
app/workers/application_worker.rb
Normal file
7
app/workers/application_worker.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationWorker
|
||||
def info(message)
|
||||
Rails.logger.info("#{self.class.name} - #{message}")
|
||||
end
|
||||
end
|
|
@ -1,14 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DistributionWorker
|
||||
class DistributionWorker < ApplicationWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(status_id)
|
||||
status = Status.find(status_id)
|
||||
|
||||
FanOutOnWriteService.new.call(status)
|
||||
WarmCacheService.new.call(status)
|
||||
FanOutOnWriteService.new.call(Status.find(status_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
info("Couldn't find the status")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,7 +46,7 @@ class ImportWorker
|
|||
|
||||
begin
|
||||
FollowService.new.call(from_account, row[0])
|
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
next
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,9 @@ class Pubsubhubbub::DeliveryWorker
|
|||
def perform(subscription_id, payload)
|
||||
subscription = Subscription.find(subscription_id)
|
||||
headers = {}
|
||||
host = Addressable::URI.parse(subscription.callback_url).host
|
||||
|
||||
return if DomainBlock.blocked?(host)
|
||||
|
||||
headers['User-Agent'] = 'Mastodon/PubSubHubbub'
|
||||
headers['Link'] = LinkHeader.new([[api_push_url, [%w(rel hub)]], [account_url(subscription.account, format: :atom), [%w(rel self)]]]).to_s
|
||||
|
@ -22,6 +25,7 @@ class Pubsubhubbub::DeliveryWorker
|
|||
.headers(headers)
|
||||
.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
|
||||
|
||||
subscription.touch(:last_successful_delivery_at)
|
||||
|
|
|
@ -10,14 +10,10 @@ class Pubsubhubbub::DistributionWorker
|
|||
|
||||
return if stream_entry.hidden?
|
||||
|
||||
account = stream_entry.account
|
||||
renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
|
||||
payload = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
|
||||
# domains = account.followers_domains
|
||||
account = stream_entry.account
|
||||
payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry]))
|
||||
|
||||
Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
|
||||
host = Addressable::URI.parse(subscription.callback_url).host
|
||||
next if DomainBlock.blocked?(host) # || !domains.include?(host)
|
||||
Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
|
|
15
app/workers/push_update_worker.rb
Normal file
15
app/workers/push_update_worker.rb
Normal 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
|
20
app/workers/remote_profile_update_worker.rb
Normal file
20
app/workers/remote_profile_update_worker.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoteProfileUpdateWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id, body, resubscribe)
|
||||
account = Account.find(account_id)
|
||||
|
||||
xml = Nokogiri::XML(body)
|
||||
xml.encoding = 'utf-8'
|
||||
|
||||
author_container = xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS) || xml.at_xpath('/xmlns:entry', xmlns: TagManager::XMLNS)
|
||||
|
||||
UpdateRemoteProfileService.new.call(author_container, account, resubscribe)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ class SalmonWorker
|
|||
|
||||
def perform(account_id, body)
|
||||
ProcessInteractionService.new.call(body, Account.find(account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ module Mastodon
|
|||
|
||||
# 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.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi]
|
||||
config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo]
|
||||
config.i18n.default_locale = :en
|
||||
|
||||
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
Rails.application.configure do
|
||||
config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' }
|
||||
config.x.email_domains_whitelist = ENV.fetch('EMAIL_DOMAIN_WHITELIST') { '' }
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
Rabl.configure do |config|
|
||||
config.json_engine = Oj
|
||||
config.cache_all_output = false
|
||||
config.cache_sources = Rails.env.production?
|
||||
config.include_json_root = false
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
---
|
||||
de:
|
||||
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
|
||||
source_code: Quellcode
|
||||
terms: AGB
|
||||
accounts:
|
||||
follow: Folgen
|
||||
followers: Folger
|
||||
following: Folgt
|
||||
followers: Follower
|
||||
following: Gefolgt
|
||||
nothing_here: Hier gibt es nichts!
|
||||
people_followed_by: Nutzer, denen %{name} folgt
|
||||
people_who_follow: Nutzer, die %{name} folgen
|
||||
|
@ -27,7 +27,7 @@ de:
|
|||
reset_password: Passwort zurücksetzen
|
||||
set_new_password: Neues Passwort setzen
|
||||
authorize_follow:
|
||||
error: Das entfernte Profil konnte nicht geladen werden
|
||||
error: Das Profil konnte nicht geladen werden
|
||||
follow: Folgen
|
||||
prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:'
|
||||
title: "%{acct} folgen"
|
||||
|
@ -55,25 +55,25 @@ de:
|
|||
notification_mailer:
|
||||
favourite:
|
||||
body: 'Dein Beitrag wurde von %{name} favorisiert:'
|
||||
subject: "%{name} hat deinen Beitrag favorisiert"
|
||||
subject: "%{name} hat deinen Beitrag favorisiert."
|
||||
follow:
|
||||
body: "%{name} folgt dir jetzt!"
|
||||
subject: "%{name} folgt dir nun"
|
||||
subject: "%{name} folgt dir jetzt."
|
||||
follow_request:
|
||||
body: "%{name} möchte dir folgen:"
|
||||
subject: "%{name} möchte dir folgen"
|
||||
subject: "%{name} möchte dir folgen."
|
||||
mention:
|
||||
body: "%{name} hat dich erwähnt:"
|
||||
subject: "%{name} hat dich erwähnt"
|
||||
subject: "%{name} hat dich erwähnt."
|
||||
reblog:
|
||||
body: 'Dein Beitrag wurde von %{name} geteilt:'
|
||||
subject: "%{name} teilte deinen Beitrag"
|
||||
subject: "%{name} teilte deinen Beitrag."
|
||||
pagination:
|
||||
next: Vorwärts
|
||||
prev: Zurück
|
||||
remote_follow:
|
||||
acct: Dein Nutzername@Domain, von dem du dieser Person folgen möchtest
|
||||
missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden
|
||||
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.
|
||||
proceed: Weiter
|
||||
prompt: 'Du wirst dieser Person folgen:'
|
||||
settings:
|
||||
|
|
|
@ -2,59 +2,59 @@
|
|||
de:
|
||||
devise:
|
||||
confirmations:
|
||||
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_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."
|
||||
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_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:
|
||||
already_authenticated: "Du bist bereits angemeldet."
|
||||
inactive: "Dein Account ist nicht aktiv."
|
||||
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."
|
||||
not_found_in_database: "E-Mail-Adresse oder Passwort ungültig."
|
||||
timeout: "Deine Sitzung ist abgelaufen, bitte melde Dich erneut an."
|
||||
unauthenticated: "Du musst Dich anmelden oder registrieren, bevor Du fortfahren kannst."
|
||||
unconfirmed: "Du musst Deinen Account bestätigen, bevor Du fortfahren kannst."
|
||||
timeout: "Deine Sitzung ist abgelaufen, bitte melde dich erneut an."
|
||||
unauthenticated: "Du musst Dich anmelden oder registrieren, bevor du fortfahren kannst."
|
||||
unconfirmed: "Du musst deinen Account bestätigen, bevor du fortfahren kannst."
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
subject: "Mastodon: Anleitung zur Bestätigung Deines Accounts"
|
||||
subject: "Mastodon: Anleitung zur Bestätigung deines Accounts"
|
||||
password_change:
|
||||
subject: 'Mastodon: Passwort wurde geändert'
|
||||
reset_password_instructions:
|
||||
subject: "Mastodon: Anleitung um Dein Passwort zurückzusetzen"
|
||||
subject: "Mastodon: Anleitung um dein Passwort zurückzusetzen"
|
||||
unlock_instructions:
|
||||
subject: "Mastodon: Anleitung um Deinen Account freizuschalten"
|
||||
subject: "Mastodon: Anleitung um deinen Account freizuschalten"
|
||||
omniauth_callbacks:
|
||||
failure: "Du konntest nicht Deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
|
||||
success: "Du hast Dich erfolgreich mit Deinem %{kind}-Account angemeldet."
|
||||
failure: "Du konntest nicht mit deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
|
||||
success: "Du hast dich erfolgreich mit Deinem %{kind}-Account angemeldet."
|
||||
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."
|
||||
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."
|
||||
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_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_not_active: "Dein Passwort wurde geändert."
|
||||
registrations:
|
||||
destroyed: "Dein Account wurde gelöscht."
|
||||
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_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."
|
||||
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."
|
||||
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_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."
|
||||
updated: "Deine Daten wurden aktualisiert."
|
||||
sessions:
|
||||
already_signed_out: "Erfolgreich abgemeldet."
|
||||
signed_in: "Erfolgreich angemeldet."
|
||||
signed_out: "Erfolgreich abgemeldet."
|
||||
unlocks:
|
||||
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_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."
|
||||
unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet."
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: "wurde bereits bestätigt"
|
||||
confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an"
|
||||
expired: "ist abgelaufen, bitte neu anfordern"
|
||||
not_found: "nicht gefunden"
|
||||
already_confirmed: "wurde bereits bestätigt."
|
||||
confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an."
|
||||
expired: "ist abgelaufen, bitte neu anfordern."
|
||||
not_found: "wurde nicht gefunden."
|
||||
not_locked: "ist nicht gesperrt"
|
||||
not_saved:
|
||||
one: "Konnte %{resource} nicht speichern: ein Fehler."
|
||||
|
|
61
config/locales/devise.eo.yml
Normal file
61
config/locales/devise.eo.yml
Normal file
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
eo:
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Via konto estas konfirmita.
|
||||
send_instructions: Vi ricevos instrukciojn por konfirmi vian konton post kelkaj minutoj.
|
||||
send_paranoid_instructions: Se via retpoŝt-adreso ekzistas en nia datumbazo, vi baldaŭ ricevos retpoŝt-mesaĝon, kiu enhavas la instrukciojn por konfirmi vian konton.
|
||||
failure:
|
||||
already_authenticated: Vi jam estas ensalutita.
|
||||
inactive: Via konto ankoraŭ ne estas konfirmita.
|
||||
invalid: Malĝusta retpoŝt-adreso aŭ pasvorto.
|
||||
last_attempt: Vi ankoraŭ povas provi unufoje antaŭ ol via konto estos ŝlosita.
|
||||
locked: Via konto estas ŝlosita.
|
||||
not_found_in_database: Malĝusta retpoŝt-adreso aŭ pasvorto.
|
||||
timeout: Via sesio eksiĝis. Bonvolu reensaluti por daŭrigi.
|
||||
unauthenticated: Vi devas ensaluti aŭ membriĝi por daŭrigi.
|
||||
unconfirmed: Vi devas konfirmi vian konton por daŭrigi.
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
subject: Instrukcioj por konfirmi
|
||||
password_change:
|
||||
subject: Via pasvorto estis ŝanĝita senprobleme.
|
||||
reset_password_instructions:
|
||||
subject: Instrukcioj por ŝanĝi la pasvorton
|
||||
unlock_instructions:
|
||||
subject: Instrukcioj por malŝlosi la konton
|
||||
omniauth_callbacks:
|
||||
failure: 'Ni ne povis aŭtentigi vin per %{kind}: ''%{reason}''.'
|
||||
success: Aŭtentigita senprobleme per %{kind}.
|
||||
passwords:
|
||||
no_token: Vi ne povas iri al tiu paĝo per alia vojo ol retpoŝt-mesaĝo por ŝanĝi pasvorton. Se vi venas de tia retpoŝt-mesaĝo, kontrolu ke vi uzis la tutan URL.
|
||||
send_instructions: Vi ricevos retpoŝt-mesaĝon kun instrukcioj por ŝanĝi vian pasvorton post kelkaj minutoj.
|
||||
send_paranoid_instructions: Se via retpoŝt-adreso ekzistas en nia datumbazo, vi ricevos ligilon por ŝanĝi vian pasvorton per retpoŝt-mesaĝo.
|
||||
updated: Via pasvorto estis redaktita senprobleme, vi nun estas ensalutita.
|
||||
updated_not_active: Via pasvorto estis redaktita senprobleme.
|
||||
registrations:
|
||||
destroyed: Ĝis! Via konto estis forigita senprobleme. Ni esperas revidi vin baldaŭ.
|
||||
signed_up: Bonvenon! Vi membriĝis senprobleme.
|
||||
signed_up_but_inactive: Vi bone membriĝis, sed vi ankoraŭ ne povas ensaluti ĉar via konto ne estis konfirmita.
|
||||
signed_up_but_locked: Vi bone membriĝis, sed vi ne povas ensaluti ĉar via konto estas ŝlosita.
|
||||
signed_up_but_unconfirmed: Retpoŝt-mesaĝo kun via ligilo por konfirmi vian konton estis sendita al via retpoŝt-adreso. Bonvolu uzi tiun ligilon por konfirmi vian konton.
|
||||
update_needs_confirmation: Vi bone aktualigis vian konton, sed ni bezonas kontroli vian novan retpoŝt-adreson. Bonvolu kontroli viajn retpoŝt-mesaĝojn kaj uzi la ligilon por konfirmi vian novan retpoŝt-adreson.
|
||||
updated: Via konto estis aktualigita senprobleme.
|
||||
sessions:
|
||||
already_signed_out: Elsalutita.
|
||||
signed_in: Ensalutita.
|
||||
signed_out: Elsalutita.
|
||||
unlocks:
|
||||
send_instructions: Vi ricevos retpoŝt-mesaĝon kun instrukcioj por malŝlosi vian konton post kelkaj minutoj.
|
||||
send_paranoid_instructions: Se via retpoŝt-adreso ekzistas en nia datumbazo, vi ricevos ligilon por malŝlosi vian konton per retpoŝt-mesaĝo.
|
||||
unlocked: Via konto estis malŝlosita senprobleme, vi nun estas ensalutita.
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: jam estis konfirmita, bonvolu provi ensaluti
|
||||
confirmation_period_expired: devas esti konfirmita en %{period}, bonvolu repeti
|
||||
expired: eksiĝis, bonvolu repeti
|
||||
not_found: ne estis trovita
|
||||
not_locked: ne estis ŝlosita
|
||||
not_saved:
|
||||
one: '1 eraro malpermesis al tiu %{resource} esti konservita:'
|
||||
other: '%{count} eraroj malpermesis al tiu %{resource} esti konservita:'
|
113
config/locales/doorkeeper.eo.yml
Normal file
113
config/locales/doorkeeper.eo.yml
Normal file
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
eo:
|
||||
activerecord:
|
||||
attributes:
|
||||
doorkeeper/application:
|
||||
name: Nomo
|
||||
redirect_uri: URI de plusendo
|
||||
errors:
|
||||
models:
|
||||
doorkeeper/application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: ne povas enhavi eron.
|
||||
invalid_uri: devas esti valida URI.
|
||||
relative_uri: devas esti absoluta URI.
|
||||
secured_uri: devas esti HTTPS/SSL-a URI.
|
||||
doorkeeper:
|
||||
applications:
|
||||
buttons:
|
||||
authorize: Rajtigi
|
||||
cancel: Rezigni
|
||||
destroy: Detrui
|
||||
edit: Redakti
|
||||
submit: Sendi
|
||||
confirmations:
|
||||
destroy: Ĉu vi certas?
|
||||
edit:
|
||||
title: Redakti aplikaĵon
|
||||
form:
|
||||
error: Ups! Kontrolu vian formularon ĉu estas eraroj
|
||||
help:
|
||||
native_redirect_uri: Uzu %{native_redirect_uri} por lokaj provoj
|
||||
redirect_uri: Uzu unu linion por ĉiu URI
|
||||
scopes: Apartigu ampleksojn per spacetoj. Lasu malplena por uzi la senŝanĝajn ampleksojn.
|
||||
index:
|
||||
callback_url: URL vokita per referenco
|
||||
name: Nomo
|
||||
new: Nova Aplikaĵo
|
||||
title: Viaj aplikaĵoj
|
||||
new:
|
||||
title: Nova aplikaĵo
|
||||
show:
|
||||
actions: Agoj
|
||||
application_id: Identigo de la aplikaĵo
|
||||
callback_urls: URL-j vokitaj per referenco
|
||||
scopes: Ampleksoj
|
||||
secret: Sekreto
|
||||
title: 'Aplikaĵo: %{name}'
|
||||
authorizations:
|
||||
buttons:
|
||||
authorize: Rajtigi
|
||||
deny: Rifuzi
|
||||
error:
|
||||
title: Eraro okazis
|
||||
new:
|
||||
able_to: Povos
|
||||
prompt: La aplikaĵo %{client_name} petas aliron al via konto
|
||||
title: Rajtigo bezonata
|
||||
show:
|
||||
title: Rajtiga kodo
|
||||
authorized_applications:
|
||||
buttons:
|
||||
revoke: Malrajtigi
|
||||
confirmations:
|
||||
revoke: Ĉu vi certas?
|
||||
index:
|
||||
application: Aplikaĵo
|
||||
created_at: Rajtigita
|
||||
date_format: "%Y-%m-%d %H:%M:%S"
|
||||
scopes: Ampleksoj
|
||||
title: Viaj rajtigitaj aplikaĵoj
|
||||
errors:
|
||||
messages:
|
||||
access_denied: La posedanto de la rimedo aŭ la rajtiga servilo rifuzis vian peton.
|
||||
credential_flow_not_configured: La sendado de la identigiloj de la posedanto de la rimedo malsukcesis ĉar Doorkeeper.configure.resource_owner_from_credentials ne estis agordita.
|
||||
invalid_client: La aŭtentigo de la kliento malsukcesis ĉar la kliento estas nekonata, aŭ mankis peto aŭtentigi, aŭ la aŭtentig-metodo ne estas subtenata.
|
||||
invalid_grant: La rajtiga konsento ne estas valida, ne plu estas valida, estis forigita, ne kongruas kun la plusenda URI uzita en la aŭtentiga peto, aŭ estis sendita al alia kliento.
|
||||
invalid_redirect_uri: La plusenda URI uzita en estas valida.
|
||||
invalid_request: Mankis al la peto nepra parametro, enhavas nesubtenatan parametran valoron, aŭ la peto simple estas misformita.
|
||||
invalid_resource_owner: La donitaj identigiloj pri la posedanto de la rimedo ne estas validaj, aŭ tiu ne povas esti trovita.
|
||||
invalid_scope: La petita amplekso ne estas valida, estas nekonata, aŭ estas misformita.
|
||||
invalid_token:
|
||||
expired: La atingoĵetono eskiĝis.
|
||||
revoked: La atingoĵetono estis rifuzita.
|
||||
unknown: La atingoĵetono ne estas valida.
|
||||
resource_owner_authenticator_not_configured: La posedanto de la rimedo ne povis esti trovita ĉar Doorkeeper.configure.resource_owner_authenticator ne estas agordita.
|
||||
server_error: La rajtiga servilo rimarkis neatenditan kondiĉon, kiu malpermesis al ĝi plenumi la peton.
|
||||
temporarily_unavailable: La rajtiga servilo ne povas nun plenumi la peton pro dumtempa superŝarĝo aŭ prizorgado de la servilo.
|
||||
unauthorized_client: La kliento ne rajtas fari tian peton uzante tiun metodon.
|
||||
unsupported_grant_type: La tipo de la rajtiga konsento ne estas subtenata de la rajtiga servilo.
|
||||
unsupported_response_type: La rajtiga servilo ne subtenas tian respondon.
|
||||
flash:
|
||||
applications:
|
||||
create:
|
||||
notice: Aplikaĵo kreita.
|
||||
destroy:
|
||||
notice: Aplikaĵo forigita.
|
||||
update:
|
||||
notice: Aplikaĵo aktualigita.
|
||||
authorized_applications:
|
||||
destroy:
|
||||
notice: Aplikaĵo malrajtigita.
|
||||
layouts:
|
||||
admin:
|
||||
nav:
|
||||
applications: Aplikaĵoj
|
||||
oauth2_provider: OAuth2-provizanto
|
||||
application:
|
||||
title: OAuth-a rajtigo bezonata
|
||||
scopes:
|
||||
follow: sekvi, bloki, malbloki kaj malsekvi kontojn
|
||||
read: legi la datumojn de via konto
|
||||
write: mesaĝi kiel vi
|
|
@ -62,7 +62,7 @@ fr:
|
|||
buttons:
|
||||
revoke: Annuler
|
||||
confirmations:
|
||||
revoke: Êtes-vous certain?
|
||||
revoke: Êtes-vous certain ?
|
||||
index:
|
||||
application: Application
|
||||
created_at: Créé le
|
||||
|
@ -72,19 +72,19 @@ fr:
|
|||
errors:
|
||||
messages:
|
||||
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_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_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_token:
|
||||
expired: Le jeton d'accès a expiré
|
||||
revoked: Le jeton d'accès a été révoqué
|
||||
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é.
|
||||
server_error: Le serveur d'autorisation a rencontré une condition inattendue qui l'a empêché de remplir la demande.
|
||||
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 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.
|
||||
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.
|
||||
|
|
|
@ -5,8 +5,8 @@ en:
|
|||
about_this: About this instance
|
||||
apps: Apps
|
||||
business_email: 'Business e-mail:'
|
||||
contact: Contact
|
||||
closed_registrations: Registrations are currently closed on this instance.
|
||||
contact: Contact
|
||||
description_headline: What is %{domain}?
|
||||
domain_count_after: other instances
|
||||
domain_count_before: Connected to
|
||||
|
@ -163,3 +163,7 @@ en:
|
|||
invalid_otp_token: Invalid two-factor code
|
||||
will_paginate:
|
||||
page_gap: "…"
|
||||
media_attachments:
|
||||
validations:
|
||||
too_many: Cannot attach more than 4 files
|
||||
images_and_video: Cannot attach a video to a status that already contains images
|
||||
|
|
164
config/locales/eo.yml
Normal file
164
config/locales/eo.yml
Normal file
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
eo:
|
||||
about:
|
||||
about_mastodon: Mastodon estas <em>senpaga, malfermitkoda</em> socia reto. Ĝi estas <em>sencentra</em> alia eblo al komercaj servoj. Ĝi evitigas, ke unusola firmao regu vian tutan komunikadon. Elektu servilon, kiun vi fidas. Kiu ajn estas via elekto, vi povas interagi kun ĉiuj aliaj uzantoj. Iu ajn povas krei sian propran aperaĵon de Mastodon en sia servilo, kaj partopreni en la <em>socia reto</em> tute glate.
|
||||
about_this: Pri tiu aperaĵo
|
||||
apps: Aplikaĵoj
|
||||
business_email: 'Profesia retpoŝt-adreso:'
|
||||
contact: Kontakti
|
||||
description_headline: Kio estas %{domain}?
|
||||
domain_count_after: aliaj aperaĵoj
|
||||
domain_count_before: Konektita al
|
||||
features:
|
||||
api: Malfermita API por aplikaĵoj kaj servoj
|
||||
blocks: Kompletaj iloj por bloki kaj kaŝi
|
||||
characters: Po 500 signoj por ĉiu mesaĝo
|
||||
chronology: Tempolinioj laŭtempaj
|
||||
ethics: 'Etike kreita: neniu reklamo, neniu ŝpurado'
|
||||
gifv: Eblo diskonigi etajn videojn kaj GIFV
|
||||
privacy: Videbleco agordita laŭ la mesaĝo
|
||||
public: Publikaj tempolinioj
|
||||
features_headline: Kiel Mastodon estas malsimila
|
||||
get_started: Komenci
|
||||
links: Ligiloj
|
||||
other_instances: Aliaj aperaĵoj
|
||||
source_code: Fontkodo
|
||||
status_count_after: mesaĝoj
|
||||
status_count_before: Kiu publikigis
|
||||
terms: Terms
|
||||
user_count_after: uzantoj
|
||||
user_count_before: Hejmo de
|
||||
accounts:
|
||||
follow: Sekvi
|
||||
followers: Sekvantoj
|
||||
following: Sekvatoj
|
||||
nothing_here: Estas nenio ĉi tie!
|
||||
people_followed_by: Sekvatoj de %{name}
|
||||
people_who_follow: Sekvantoj de %{name}
|
||||
posts: Mesaĝoj
|
||||
remote_follow: Fore sekvi
|
||||
unfollow: Malsekvi
|
||||
application_mailer:
|
||||
settings: 'Ŝanĝi la retpoŝt-mesaĝajn preferojn: %{link}'
|
||||
signature: Sciigoj de Mastodon el %{instance}
|
||||
view: 'Vidi:'
|
||||
applications:
|
||||
invalid_url: La URL donita ne estas valida
|
||||
auth:
|
||||
change_password: Ŝanĝi pasvorton
|
||||
didnt_get_confirmation: Ĉu vi ne ricevis la instrukciojn por konfirmi?
|
||||
forgot_password: Pasvorto forgesita?
|
||||
login: Ensaluti
|
||||
logout: Elsaluti
|
||||
register: Membriĝi
|
||||
resend_confirmation: Resendi la instrukciojn por konfirmi
|
||||
reset_password: Ŝanĝi la pasvorton
|
||||
set_new_password: Elekti novan pasvorton
|
||||
authorize_follow:
|
||||
error: Bedaŭrinde, okazis eraro provante konsulti la foran konton
|
||||
follow: Sekvi
|
||||
prompt_html: 'Vi (<strong>%{self}</strong>) petis sekvi:'
|
||||
title: Sekvi %{acct}
|
||||
datetime:
|
||||
distance_in_words:
|
||||
about_x_hours: "%{count}h"
|
||||
about_x_months: "%{count}mo"
|
||||
about_x_years: "%{count}j"
|
||||
almost_x_years: "%{count}j"
|
||||
half_a_minute: Ĵus
|
||||
less_than_x_minutes: "%{count}m"
|
||||
less_than_x_seconds: Ĵus
|
||||
over_x_years: "%{count}j"
|
||||
x_days: "%{count}t"
|
||||
x_minutes: "%{count}m"
|
||||
x_months: "%{count}mo"
|
||||
x_seconds: "%{count}s"
|
||||
exports:
|
||||
blocks: Vi blokas
|
||||
csv: CSV
|
||||
follows: Vi sekvas
|
||||
storage: Mediaĵa konservado
|
||||
generic:
|
||||
changes_saved_msg: Ŝanĝoj senprobleme konservitaj!
|
||||
powered_by: povigita de %{link}
|
||||
save_changes: Konservi la ŝanĝojn
|
||||
validation_errors:
|
||||
one: Io ne okazis senprobleme! Bonvolu konsulti la suban erar-raporton.
|
||||
other: Io ne okazis senprobleme! Bonvolu konsulti la subajn %{count} erar-raportojn.
|
||||
imports:
|
||||
preface: Vi povas alporti kelkajn datumojn, kiel listojn de ĉiuj homoj kiujn vi sekvas aŭ blokas, al via konto de ĉi tiu aperaĵo, per dosiero elportita de alia aperaĵo.
|
||||
success: Viaj datumoj estis senprobleme alportitaj kaj estos traktitaj kiel planite.
|
||||
types:
|
||||
blocking: Listo de blokitoj
|
||||
following: Listo de sekvatoj
|
||||
upload: Alporti
|
||||
landing_strip_html: <strong>%{name}</strong> estas uzanto en <strong>%{domain}</strong>. Vi povas sekvi tiun aŭ interagi kun tiu, se vi havas konton ie ajn en la Fediverse. Se vi ne havas, vi povas <a href="%{sign_up_path}">membriĝi ĉi tie.</a>.
|
||||
notification_mailer:
|
||||
digest:
|
||||
body: 'Jen eta resumo de tio, kio okazis en %{instance}, ekde kiam vi laste vizitis en %{since}:'
|
||||
mention: "%{name} menciis vin en:"
|
||||
new_followers_summary:
|
||||
one: Vi ekhavis novan sekvanton! Jej!
|
||||
other: Vi ekhavis %{count} novajn sekvantojn! Mirinde!
|
||||
subject:
|
||||
one: "1 nova sciigo ekde via lasta vizito \U0001F418"
|
||||
other: "%{count} novaj sciigoj ekde via lasta vizito \U0001F418"
|
||||
favourite:
|
||||
body: '%{name} favoris vian mesaĝon:'
|
||||
subject: "%{name} favoris vian mesaĝon"
|
||||
follow:
|
||||
body: "%{name} eksekvis vin:"
|
||||
subject: "%{name} eksekvis vin"
|
||||
follow_request:
|
||||
body: "%{name} petis sekvi vin:"
|
||||
subject: '%{name} petis sekvi vin'
|
||||
mention:
|
||||
body: '%{name} menciis vin en:'
|
||||
subject: '%{name} menciis vin'
|
||||
reblog:
|
||||
body: '%{name} diskonigis vian mesaĝon:'
|
||||
subject: "%{name} diskonigis vian mesaĝon"
|
||||
pagination:
|
||||
next: Sekva
|
||||
prev: Malsekva
|
||||
remote_follow:
|
||||
acct: Enmetu vian uzantnomo@aperaĵo de kie vi volas sekvi tiun uzanton
|
||||
missing_resource: La URL de plusendado ne povis esti trovita
|
||||
proceed: Daŭrigi por plusendi
|
||||
prompt: 'Vi eksekvos:'
|
||||
settings:
|
||||
authorized_apps: Rajtigitaj aplikaĵoj
|
||||
back: Reveni al Mastodon
|
||||
edit_profile: Redakti la profilon
|
||||
export: Elporti datumojn
|
||||
import: Alporti
|
||||
preferences: Preferoj
|
||||
settings: Agordoj
|
||||
two_factor_auth: Dufaktora aŭtentigo
|
||||
statuses:
|
||||
open_in_web: Malfermi retumile
|
||||
over_character_limit: limo de %{max} signoj trapasita
|
||||
show_more: Montri pli
|
||||
visibilities:
|
||||
private: Montri nur al sekvantoj
|
||||
public: Publika
|
||||
unlisted: Publika, sed ne aperos en publikaj tempolinioj
|
||||
stream_entries:
|
||||
click_to_show: Alklaki por montri
|
||||
reblogged: diskonigita
|
||||
sensitive_content: Tikla enhavo
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
two_factor_auth:
|
||||
description_html: Se vi ebligas <strong>dufaktoran aŭtentigon</strong>, vi bezonos vian poŝtelefonon por ensaluti, ĉar ĝi kreos nombrojn, kiujn vi devos entajpi.
|
||||
disable: Malebligi
|
||||
enable: Ebligi
|
||||
instructions_html: "<strong>Skanu tiun QR-kodon per Google Authenticator aŭ per simila aplikaĵo de via poŝtelefono</strong>. De tiam, la aplikaĵo kreos nombrojn, kiujn vi devos entajpi."
|
||||
plaintext_secret_html: 'Rekte legebla sekreta kodo: <samp>%{secret}</samp>'
|
||||
warning: Se vi ne povas agordi aŭtentigan aplikaĵon nun, elektu "malebligi" aŭ vi ne plu povos ensaluti.
|
||||
users:
|
||||
invalid_email: La retpoŝt-adreso ne estas valida
|
||||
invalid_otp_token: La dufaktora aŭtentigila kodo ne estas valida
|
||||
will_paginate:
|
||||
page_gap: "…"
|
|
@ -16,18 +16,18 @@ fi:
|
|||
chronology: Aikajana on kronologisessa järjestyksessä
|
||||
ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa'
|
||||
gifv: GIFV settejä ja lyhyitä videoita
|
||||
privacy: Julkaisu kohtainen yksityisyys aseuts
|
||||
privacy: Julkaisu kohtainen yksityisyys asetus
|
||||
public: Julkiset aikajanat
|
||||
features_headline: Mikä erottaa Mastodonin muista
|
||||
get_started: Aloita käyttö
|
||||
links: Linkit
|
||||
other_instances: Muut palvelimet
|
||||
source_code: Lähdekoodi
|
||||
status_count_after: statukset
|
||||
status_count_before: Kuka loi
|
||||
status_count_after: statusta
|
||||
status_count_before: Ovat luoneet
|
||||
terms: Ehdot
|
||||
user_count_after: käyttäjät
|
||||
user_count_before: Koti käyttäjälle
|
||||
user_count_after: käyttäjälle
|
||||
user_count_before: Koti
|
||||
accounts:
|
||||
follow: Seuraa
|
||||
followers: Seuraajat
|
||||
|
@ -89,7 +89,7 @@ fi:
|
|||
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: Esto lista
|
||||
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>.
|
||||
|
@ -130,9 +130,9 @@ fi:
|
|||
authorized_apps: Valtuutetut ohjelmat
|
||||
back: Takaisin Mastodoniin
|
||||
edit_profile: Muokkaa profiilia
|
||||
export: Datan vienti
|
||||
import: Datan tuonti
|
||||
preferences: Mieltymykset
|
||||
export: Vie dataa
|
||||
import: Tuo dataa
|
||||
preferences: Ominaisuudet
|
||||
settings: Asetukset
|
||||
two_factor_auth: Kaksivaiheinen tunnistus
|
||||
statuses:
|
||||
|
@ -154,7 +154,7 @@ fi:
|
|||
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 tokenit mikä sinun tarvitsee syöttää sisäänkirjautuessa."
|
||||
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:
|
||||
|
|
|
@ -5,6 +5,7 @@ fr:
|
|||
about_this: À propos de cette instance
|
||||
apps: Applications
|
||||
business_email: E-mail professionnel
|
||||
closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. .
|
||||
description_headline: Qu'est-ce que %{domain} ?
|
||||
domain_count_after: autres instances
|
||||
domain_count_before: Connectés à
|
||||
|
|
|
@ -38,7 +38,7 @@ en:
|
|||
follow: Send e-mail when someone follows you
|
||||
follow_request: Send e-mail when someone requests to follow 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'
|
||||
required:
|
||||
mark: "*"
|
||||
|
|
46
config/locales/simple_form.eo.yml
Normal file
46
config/locales/simple_form.eo.yml
Normal file
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
eo:
|
||||
simple_form:
|
||||
hints:
|
||||
defaults:
|
||||
avatar: En la formato PNG, GIF aŭ JPG. Ĝis 2Mo. Estos malgrandigita al 120x120px
|
||||
display_name: 30 signoj pleje
|
||||
header: En la formato PNG, GIF aŭ JPG. Ĝis 2Mo. Estos malgrandigita al 700x335px
|
||||
locked: Vi devos aprobi ĉiun peton de sekvado, kaj viaj mesaĝoj estos senŝanĝe nur por viaj sekvantoj.
|
||||
note: 160 signoj pleje
|
||||
imports:
|
||||
data: Dosiero CSV el alia aperaĵo de Mastodon
|
||||
labels:
|
||||
defaults:
|
||||
avatar: Profilbildo
|
||||
confirm_new_password: Konfirmi novan pasvorton
|
||||
confirm_password: Konfirmi la pasvorton
|
||||
current_password: Nuna pasvorto
|
||||
data: Datumoj
|
||||
display_name: Publika nomo
|
||||
email: Retpoŝt-adreso
|
||||
header: Kapbildo
|
||||
locale: Lingvo
|
||||
locked: Privatigi la konton
|
||||
new_password: Nova pasvorto
|
||||
note: Sinprezento
|
||||
otp_attempt: Dufaktora identigilo
|
||||
password: Pasvorto
|
||||
setting_default_privacy: Videbleco de la mesaĝoj
|
||||
type: Tipo de alportado
|
||||
username: Uzantnomo
|
||||
interactions:
|
||||
must_be_follower: Kaŝi la sciigojn de homoj, kiuj ne sekvas vin
|
||||
must_be_following: Kaŝi la sciigojn de homoj, kiujn vi ne sekas
|
||||
notification_emails:
|
||||
digest: Sendi resumajn retpoŝt-mesaĝojn
|
||||
favourite: Sendi retpoŝt-mesaĝon, kiam iu favoras mesaĝon de vi
|
||||
follow: Sendi retpoŝt-mesaĝon, kiam iu eksekvas vin
|
||||
follow_request: Sendi retpoŝt-mesaĝon, kiam iu petas sekvi vin
|
||||
mention: Sendi retpoŝt-mesaĝon, kiam iu mencias vin
|
||||
reblog: Sendi retpoŝt-mesaĝon, kiam iu diskonigas mesaĝon de vi
|
||||
'no': 'Ne'
|
||||
required:
|
||||
mark: "*"
|
||||
text: bezonata
|
||||
'yes': 'Jes'
|
|
@ -6,7 +6,7 @@ fi:
|
|||
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 julkaisun yksityisyyden vain seuraajille
|
||||
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
|
||||
|
@ -17,7 +17,7 @@ fi:
|
|||
confirm_password: Varmista salasana
|
||||
current_password: Nykyinen salasana
|
||||
data: Data
|
||||
display_name: Näyttö nimi
|
||||
display_name: Näykyvä nimi
|
||||
email: Sähköpostiosoite
|
||||
header: Header
|
||||
locale: Kieli
|
||||
|
@ -38,7 +38,7 @@ fi:
|
|||
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 uudestaanblogaa julkaisusi
|
||||
reblog: Lähetä s-posti kun joku buustaa julkaisusi
|
||||
'no': 'Ei'
|
||||
required:
|
||||
mark: "*"
|
||||
|
|
|
@ -9,7 +9,7 @@ preload_app!
|
|||
|
||||
on_worker_boot do
|
||||
if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno
|
||||
@sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q mailers -q push')
|
||||
@sidekiq_pid ||= spawn('bundle exec sidekiq -q default -q push -q pull -q mailers ')
|
||||
end
|
||||
|
||||
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
||||
|
|
|
@ -11,7 +11,7 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
use_doorkeeper do
|
||||
controllers authorizations: 'oauth/authorizations'
|
||||
controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications'
|
||||
end
|
||||
|
||||
get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddIndexOnMentionsStatusId < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
add_index :mentions, :status_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
class AddNotificationsAndFavouritesIndices < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
add_index :notifications, [:activity_id, :activity_type]
|
||||
add_index :accounts, :url
|
||||
add_index :favourites, :status_id
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20170403172249) do
|
||||
ActiveRecord::Schema.define(version: 20170406215816) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -49,6 +49,7 @@ ActiveRecord::Schema.define(version: 20170403172249) do
|
|||
t.integer "following_count", default: 0, null: false
|
||||
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
|
||||
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", using: :btree
|
||||
t.index ["url"], name: "index_accounts_on_url", using: :btree
|
||||
t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree
|
||||
end
|
||||
|
||||
|
@ -75,6 +76,7 @@ ActiveRecord::Schema.define(version: 20170403172249) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree
|
||||
t.index ["status_id"], name: "index_favourites_on_status_id", using: :btree
|
||||
end
|
||||
|
||||
create_table "follow_requests", force: :cascade do |t|
|
||||
|
@ -127,6 +129,8 @@ ActiveRecord::Schema.define(version: 20170403172249) do
|
|||
t.datetime "created_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 ["status_id"], name: "index_mentions_on_status_id", using: :btree
|
||||
t.index ["status_id"], name: "mentions_status_id_index", using: :btree
|
||||
end
|
||||
|
||||
create_table "mutes", force: :cascade do |t|
|
||||
|
@ -145,6 +149,7 @@ ActiveRecord::Schema.define(version: 20170403172249) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.integer "from_account_id"
|
||||
t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true, using: :btree
|
||||
t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type", using: :btree
|
||||
end
|
||||
|
||||
create_table "oauth_access_grants", force: :cascade do |t|
|
||||
|
|
|
@ -7,7 +7,7 @@ So, you have a working Mastodon instance... now what?
|
|||
|
||||
The following rake task:
|
||||
|
||||
rails mastodon:make_admin USERNAME=alice
|
||||
RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=alice
|
||||
|
||||
Would turn the local user "alice" into an admin.
|
||||
|
||||
|
|
|
@ -3,11 +3,50 @@ Heroku guide
|
|||
|
||||
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?button-url=https://github.com/tootsuite/mastodon&template=https://github.com/tootsuite/mastodon)
|
||||
|
||||
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. It should be noted this has limited testing and could have unpredictable results.
|
||||
Mastodon can be run on a free [Heroku](https://heroku.com) app. It should be
|
||||
noted this has limited testing and could have unpredictable results.
|
||||
|
||||
1. Click the above button.
|
||||
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 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.
|
||||
## Basic setup
|
||||
|
||||
Click the button above to start creating a Heroku app with the Mastodon repo as
|
||||
the source. This tells Heroku to use the `app.json` file which does things like
|
||||
prompt for config variables, set up the right buildpacks, run a postdeploy task,
|
||||
and add the appropriate addons.
|
||||
|
||||
If you don't use the deploy button and app.json approach, you will need to do
|
||||
some of that manually.
|
||||
|
||||
## Domain names and SSL
|
||||
|
||||
You can add your domain name to the Heroku app's setting, and then also use
|
||||
Heroku's (free) auto renewal program for Lets Encrypt certificates, by
|
||||
requesting a cert from the settings screen. You'll have to point your hostname
|
||||
DNS at Heroku using the values heroku gives you on this screen, using whatever
|
||||
method is appropriate for your DNS setup.
|
||||
|
||||
You should set the Heroku config vars of `LOCAL_DOMAIN` to your hostname, and
|
||||
`LOCAL_HTTPS` to "true" as well.
|
||||
|
||||
## Email
|
||||
|
||||
Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans
|
||||
that should suit your interests. Look in `production.rb` to see which config
|
||||
variables need to be set on Heroku for outgoing email to work.
|
||||
|
||||
## File storage
|
||||
|
||||
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.
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy from the Heroku web interface or from the command line. Run:
|
||||
|
||||
`heroku run rails db:migrate`
|
||||
|
||||
after you first deploy to set up the first database.
|
||||
|
||||
To make yourself an admin, you may need to use the `heroku` CLI application after creating an account online:
|
||||
|
||||
`heroku rake mastodon:make_admin USERNAME=yourUsername`
|
||||
|
|
|
@ -11,10 +11,23 @@ map $http_upgrade $connection_upgrade {
|
|||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name example.com;
|
||||
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers EECDH+AESGCM:EECDH+AES;
|
||||
ssl_ecdh_curve prime256v1;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.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
|
||||
|
||||
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 -
|
||||
sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs
|
||||
apt-get install nodejs
|
||||
sudo npm install -g yarn
|
||||
|
||||
## 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:
|
||||
|
||||
cd ~
|
||||
git clone https://github.com/Gargron/mastodon.git live
|
||||
git clone https://github.com/tootsuite/mastodon.git live
|
||||
cd live
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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 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
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue