gearheads
/
mastodon
Archived
2
0
Fork 0

Merge branch 'master' into master

gh/stable
Valentin Ouvrard 2017-04-08 13:39:09 +11:00 committed by GitHub
commit 3e4eb9c95f
128 changed files with 2013 additions and 519 deletions

2
.buildpacks 100644
View File

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

View File

@ -5,3 +5,4 @@ public/assets
node_modules node_modules
storybook storybook
neo4j neo4j
vendor/bundle

View File

@ -22,8 +22,14 @@ OTP_SECRET=
# SINGLE_USER_MODE=true # SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains # Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc # EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration # E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
@ -42,6 +48,16 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_PROTOCOL=http # S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000 # 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 # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

5
.slugignore 100644
View File

@ -0,0 +1,5 @@
node_modules/
.cache/
docs/
spec/
storybook/

View File

@ -1,11 +1,16 @@
FROM ruby:2.3.1-alpine FROM ruby:2.3.1-alpine
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server"
ENV RAILS_ENV=production \ ENV RAILS_ENV=production \
NODE_ENV=production NODE_ENV=production
EXPOSE 3000 4000
WORKDIR /mastodon WORKDIR /mastodon
COPY . /mastodon COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN BUILD_DEPS=" \ RUN BUILD_DEPS=" \
postgresql-dev \ postgresql-dev \
@ -24,8 +29,11 @@ RUN BUILD_DEPS=" \
&& npm install -g npm@3 && npm install -g yarn \ && npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \ && bundle install --deployment --without test development \
&& yarn \ && yarn \
&& npm cache clean \ && yarn cache clean \
&& npm -g cache clean \
&& apk del $BUILD_DEPS \ && apk del $BUILD_DEPS \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/* /var/cache/apk/*
COPY . /mastodon
VOLUME /mastodon/public/system /mastodon/public/assets VOLUME /mastodon/public/system /mastodon/public/assets

View File

@ -8,8 +8,6 @@ gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0' gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails' gem 'jquery-rails'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'puma' gem 'puma'
gem 'hamlit-rails' gem 'hamlit-rails'
@ -36,6 +34,7 @@ gem 'doorkeeper'
gem 'rabl' gem 'rabl'
gem 'rqrcode' gem 'rqrcode'
gem 'twitter-text' gem 'twitter-text'
gem 'ox'
gem 'oj' gem 'oj'
gem 'hiredis' gem 'hiredis'
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
@ -67,9 +66,10 @@ group :development, :test do
end end
group :test do group :test do
gem 'faker'
gem 'rspec-sidekiq'
gem 'simplecov', require: false gem 'simplecov', require: false
gem 'webmock' gem 'webmock'
gem 'rspec-sidekiq'
end end
group :development do group :development do

View File

@ -149,6 +149,8 @@ GEM
erubis (2.7.0) erubis (2.7.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.15.2) fabrication (2.15.2)
faker (1.6.6)
i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
font-awesome-rails (4.6.3.1) font-awesome-rails (4.6.3.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
@ -196,9 +198,6 @@ GEM
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
term-ansicolor (>= 1.3.2) term-ansicolor (>= 1.3.2)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
jbuilder (2.6.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jmespath (1.3.1) jmespath (1.3.1)
jquery-rails (4.1.1) jquery-rails (4.1.1)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
@ -229,7 +228,6 @@ GEM
mimemagic (0.3.2) mimemagic (0.3.2)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.10.1) minitest (5.10.1)
multi_json (1.12.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.0.1) net-ssh (4.0.1)
@ -242,6 +240,7 @@ GEM
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
ox (2.4.11)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -308,8 +307,6 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.1.0)
rake (12.0.0) rake (12.0.0)
rdoc (4.2.2)
json (~> 1.4)
react-rails (1.10.0) react-rails (1.10.0)
babel-transpiler (>= 0.7.0) babel-transpiler (>= 0.7.0)
coffee-script-source (~> 1.8) coffee-script-source (~> 1.8)
@ -379,9 +376,6 @@ GEM
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0) sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3) tilt (>= 1.1, < 3)
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
sidekiq (4.2.7) sidekiq (4.2.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
@ -470,6 +464,7 @@ DEPENDENCIES
doorkeeper doorkeeper
dotenv-rails dotenv-rails
fabrication fabrication
faker
fast_blank fast_blank
font-awesome-rails font-awesome-rails
fuubar fuubar
@ -480,7 +475,6 @@ DEPENDENCIES
http http
httplog httplog
i18n-tasks (~> 0.9.6) i18n-tasks (~> 0.9.6)
jbuilder (~> 2.0)
jquery-rails jquery-rails
letter_opener letter_opener
letter_opener_web letter_opener_web
@ -489,6 +483,7 @@ DEPENDENCIES
nokogiri nokogiri
oj oj
ostatus2 ostatus2
ox
paperclip (~> 5.1) paperclip (~> 5.1)
paperclip-av-transcoder paperclip-av-transcoder
pg pg
@ -511,7 +506,6 @@ DEPENDENCIES
rubocop rubocop
ruby-oembed ruby-oembed
sass-rails (~> 5.0) sass-rails (~> 5.0)
sdoc (~> 0.4.0)
sidekiq sidekiq
sidekiq-unique-jobs sidekiq-unique-jobs
simple-navigation simple-navigation

View File

@ -1,2 +1,2 @@
web: bundle exec puma -C config/puma.rb 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

View File

@ -65,6 +65,8 @@ Consult the example configuration file, `.env.production.sample` for the full li
## Running with Docker and Docker-Compose ## 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: The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
docker-compose build docker-compose build
@ -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. Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
## Deployment on Scalingo
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
[You can view a guide for deployment on Scalingo here.](docs/Running-Mastodon/Scalingo-guide.md)
## Deployment on Heroku (experimental) ## Deployment on Heroku (experimental)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

16
Vagrantfile vendored
View File

@ -84,6 +84,16 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "1024"] 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 end
config.vm.hostname = "mastodon.dev" 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 # This uses the vagrant-hostsupdater plugin, and lets you
# access the development site at http://mastodon.dev. # access the development site at http://mastodon.dev.
# To install: # To install:
# $ vagrant plugin install hostsupdater # $ vagrant plugin install vagrant-hostsupdater
if defined?(VagrantPlugins::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 config.hostsupdater.remove_on_suspend = false
end end
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
# Otherwise, you can access the site at http://localhost:3000 # Otherwise, you can access the site at http://localhost:3000
config.vm.network :forwarded_port, guest: 80, host: 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

View File

@ -47,6 +47,7 @@ import pt from 'react-intl/locale-data/pt';
import hu from 'react-intl/locale-data/hu'; import hu from 'react-intl/locale-data/hu';
import uk from 'react-intl/locale-data/uk'; import uk from 'react-intl/locale-data/uk';
import fi from 'react-intl/locale-data/fi'; import fi from 'react-intl/locale-data/fi';
import eo from 'react-intl/locale-data/eo';
import getMessagesForLocale from '../locales'; import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import createStream from '../stream'; import createStream from '../stream';
@ -59,7 +60,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web' 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({ const Mastodon = React.createClass({

View 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;

View File

@ -5,9 +5,9 @@ const fi = {
"status.mention": "Mainitse @{name}", "status.mention": "Mainitse @{name}",
"status.delete": "Poista", "status.delete": "Poista",
"status.reply": "Vastaa", "status.reply": "Vastaa",
"status.reblog": "Boostaa", "status.reblog": "Buustaa",
"status.favourite": "Tykkää", "status.favourite": "Tykkää",
"status.reblogged_by": "{name} boostattu", "status.reblogged_by": "{name} buustasi",
"status.sensitive_warning": "Arkaluontoista sisältöä", "status.sensitive_warning": "Arkaluontoista sisältöä",
"status.sensitive_toggle": "Klikkaa nähdäksesi", "status.sensitive_toggle": "Klikkaa nähdäksesi",
"video_player.toggle_sound": "Äänet päälle/pois", "video_player.toggle_sound": "Äänet päälle/pois",
@ -22,13 +22,13 @@ const fi = {
"account.followers": "Seuraajia", "account.followers": "Seuraajia",
"account.follows_you": "Seuraa sinua", "account.follows_you": "Seuraa sinua",
"account.requested": "Odottaa hyväksyntää", "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_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.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.home": "Koti",
"column.community": "Paikallinen aikajana", "column.community": "Paikallinen aikajana",
"column.public": "Yhdistetty aikajana", "column.public": "Yleinen aikajana",
"column.notifications": "Ilmoitukset", "column.notifications": "Ilmoitukset",
"tabs_bar.compose": "Luo", "tabs_bar.compose": "Luo",
"tabs_bar.home": "Koti", "tabs_bar.home": "Koti",
@ -41,7 +41,7 @@ const fi = {
"compose_form.spoiler": "Piiloita teksti varoituksen taakse", "compose_form.spoiler": "Piiloita teksti varoituksen taakse",
"compose_form.private": "Merkitse yksityiseksi", "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.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.edit_profile": "Muokkaa profiilia",
"navigation_bar.preferences": "Ominaisuudet", "navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.community_timeline": "Paikallinen aikajana", "navigation_bar.community_timeline": "Paikallinen aikajana",
@ -55,14 +55,14 @@ const fi = {
"upload_form.undo": "Peru", "upload_form.undo": "Peru",
"notification.follow": "{name} seurasi sinua", "notification.follow": "{name} seurasi sinua",
"notification.favourite": "{name} tykkäsi statuksestasi", "notification.favourite": "{name} tykkäsi statuksestasi",
"notification.reblog": "{name} boostasi statustasi", "notification.reblog": "{name} buustasi statustasi",
"notification.mention": "{name} mainitsi sinut", "notification.mention": "{name} mainitsi sinut",
"notifications.column_settings.alert": "Työpöytä ilmoitukset", "notifications.column_settings.alert": "Työpöytä ilmoitukset",
"notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.follow": "Uusia seuraajia:", "notifications.column_settings.follow": "Uusia seuraajia:",
"notifications.column_settings.favourite": "Tykkäyksiä:", "notifications.column_settings.favourite": "Tykkäyksiä:",
"notifications.column_settings.mention": "Mainintoja:", "notifications.column_settings.mention": "Mainintoja:",
"notifications.column_settings.reblog": "Boosteja:", "notifications.column_settings.reblog": "Buusteja:",
}; };
export default fi; export default fi;

View File

@ -6,6 +6,7 @@ import fr from './fr';
import pt from './pt'; import pt from './pt';
import uk from './uk'; import uk from './uk';
import fi from './fi'; import fi from './fi';
import eo from './eo';
const locales = { const locales = {
en, en,
@ -15,7 +16,8 @@ const locales = {
fr, fr,
pt, pt,
uk, uk,
fi fi,
eo
}; };
export default function getMessagesForLocale (locale) { export default function getMessagesForLocale (locale) {

View File

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

View File

@ -1,5 +1,9 @@
@import 'variables'; @import 'variables';
.app-body{
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.button { .button {
background-color: darken($color4, 3%); background-color: darken($color4, 3%);
font-family: inherit; font-family: inherit;

View File

@ -16,7 +16,8 @@ class AccountsController < ApplicationController
end end
format.atom do 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 end
format.activitystreams2 format.activitystreams2

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include Localized
# Prevent CSRF attacks by raising an exception. # Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead. # For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception protect_from_forgery with: :exception
@ -14,7 +16,6 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :set_locale
before_action :set_user_activity before_action :set_user_activity
before_action :check_suspension, if: :user_signed_in? before_action :check_suspension, if: :user_signed_in?
@ -28,12 +29,6 @@ class ApplicationController < ActionController::Base
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
rescue I18n::InvalidLocale
I18n.locale = I18n.default_locale
end
def require_admin! def require_admin!
redirect_to root_path unless current_user&.admin? redirect_to root_path unless current_user&.admin?
end end

View 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

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Localized
skip_before_action :authenticate_resource_owner! skip_before_action :authenticate_resource_owner!
before_action :set_locale
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
@ -12,10 +13,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
rescue I18n::InvalidLocale
I18n.locale = I18n.default_locale
end
end end

View File

@ -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

View File

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

View File

@ -19,7 +19,9 @@ class StreamEntriesController < ApplicationController
end end
end end
format.atom format.atom do
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
end
end end
end end

View File

@ -11,6 +11,7 @@ module SettingsHelper
uk: 'Українська', uk: 'Українська',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
fi: 'Suomi', fi: 'Suomi',
eo: 'Esperanto',
}.freeze }.freeze
def human_locale(locale) def human_locale(locale)

View File

@ -34,10 +34,6 @@ module StreamEntriesHelper
user_signed_in? && @favourited.key?(status.id) ? 'favourited' : '' user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
end end
def proper_status(status)
status.reblog? ? status.reblog : status
end
def rtl?(text) def rtl?(text)
return false if text.empty? return false if text.empty?

View 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

View File

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

View File

@ -34,12 +34,7 @@ class FeedManager
trim(timeline_type, account.id) trim(timeline_type, account.id)
end end
broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) PushUpdateWorker.perform_async(account.id, status.id)
end
def broadcast(timeline_id, options = {})
options[:queued_at] = (Time.now.to_f * 1000.0).to_i
ActionCable.server.broadcast("timeline:#{timeline_id}", options)
end end
def trim(type, account_id) def trim(type, account_id)
@ -81,10 +76,6 @@ class FeedManager
end end
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 private
def redis def redis
@ -118,7 +109,7 @@ class FeedManager
def filter_from_mentions?(status, receiver_id) def filter_from_mentions?(status, receiver_id)
check_for_blocks = [status.account_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? 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 should_filter = receiver_id == status.account_id # Filter if I'm mentioning myself

View File

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

View File

@ -78,6 +78,8 @@ class TagManager
case target.object_type case target.object_type
when :person when :person
account_url(target) account_url(target)
when :note, :comment, :activity
unique_tag(target.created_at, target.id, 'Status')
else else
unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type) unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type)
end end

View File

@ -125,11 +125,11 @@ class Account < ApplicationRecord
end end
def favourited?(status) def favourited?(status)
(status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive? status.proper.favourites.where(account: self).count.positive?
end end
def reblogged?(status) def reblogged?(status)
(status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive? status.proper.reblogs.where(account: self).count.positive?
end end
def keypair def keypair

View File

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

View File

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

View File

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

View File

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

View File

@ -62,8 +62,12 @@ class Status < ApplicationRecord
reply? ? :comment : :note reply? ? :comment : :note
end end
def proper
reblog? ? reblog : self
end
def content def content
reblog? ? reblog.text : text proper.text
end end
def target def target
@ -161,9 +165,9 @@ class Status < ApplicationRecord
return where.not(visibility: [:private, :direct]) if account.nil? return where.not(visibility: [:private, :direct]) if account.nil?
if target_account.blocking?(account) # get rid of blocked peeps if target_account.blocking?(account) # get rid of blocked peeps
where('1 = 0') none
elsif account.id == target_account.id # author can see own stuff elsif account.id == target_account.id # author can see own stuff
where('1 = 1') all
elsif account.following?(target_account) # followers can see followers-only stuff, but also things they are mentioned in elsif account.following?(target_account) # followers can see followers-only stuff, but also things they are mentioned in
joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s) joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s)
.where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct]) .where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct])

View File

@ -5,25 +5,21 @@ class StreamEntry < ApplicationRecord
belongs_to :account, inverse_of: :stream_entries belongs_to :account, inverse_of: :stream_entries
belongs_to :activity, polymorphic: true belongs_to :activity, polymorphic: true
belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry
validates :account, :activity, presence: true 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) } scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
def object_type def object_type
if orphaned? orphaned? || targeted? ? :activity : status.object_type
:activity
else
targeted? ? :activity : activity.object_type
end
end end
def verb def verb
orphaned? ? :delete : activity.verb orphaned? ? :delete : status.verb
end end
def targeted? def targeted?
@ -31,15 +27,15 @@ class StreamEntry < ApplicationRecord
end end
def target def target
orphaned? ? nil : activity.target orphaned? ? nil : status.target
end end
def title def title
orphaned? ? nil : activity.title orphaned? ? nil : status.title
end end
def content def content
orphaned? ? nil : activity.content orphaned? ? nil : status.content
end end
def threaded? def threaded?
@ -47,20 +43,16 @@ class StreamEntry < ApplicationRecord
end end
def thread def thread
orphaned? ? nil : activity.thread orphaned? ? nil : status.thread
end end
def mentions def mentions
activity.respond_to?(:mentions) ? activity.mentions.map(&:account) : [] orphaned? ? [] : status.mentions.map(&:account)
end
def activity
!new_record? ? send(activity_type.underscore) || super : super
end end
private private
def orphaned? def orphaned?
activity.nil? status.nil?
end end
end end

View File

@ -9,20 +9,20 @@ class AfterBlockService < BaseService
private private
def clear_timelines(account, target_account) 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.pipelined do
redis.zrem(mentions_key, status.id) target_account.statuses.select('id').find_each do |status|
redis.zrem(home_key, status.id) redis.zrem(home_key, status.id)
end
end end
end end
def clear_notifications(account, target_account) 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(: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 }).destroy_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 }).destroy_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 }).destroy_all Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).delete_all
end end
def redis def redis

View File

@ -10,31 +10,6 @@ class AuthorizeFollowService < BaseService
private private
def build_xml(follow_request) def build_xml(follow_request)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.authorize_follow_request_salmon(follow_request))
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
end end
end end

View File

@ -18,22 +18,6 @@ class BlockService < BaseService
private private
def build_xml(block) def build_xml(block)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.block_salmon(block))
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
end end
end end

View File

@ -2,7 +2,6 @@
module StreamEntryRenderer module StreamEntryRenderer
def stream_entry_to_xml(stream_entry) 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) AtomSerializer.render(AtomSerializer.new.entry(stream_entry, true))
renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom])
end end
end end

View File

@ -16,6 +16,7 @@ class FanOutOnWriteService < BaseService
return if status.account.silenced? || !status.public_visibility? || status.reblog? return if status.account.silenced? || !status.public_visibility? || status.reblog?
render_anonymous_payload(status)
deliver_to_hashtags(status) deliver_to_hashtags(status)
return if status.reply? && status.in_reply_to_account_id != status.account_id return if status.reply? && status.in_reply_to_account_id != status.account_id
@ -48,23 +49,24 @@ class FanOutOnWriteService < BaseService
end end
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) def deliver_to_hashtags(status)
Rails.logger.debug "Delivering status #{status.id} to hashtags" Rails.logger.debug "Delivering status #{status.id} to hashtags"
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status)
status.tags.pluck(:name).each do |hashtag| status.tags.pluck(:name).each do |hashtag|
FeedManager.instance.broadcast("hashtag:#{hashtag}", event: 'update', payload: payload) Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
FeedManager.instance.broadcast("hashtag:#{hashtag}:local", event: 'update', payload: payload) if status.account.local? Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
end end
end end
def deliver_to_public(status) def deliver_to_public(status)
Rails.logger.debug "Delivering status #{status.id} to public timeline" Rails.logger.debug "Delivering status #{status.id} to public timeline"
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status) Redis.current.publish('timeline:public', @payload)
Redis.current.publish('timeline:public:local', @payload) if status.local?
FeedManager.instance.broadcast(:public, event: 'update', payload: payload)
FeedManager.instance.broadcast('public:local', event: 'update', payload: payload) if status.account.local?
end end
end end

View File

@ -22,26 +22,6 @@ class FavouriteService < BaseService
private private
def build_xml(favourite) def build_xml(favourite)
description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite))
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
end end
end end

View File

@ -45,13 +45,13 @@ class FollowRemoteAccountService < BaseService
account.suspended = true if domain_block && domain_block.suspend? account.suspended = true if domain_block && domain_block.suspend?
account.silenced = true if domain_block && domain_block.silence? account.silenced = true if domain_block && domain_block.silence?
xml = get_feed(account.remote_url) body, xml = get_feed(account.remote_url)
hubs = get_hubs(xml) hubs = get_hubs(xml)
account.uri = get_account_uri(xml) account.uri = get_account_uri(xml)
account.hub_url = hubs.first.attribute('href').value account.hub_url = hubs.first.attribute('href').value
get_profile(xml, account) get_profile(body, account)
account.save! account.save!
account account
@ -61,7 +61,7 @@ class FollowRemoteAccountService < BaseService
def get_feed(url) def get_feed(url)
response = http_client.get(Addressable::URI.parse(url)) response = http_client.get(Addressable::URI.parse(url))
Nokogiri::XML(response) [response.to_s, Nokogiri::XML(response)]
end end
def get_hubs(xml) def get_hubs(xml)
@ -82,12 +82,8 @@ class FollowRemoteAccountService < BaseService
author_uri.content author_uri.content
end end
def get_profile(xml, account) def get_profile(body, account)
update_remote_profile_service.call(xml.at_xpath('/xmlns:feed'), account) RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), false)
end
def update_remote_profile_service
@update_remote_profile_service ||= UpdateRemoteProfileService.new
end end
def http_client def http_client

View File

@ -10,7 +10,7 @@ class FollowService < BaseService
target_account = FollowRemoteAccountService.new.call(uri) target_account = FollowRemoteAccountService.new.call(uri)
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? 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? if target_account.locked?
request_follow(source_account, target_account) request_follow(source_account, target_account)
@ -55,48 +55,10 @@ class FollowService < BaseService
end end
def build_follow_request_xml(follow_request) def build_follow_request_xml(follow_request)
description = "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}" AtomSerializer.render(AtomSerializer.new.follow_request_salmon(follow_request))
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
end end
def build_follow_xml(follow) def build_follow_xml(follow)
description = "#{follow.account.acct} started following #{follow.target_account.acct}" AtomSerializer.render(AtomSerializer.new.follow_salmon(follow))
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
end end
end end

View File

@ -50,7 +50,7 @@ class NotifyService < BaseService
def create_notification def create_notification
@notification.save! @notification.save!
return unless @notification.browserable? return unless @notification.browserable?
FeedManager.instance.broadcast(@recipient.id, event: 'notification', payload: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification)) Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
end end
def send_email def send_email

View File

@ -37,11 +37,11 @@ class PostStatusService < BaseService
def validate_media!(media_ids) def validate_media!(media_ids)
return if media_ids.nil? || !media_ids.is_a?(Enumerable) 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)) 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 media
end end

View File

@ -5,15 +5,15 @@ class ProcessFeedService < BaseService
xml = Nokogiri::XML(body) xml = Nokogiri::XML(body)
xml.encoding = 'utf-8' xml.encoding = 'utf-8'
update_author(xml, account) update_author(body, xml, account)
process_entries(xml, account) process_entries(xml, account)
end end
private private
def update_author(xml, account) def update_author(body, xml, account)
return if xml.at_xpath('/xmlns:feed', xmlns: TagManager::XMLNS).nil? 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 end
def process_entries(xml, account) def process_entries(xml, account)

View File

@ -24,7 +24,7 @@ class ProcessInteractionService < BaseService
return if account.suspended? return if account.suspended?
if salmon.verify(envelope, account.keypair) 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) case verb(xml)
when :follow when :follow
@ -114,7 +114,7 @@ class ProcessInteractionService < BaseService
return if status.nil? 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 end
def favourite!(xml, from_account) def favourite!(xml, from_account)
@ -130,7 +130,7 @@ class ProcessInteractionService < BaseService
end end
def add_post!(body, account) def add_post!(body, account)
process_feed_service.call(body, account) ProcessingWorker.perform_async(account.id, body.force_encoding('UTF-8'))
end end
def status(xml) def status(xml)
@ -153,10 +153,6 @@ class ProcessInteractionService < BaseService
@process_feed_service ||= ProcessFeedService.new @process_feed_service ||= ProcessFeedService.new
end end
def update_remote_profile_service
@update_remote_profile_service ||= UpdateRemoteProfileService.new
end
def remove_status_service def remove_status_service
@remove_status_service ||= RemoveStatusService.new @remove_status_service ||= RemoveStatusService.new
end end

View File

@ -10,31 +10,6 @@ class RejectFollowService < BaseService
private private
def build_xml(follow_request) def build_xml(follow_request)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.reject_follow_request_salmon(follow_request))
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
end end
end end

View File

@ -4,6 +4,8 @@ class RemoveStatusService < BaseService
include StreamEntryRenderer include StreamEntryRenderer
def call(status) def call(status)
@payload = Oj.dump(event: :delete, payload: status.id)
remove_from_self(status) if status.account.local? remove_from_self(status) if status.account.local?
remove_from_followers(status) remove_from_followers(status)
remove_from_mentioned(status) remove_from_mentioned(status)
@ -25,25 +27,23 @@ class RemoveStatusService < BaseService
end end
def remove_from_followers(status) def remove_from_followers(status)
status.account.followers.each do |follower| status.account.followers.where(domain: nil).each do |follower|
next unless follower.local?
unpush(:home, follower, status) unpush(:home, follower, status)
end end
end end
def remove_from_mentioned(status) def remove_from_mentioned(status)
return unless status.local?
notified_domains = [] notified_domains = []
status.mentions.each do |mention| status.mentions.each do |mention|
mentioned_account = mention.account mentioned_account = mention.account
if mentioned_account.local? next if mentioned_account.local?
unpush(:mentions, mentioned_account, status) next if notified_domains.include?(mentioned_account.domain)
else
next if notified_domains.include?(mentioned_account.domain) notified_domains << mentioned_account.domain
notified_domains << mentioned_account.domain send_delete_salmon(mentioned_account, status)
send_delete_salmon(mentioned_account, status)
end
end end
end end
@ -65,17 +65,19 @@ class RemoveStatusService < BaseService
redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id) redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
end end
FeedManager.instance.broadcast(receiver.id, event: 'delete', payload: status.id) Redis.current.publish("timeline:#{receiver.id}", @payload)
end end
def remove_from_hashtags(status) def remove_from_hashtags(status)
status.tags.each do |tag| status.tags.pluck(:name) do |hashtag|
FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'delete', payload: status.id) Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
end end
end end
def remove_from_public(status) def remove_from_public(status)
FeedManager.instance.broadcast(:public, event: 'delete', payload: status.id) Redis.current.publish('timeline:public', @payload)
Redis.current.publish('timeline:public:local', @payload) if status.local?
end end
def redis def redis

View File

@ -11,22 +11,6 @@ class UnblockService < BaseService
private private
def build_xml(block) def build_xml(block)
Nokogiri::XML::Builder.new do |xml| AtomSerializer.render(AtomSerializer.new.unblock_salmon(block))
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
end end
end end

View File

@ -13,26 +13,6 @@ class UnfavouriteService < BaseService
private private
def build_xml(favourite) def build_xml(favourite)
description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite))
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
end end
end end

View File

@ -13,25 +13,6 @@ class UnfollowService < BaseService
private private
def build_xml(follow) def build_xml(follow)
description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow))
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
end end
end end

View File

@ -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

View File

@ -11,8 +11,10 @@
%meta{:name => "theme-color", :content => "#282c37"}/ %meta{:name => "theme-color", :content => "#282c37"}/
%meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/ %meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/
%title %title<
= "#{yield(:page_title)} - " if content_for?(:page_title) - if content_for?(:page_title)
= yield(:page_title)
= ' - '
= Setting.site_title = Setting.site_title
= stylesheet_link_tag 'application', media: 'all' = stylesheet_link_tag 'application', media: 'all'

View File

@ -16,7 +16,7 @@
%strong= display_name(status.account) %strong= display_name(status.account)
= t('stream_entries.reblogged') = 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 - if include_threads
= render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true } = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true }

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,5 @@
Tervetuloa <%= @resource.email %>!
Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä:
<%= confirmation_url(@resource, confirmation_token: @token) %>

View File

@ -0,0 +1,3 @@
<p>Hei <%= @resource.email %>!</p>
<p>Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.</p>

View File

@ -0,0 +1,3 @@
Hei <%= @resource.email %>!
Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.

View File

@ -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>

View File

@ -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.

View File

@ -3,6 +3,8 @@
class Admin::SuspensionWorker class Admin::SuspensionWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(account_id) def perform(account_id)
SuspendAccountService.new.call(Account.find(account_id)) SuspendAccountService.new.call(Account.find(account_id))
end end

View File

@ -13,5 +13,7 @@ class AfterRemoteFollowRequestWorker
follow_request.destroy follow_request.destroy
FollowService.new.call(follow_request.account, updated_account.acct) FollowService.new.call(follow_request.account, updated_account.acct)
rescue ActiveRecord::RecordNotFound
true
end end
end end

View File

@ -13,5 +13,7 @@ class AfterRemoteFollowWorker
follow.destroy follow.destroy
FollowService.new.call(follow.account, updated_account.acct) FollowService.new.call(follow.account, updated_account.acct)
rescue ActiveRecord::RecordNotFound
true
end end
end end

View File

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

View File

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

View File

@ -46,7 +46,7 @@ class ImportWorker
begin begin
FollowService.new.call(from_account, row[0]) 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 next
end end
end end

View File

@ -13,6 +13,9 @@ class Pubsubhubbub::DeliveryWorker
def perform(subscription_id, payload) def perform(subscription_id, payload)
subscription = Subscription.find(subscription_id) subscription = Subscription.find(subscription_id)
headers = {} headers = {}
host = Addressable::URI.parse(subscription.callback_url).host
return if DomainBlock.blocked?(host)
headers['User-Agent'] = 'Mastodon/PubSubHubbub' 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 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) .headers(headers)
.post(subscription.callback_url, body: payload) .post(subscription.callback_url, body: payload)
return subscription.destroy! if response.code > 299 && response.code < 500 && response.code != 429 # HTTP 4xx means error is not temporary, except for 429 (throttling)
raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300 raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300
subscription.touch(:last_successful_delivery_at) subscription.touch(:last_successful_delivery_at)

View File

@ -10,14 +10,10 @@ class Pubsubhubbub::DistributionWorker
return if stream_entry.hidden? return if stream_entry.hidden?
account = stream_entry.account account = stream_entry.account
renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry]))
payload = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
# domains = account.followers_domains
Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription| 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) Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound

View File

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

View File

@ -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

View File

@ -7,7 +7,7 @@ class SalmonWorker
def perform(account_id, body) def perform(account_id, body)
ProcessInteractionService.new.call(body, Account.find(account_id)) ProcessInteractionService.new.call(body, Account.find(account_id))
rescue ActiveRecord::RecordNotFound rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord::RecordNotFound
true true
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,61 @@
---
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:'

View 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

View File

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

View File

@ -5,8 +5,8 @@ en:
about_this: About this instance about_this: About this instance
apps: Apps apps: Apps
business_email: 'Business e-mail:' business_email: 'Business e-mail:'
contact: Contact
closed_registrations: Registrations are currently closed on this instance. closed_registrations: Registrations are currently closed on this instance.
contact: Contact
description_headline: What is %{domain}? description_headline: What is %{domain}?
domain_count_after: other instances domain_count_after: other instances
domain_count_before: Connected to domain_count_before: Connected to
@ -163,3 +163,7 @@ en:
invalid_otp_token: Invalid two-factor code invalid_otp_token: Invalid two-factor code
will_paginate: will_paginate:
page_gap: "&hellip;" page_gap: "&hellip;"
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

View 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: "&hellip;"

View File

@ -16,18 +16,18 @@ fi:
chronology: Aikajana on kronologisessa järjestyksessä chronology: Aikajana on kronologisessa järjestyksessä
ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa' ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa'
gifv: GIFV settejä ja lyhyitä videoita gifv: GIFV settejä ja lyhyitä videoita
privacy: Julkaisu kohtainen yksityisyys aseuts privacy: Julkaisu kohtainen yksityisyys asetus
public: Julkiset aikajanat public: Julkiset aikajanat
features_headline: Mikä erottaa Mastodonin muista features_headline: Mikä erottaa Mastodonin muista
get_started: Aloita käyttö get_started: Aloita käyttö
links: Linkit links: Linkit
other_instances: Muut palvelimet other_instances: Muut palvelimet
source_code: Lähdekoodi source_code: Lähdekoodi
status_count_after: statukset status_count_after: statusta
status_count_before: Kuka loi status_count_before: Ovat luoneet
terms: Ehdot terms: Ehdot
user_count_after: käyttäjät user_count_after: käyttäjälle
user_count_before: Koti käyttäjälle user_count_before: Koti
accounts: accounts:
follow: Seuraa follow: Seuraa
followers: Seuraajat 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 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 success: Datasi on onnistuneesti ladattu ja käsitellään pian
types: types:
blocking: Esto lista blocking: Estetyt lista
following: Seuratut lista following: Seuratut lista
upload: Lähetä 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>. 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 authorized_apps: Valtuutetut ohjelmat
back: Takaisin Mastodoniin back: Takaisin Mastodoniin
edit_profile: Muokkaa profiilia edit_profile: Muokkaa profiilia
export: Datan vienti export: Vie dataa
import: Datan tuonti import: Tuo dataa
preferences: Mieltymykset preferences: Ominaisuudet
settings: Asetukset settings: Asetukset
two_factor_auth: Kaksivaiheinen tunnistus two_factor_auth: Kaksivaiheinen tunnistus
statuses: 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. 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ä disable: Poista käytöstä
enable: Ota käyttöön 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>' 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. 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: users:

View File

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

View File

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

View File

@ -0,0 +1,46 @@
---
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'

View File

@ -6,7 +6,7 @@ fi:
avatar: PNG, GIF tai JPG. Korkeintaan 2MB. Skaalataan kokoon 120x120px avatar: PNG, GIF tai JPG. Korkeintaan 2MB. Skaalataan kokoon 120x120px
display_name: Korkeintaan 30 merkkiä display_name: Korkeintaan 30 merkkiä
header: PNG, GIF tai JPG. Korkeintaan 2MB. Skaalataan kokoon 700x335px 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ä note: Korkeintaan 160 merkkiä
imports: imports:
data: CSV tiedosto tuotu toiselta Mastodon palvelimelta data: CSV tiedosto tuotu toiselta Mastodon palvelimelta
@ -17,7 +17,7 @@ fi:
confirm_password: Varmista salasana confirm_password: Varmista salasana
current_password: Nykyinen salasana current_password: Nykyinen salasana
data: Data data: Data
display_name: Näyttö nimi display_name: Näykyvä nimi
email: Sähköpostiosoite email: Sähköpostiosoite
header: Header header: Header
locale: Kieli locale: Kieli
@ -38,7 +38,7 @@ fi:
follow: Lähetä s-posti kun joku seuraa sinua follow: Lähetä s-posti kun joku seuraa sinua
follow_request: Lähetä s-posti kun joku pyytää seurata sinua follow_request: Lähetä s-posti kun joku pyytää seurata sinua
mention: Lähetä s-posti kun joku mainitsee sinut 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' 'no': 'Ei'
required: required:
mark: "*" mark: "*"

View File

@ -9,7 +9,7 @@ preload_app!
on_worker_boot do on_worker_boot do
if ENV['HEROKU'] # Spawn the workers from Puma, to only use one dyno 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 end
ActiveRecord::Base.establish_connection if defined?(ActiveRecord) ActiveRecord::Base.establish_connection if defined?(ActiveRecord)

View File

@ -11,7 +11,7 @@ Rails.application.routes.draw do
end end
use_doorkeeper do use_doorkeeper do
controllers authorizations: 'oauth/authorizations' controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications'
end end
get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta

View File

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

View File

@ -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

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170403172249) do ActiveRecord::Schema.define(version: 20170406215816) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -49,6 +49,7 @@ ActiveRecord::Schema.define(version: 20170403172249) do
t.integer "following_count", default: 0, null: false 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 "(((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 "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 t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true, using: :btree
end end
@ -75,6 +76,7 @@ ActiveRecord::Schema.define(version: 20170403172249) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id", "status_id"], name: "index_favourites_on_account_id_and_status_id", unique: true, using: :btree 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 end
create_table "follow_requests", force: :cascade do |t| 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 "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree
t.index ["status_id"], name: "index_mentions_on_status_id", using: :btree
t.index ["status_id"], name: "mentions_status_id_index", using: :btree
end end
create_table "mutes", force: :cascade do |t| create_table "mutes", force: :cascade do |t|
@ -145,6 +149,7 @@ ActiveRecord::Schema.define(version: 20170403172249) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "from_account_id" t.integer "from_account_id"
t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true, using: :btree 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 end
create_table "oauth_access_grants", force: :cascade do |t| create_table "oauth_access_grants", force: :cascade do |t|

View File

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

View File

@ -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) [![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. ## Basic setup
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). Click the button above to start creating a Heroku app with the Mastodon repo as
* 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. the source. This tells Heroku to use the `app.json` file which does things like
* 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. prompt for config variables, set up the right buildpacks, run a postdeploy task,
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. 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`

View File

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

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