merge catstodon/main into main

main
anna 1 year ago
commit 380567f453
Signed by: fef
GPG Key ID: EC22E476DC2D3D84

@ -25,7 +25,7 @@ jobs:
- name: Set up Ruby - name: Set up Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.0' ruby-version: .ruby-version
bundler-cache: true bundler-cache: true
- name: Check locale file normalization - name: Check locale file normalization
run: bundle exec i18n-tasks check-normalized run: bundle exec i18n-tasks check-normalized

@ -53,7 +53,7 @@ jobs:
- name: Set-up Node.js - name: Set-up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16.x node-version-file: .nvmrc
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile

71
Vagrantfile vendored

@ -3,16 +3,14 @@
ENV["PORT"] ||= "3000" ENV["PORT"] ||= "3000"
$provision = <<SCRIPT $provisionA = <<SCRIPT
cd /vagrant # This is where the host folder/repo is mounted
# Add the yarn repo + yarn repo keys # Add the yarn repo + yarn repo keys
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
# Add repo for NodeJS # Add repo for NodeJS
curl -sL https://deb.nodesource.com/setup_14.x | sudo bash - curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
# Add firewall rule to redirect 80 to PORT and save # Add firewall rule to redirect 80 to PORT and save
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
@ -33,32 +31,56 @@ sudo apt-get install \
redis-tools \ redis-tools \
postgresql \ postgresql \
postgresql-contrib \ postgresql-contrib \
yarn \
libicu-dev \ libicu-dev \
libidn11-dev \ libidn11-dev \
libreadline-dev \ libreadline6-dev \
libpam0g-dev \ autoconf \
bison \
build-essential \
ffmpeg \
file \
gcc \
libffi-dev \
libgdbm-dev \
libjemalloc-dev \
libncurses5-dev \
libprotobuf-dev \
libssl-dev \
libyaml-dev \
pkg-config \
protobuf-compiler \
zlib1g-dev \
-y -y
# Install rvm # Install rvm
read RUBY_VERSION < .ruby-version sudo apt-add-repository -y ppa:rael-gc/rvm
sudo apt-get install rvm -y
curl -sSL https://rvm.io/mpapis.asc | gpg --import sudo usermod -a -G rvm $USER
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
SCRIPT
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION $provisionB = <<SCRIPT
source /home/vagrant/.rvm/scripts/rvm
source "/etc/profile.d/rvm.sh"
# Install Ruby # Install Ruby
rvm reinstall ruby-$RUBY_VERSION --disable-binary read RUBY_VERSION < /vagrant/.ruby-version
rvm install ruby-$RUBY_VERSION --disable-binary
# Configure database # Configure database
sudo -u postgres createuser -U postgres vagrant -s sudo -u postgres createuser -U postgres vagrant -s
sudo -u postgres createdb -U postgres mastodon_development sudo -u postgres createdb -U postgres mastodon_development
# Install gems and node modules cd /vagrant # This is where the host folder/repo is mounted
# Install gems
gem install bundler foreman gem install bundler foreman
bundle install bundle install
# Install node modules
sudo corepack enable
yarn set version classic
yarn install yarn install
# Build Mastodon # Build Mastodon
@ -72,18 +94,11 @@ echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
SCRIPT SCRIPT
$start = <<SCRIPT
echo 'To start server'
echo ' $ vagrant ssh -c "cd /vagrant && foreman start"'
SCRIPT
VAGRANTFILE_API_VERSION = "2" VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/bionic64" config.vm.box = "ubuntu/focal64"
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
@ -100,7 +115,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Use "virtio" network interfaces for better performance. # Use "virtio" network interfaces for better performance.
vb.customize ["modifyvm", :id, "--nictype1", "virtio"] vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
vb.customize ["modifyvm", :id, "--nictype2", "virtio"] vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
end end
# This uses the vagrant-hostsupdater plugin, and lets you # This uses the vagrant-hostsupdater plugin, and lets you
@ -118,7 +132,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
end end
if config.vm.networks.any? { |type, options| type == :private_network } if config.vm.networks.any? { |type, options| type == :private_network }
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp', 'actimeo=1'] config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1']
else else
config.vm.synced_folder ".", "/vagrant" config.vm.synced_folder ".", "/vagrant"
end end
@ -129,9 +143,12 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.network :forwarded_port, guest: 8080, host: 8080 config.vm.network :forwarded_port, guest: 8080, host: 8080
# Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision' # Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
config.vm.provision :shell, inline: $provision, privileged: false config.vm.provision :shell, inline: $provisionA, privileged: false, reset: true
config.vm.provision :shell, inline: $provisionB, privileged: false
# Start up script, runs on every 'vagrant up' config.vm.post_up_message = <<MESSAGE
config.vm.provision :shell, inline: $start, run: 'always', privileged: false To start server
$ vagrant ssh -c "cd /vagrant && foreman start"
MESSAGE
end end

@ -55,12 +55,14 @@ module Admin
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
log_action :approve, @account.user
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
log_action :reject, @account.user
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
end end

@ -54,12 +54,14 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
log_action :approve, @account.user
render json: @account, serializer: REST::Admin::AccountSerializer render json: @account, serializer: REST::Admin::AccountSerializer
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
log_action :reject, @account.user
render_empty render_empty
end end

@ -13,7 +13,7 @@ class Api::V1::FiltersController < Api::BaseController
def create def create
ApplicationRecord.transaction do ApplicationRecord.transaction do
filter_category = current_account.custom_filters.create!(resource_params) filter_category = current_account.custom_filters.create!(filter_params)
@filter = filter_category.keywords.create!(keyword_params) @filter = filter_category.keywords.create!(keyword_params)
end end
@ -52,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController
end end
def resource_params def resource_params
params.permit(:phrase, :expires_in, :irreversible, context: []) params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
end end
def filter_params def filter_params
resource_params.slice(:expires_in, :irreversible, :context) resource_params.slice(:phrase, :expires_in, :irreversible, :context)
end end
def keyword_params def keyword_params

@ -7,6 +7,7 @@ import Avatar from 'flavours/glitch/components/avatar';
import Permalink from 'flavours/glitch/components/permalink'; import Permalink from 'flavours/glitch/components/permalink';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
const Account = connect(state => ({ const Account = connect(state => ({
account: state.getIn(['accounts', me]), account: state.getIn(['accounts', me]),
@ -16,7 +17,14 @@ const Account = connect(state => ({
</Permalink> </Permalink>
)); ));
export default @withRouter const mapDispatchToProps = (dispatch) => ({
openClosedRegistrationsModal() {
dispatch(openModal('CLOSED_REGISTRATIONS'));
},
});
export default @connect(null, mapDispatchToProps)
@withRouter
class Header extends React.PureComponent { class Header extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -24,12 +32,13 @@ class Header extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
openClosedRegistrationsModal: PropTypes.func,
location: PropTypes.object, location: PropTypes.object,
}; };
render () { render () {
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
const { location } = this.props; const { location, openClosedRegistrationsModal } = this.props;
let content; let content;
@ -41,10 +50,26 @@ class Header extends React.PureComponent {
</> </>
); );
} else { } else {
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href='/auth/sign_up' className='button button-tertiary'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
}
content = ( content = (
<> <>
<a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
<a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> {signupButton}
</> </>
); );
} }

@ -1,7 +1,180 @@
import inherited from 'mastodon/locales/cs.json'; import inherited from 'mastodon/locales/cs.json';
const messages = { const messages = {
// No translations available. 'about.fork_disclaimer': 'Glitch-soc je svobodný software s otevřeným zdrojovým kódem založený na Mastodonu.',
'settings.layout_opts': 'Možnosti rozvržení',
'settings.layout': 'Rozložení:',
'layout.current_is': 'Nastavené rozložení je:',
'layout.auto': 'Automatické',
'layout.desktop': 'Desktop',
'layout.mobile': 'Mobil',
'layout.hint.auto': 'Vybrat rozložení automaticky v závislosti na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
'layout.hint.desktop': 'Použít vícesloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
'layout.hint.single': 'Použít jednosloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
'navigation_bar.app_settings': 'Nastavení aplikace',
'navigation_bar.featured_users': 'Vybraní uživatelé',
'endorsed_accounts_editor.endorsed_accounts': 'Vybrané účty',
'navigation_bar.info': 'Rozšířené informace',
'navigation_bar.misc': 'Různé',
'navigation_bar.keyboard_shortcuts': 'Klávesové zkratky',
'getting_started.onboarding': 'Ukaž mi to tu',
'onboarding.skip': 'Přeskočit',
'onboarding.next': 'Další',
'onboarding.done': 'Hotovo',
'onboarding.page_one.federation': '{domain} je \'instance\' Mastodonu. Mastodon je síť nezávislých serverů, které jsou spolu propojené do jedné velké sociální sítě. Těmto serverům říkáme instance.',
'onboarding.page_one.handle': 'Jste na instanci {domain}, takže celá adresa vašeho profilu je {handle}',
'onboarding.page_one.welcome': 'Vítá vás {domain}!',
'onboarding.page_two.compose': 'Příspěvky se píší v levém sloupci. Pomocí ikon pod příspěvkem k němu můžete připojit obrázky, změnit úroveň soukromí nebo přidat varování o obsahu.',
'onboarding.page_three.search': 'Pomocí vyhledávací lišty můžete hledat lidi nebo hashtagy. Pokud hledáte někoho z jiné instance, musíte použít celou adresu jeho profilu.',
'onboarding.page_three.profile': 'Upravte si svůj profil a nastavte si profilový obrázek, jméno, a krátký text o sobě. Naleznete tam i další možnosti nastavení.',
'onboarding.page_four.home': 'Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.',
'onboarding.page_four.notifications': 'Notifikace se zobrazí, když s vámi někdo interaguje.',
'onboarding.page_five.public_timelines': 'Místní časová osa zobrazuje veřejné příspěvky všech uživatelů instance {domain}. Federovaná časová osa zobrazí příspěvky od všech, koho uživatelé instance {domain} sledují. Tyto veřejné časové osy jsou skvělý způsob, jak objevit nové lidi.',
'onboarding.page_six.almost_done': 'Skoro hotovo...',
'onboarding.page_six.github': 'Na serveru {domain} běží Glitchsoc. Glitchsoc je přátelský {fork} programu {Mastodon}, a je kompatibilní s jakoukoliv jinou mastodoní instancí nebo aplikací. Glitchsoc je zcela svobodný a má otevřený zdrojový kód. Na stránce {github} můžete hlásit chyby, žádat o nové funkce, nebo ke kódu vlastnoručně přispět.',
'onboarding.page_six.apps_available': 'Jsou dostupné {apps} pro iOS, Android i jiné platformy.',
'onboarding.page_six.various_app': 'mobilní aplikace',
'onboarding.page_six.appetoot': 'Veselé mastodonění!',
'settings.auto_collapse': 'Automaticky sbalit',
'settings.auto_collapse_all': 'Všechno',
'settings.auto_collapse_lengthy': 'Dlouhé příspěvky',
'settings.auto_collapse_media': 'Příspěvky s přílohami',
'settings.auto_collapse_notifications': 'Oznámení',
'settings.auto_collapse_reblogs': 'Boosty',
'settings.auto_collapse_replies': 'Odpovědi',
'settings.show_action_bar': 'Zobrazit ve sbalených příspěvcích tlačítka s akcemi',
'settings.close': 'Zavřít',
'settings.collapsed_statuses': 'Sbalené příspěvky',
'settings.confirm_boost_missing_media_description': 'Zobrazit potvrzovací dialog před boostnutím příspěvku s chybějícími popisky obrázků',
'boost_modal.missing_description': 'Příspěvek obsahuje obrázky bez popisků',
'settings.enable_collapsed': 'Povolit sbalené příspěvky',
'settings.enable_collapsed_hint': 'U sbalených příspěvků je část jejich obsahu skrytá, aby zabraly méně místa na obrazovce. (Tohle není stejná funkce jako varování o obsahu.)',
'settings.general': 'Obecné',
'settings.hicolor_privacy_icons': 'Barevné ikony soukromí',
'settings.hicolor_privacy_icons.hint': 'Zobrazit ikony úrovně soukromí příspěvků v jasných, snadno rozlišitelných barvách',
'settings.image_backgrounds': 'Obrázkové pozadí',
'settings.image_backgrounds_media': 'Náhled médií ve sbalených příspěvcích',
'settings.image_backgrounds_media_hint': 'Pokud jsou k příspěvku přiložena média, použije se první z nich jako pozadí',
'settings.image_backgrounds_users': 'Nastavit sbaleným příspěvkům obrázkové pozadí',
'settings.inline_preview_cards': 'Zobrazit v časové ose náhledy externích odkazů',
'settings.media': 'Média',
'settings.media_letterbox': 'Neořezávat obrázky',
'settings.media_letterbox_hint': 'Místo výřezu obrázku zobrazit obrázek celý, doplněný podle potřeby o prázdné okraje',
'settings.media_fullwidth': 'Zobrazit náhledy v plné šířce',
'settings.notifications_opts': 'Možnosti oznámení',
'settings.notifications.tab_badge': 'Zobrazit počet nepřečtených oznámení',
'settings.notifications.tab_badge.hint': 'Počet nepřečtených oznámení se viditelně zobrazí na hlavní stránce (pokud není seznam oznámení viditelný)',
'settings.notifications.favicon_badge': 'Zobrazit počet na ikoně serveru',
'settings.notifications.favicon_badge.hint': 'Zobrazí počet nepřečtených oznámení na ikoně serveru',
'settings.preferences': 'Předvolby',
'settings.rewrite_mentions': 'Přepsat zmínky v zobrazených příspěvcích',
'settings.rewrite_mentions_no': 'Nepřepisovat zmínky',
'settings.rewrite_mentions_acct': 'Přepsat uživatelským jménem a doménou (pokud je účet na jiném serveru)',
'settings.rewrite_mentions_username': 'Přepsat uživatelským jménem',
'settings.show_reply_counter': 'Zobrazit odhad počtu odpovědí',
'settings.status_icons': 'Ikony u příspěvků',
'settings.status_icons_language': 'Indikace jazyk',
'settings.status_icons_reply': 'Indikace odpovědi',
'settings.status_icons_local_only': 'Indikace lokálního příspěvku',
'settings.status_icons_media': 'Indikace obrázků a anket',
'settings.status_icons_visibility': 'Indikace úrovně soukromí',
'settings.tag_misleading_links': 'Označit zavádějící odkazy',
'settings.tag_misleading_links.hint': 'Zobrazit skutečný cíl u každého odkazu, který ho explicitně nezmiňuje',
'settings.wide_view': 'Široké sloupce (pouze v režimu Desktop)',
'settings.wide_view_hint': 'Sloupce se roztáhnout, aby lépe vyplnily dostupný prostor.',
'settings.navbar_under': 'Navigační lišta vespod (pouze v režimu Mobil)',
'settings.compose_box_opts': 'Editační pole',
'settings.always_show_spoilers_field': 'Vždy zobrazit pole pro varování o obsahu',
'settings.prepend_cw_re': 'Při odpovídání přidat před varování o obsahu “re: ”',
'settings.preselect_on_reply': 'Při odpovědi označit uživatelská jména',
'settings.preselect_on_reply_hint': 'Při odpovídání na konverzaci s více účastníky se jména všech kromě prvního označí, aby šla jednoduše smazat',
'settings.confirm_missing_media_description': 'Zobrazit potvrzovací dialog při odesílání příspěvku, ve kterém chybí popisky obrázků',
'settings.confirm_before_clearing_draft': 'Zobrazit potvrzovací dialog před přepsáním právě vytvářené zprávy',
'settings.show_content_type_choice': 'Zobrazit volbu formátu příspěvku',
'settings.side_arm': 'Vedlejší odesílací tlačítko:',
'settings.side_arm.none': 'Žádné',
'settings.side_arm_reply_mode': 'Při odpovídání na příspěvek by vedlejší odesílací tlačítko mělo:',
'settings.side_arm_reply_mode.keep': 'Použít svou nastavenou úroveň soukromí',
'settings.side_arm_reply_mode.copy': 'Použít úroveň soukromí příspěvku, na který odpovídáte',
'settings.side_arm_reply_mode.restrict': 'Zvýšit úroveň soukromí nejméně na úroveň příspěvku, na který odpovídáte',
'settings.content_warnings': 'Varování o obsahu',
'settings.content_warnings_shared_state': 'Zobrazit/schovat všechny kopie naráz',
'settings.content_warnings_shared_state_hint': 'Tlačítko varování o obsahu bude mít efekt na všechny kopie příspěvku naráz, stejně jako na běžném Mastodonu. Nebude pak možné automaticky sbalit jakoukoliv kopii příspěvku, která má rozbalené varování o obsahu',
'settings.content_warnings_media_outside': 'Zobrazit obrázky a videa mimo varování o obsahu',
'settings.content_warnings_media_outside_hint': 'Obrázky a videa z příspěvku s varováním o obsahu se zobrazí se separátním přepínačem zobrazení, stejně jako na běžném Mastodonu.',
'settings.content_warnings_unfold_opts': 'Možnosti automatického rozbalení',
'settings.enable_content_warnings_auto_unfold': 'Vždy rozbalit příspěvky označené varováním o obsahu',
'settings.deprecated_setting': 'Tato možnost se nyní nastavuje v {settings_page_link}',
'settings.shared_settings_link': 'předvolbách Mastodonu',
'settings.content_warnings_filter': 'Tato varování o obsahu automaticky nerozbalovat:',
'settings.content_warnings.regexp': 'Regulární výraz',
'settings.media_reveal_behind_cw': 'Automaticky zobrazit média označená varováním o obsahu',
'settings.pop_in_player': 'Povolit plovoucí okno přehrávače',
'settings.pop_in_position': 'Pozice plovoucího okna:',
'settings.pop_in_left': 'Vlevo',
'settings.pop_in_right': 'Vpravo',
'status.collapse': 'Sbalit',
'status.uncollapse': 'Rozbalit',
'status.in_reply_to': 'Tento příspěvek je odpověď',
'status.has_preview_card': 'Obsahuje náhled odkazu',
'status.has_pictures': 'Obsahuje obrázky',
'status.is_poll': 'Tento příspěvek je anketa',
'status.has_video': 'Obsahuje video',
'status.has_audio': 'Obsahuje audio',
'status.local_only': 'Viditelné pouze z vaší instance',
'media_gallery.sensitive': 'Citlivý obsah',
'favourite_modal.combo': 'Příště můžete pro přeskočení stisknout {combo}',
'home.column_settings.show_direct': 'Zobrazit přímé zprávy',
'notification_purge.start': 'Čistící režim',
'notifications.mark_as_read': 'Označit všechna oznámení jako přečtená',
'notification.markForDeletion': 'Označit pro smazání',
'notifications.clear': 'Vymazat všechna oznámení',
'notifications.marked_clear_confirmation': 'Určitě chcete trvale smazat všechna vybraná oznámení?',
'notifications.marked_clear': 'Smazat vybraná oznámení',
'notification_purge.btn_all': 'Vybrat\nvše',
'notification_purge.btn_none': 'Nevybrat\nnic',
'notification_purge.btn_invert': 'Obrátit\nvýběr',
'notification_purge.btn_apply': 'Smazat\nvybrané',
'compose.attach.upload': 'Nahrát soubor',
'compose.attach.doodle': 'Něco namalovat',
'compose.attach': 'Připojit...',
'advanced_options.local-only.short': 'Lokální příspěvek',
'advanced_options.local-only.long': 'Neposílat na jiné servery',
'advanced_options.local-only.tooltip': 'Tento příspěvek je pouze lokální',
'advanced_options.icon_title': 'Pokročilá nastavení',
'advanced_options.threaded_mode.short': 'Režim vlákna',
'advanced_options.threaded_mode.long': 'Po odeslání automaticky otevře pole pro odpověď',
'advanced_options.threaded_mode.tooltip': 'Režim vlákna je zapnutý',
'home.column_settings.advanced': 'Pokročilé',
'home.column_settings.filter_regex': 'Filtrovat podle regulárních výrazů',
'compose_form.poll.single_choice': 'Povolit jednu odpověď',
'compose_form.poll.multiple_choices': 'Povolit více odpovědí',
'compose.content-type.plain': 'Prostý text',
'content-type.change': 'Formát příspěvku',
'compose_form.spoiler': 'Přidat varování o obsahu',
'direct.group_by_conversations': 'Seskupit do konverzací',
'column.toot': 'Příspěvky a odpovědi',
'confirmation_modal.do_not_ask_again': 'Příště se už neptat',
'keyboard_shortcuts.bookmark': 'Přidat do záložek',
'keyboard_shortcuts.toggle_collapse': 'Sbalit/rozbalit příspěvek',
'keyboard_shortcuts.secondary_toot': 'Odeslat příspěvek s druhotným nastavením soukromí',
'column.subheading': 'Různé',
}; };
export default Object.assign({}, inherited, messages); export default Object.assign({}, inherited, messages);

@ -6,6 +6,14 @@ en:
skins: skins:
glitch: glitch:
default: Default default: Default
cs:
flavours:
glitch:
description: Výchozí rozhraní instancí GlitchSoc.
name: Glitch
skins:
glitch:
default: Výchozí
pl: pl:
flavours: flavours:
glitch: glitch:

@ -65,6 +65,7 @@ $ui-header-height: 55px;
z-index: 2; z-index: 2;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
overflow: hidden;
&__logo { &__logo {
display: inline-flex; display: inline-flex;
@ -81,10 +82,15 @@ $ui-header-height: 55px;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 0 10px; padding: 0 10px;
overflow: hidden;
.button { .button {
flex: 0 0 auto; flex: 0 0 auto;
} }
.button-tertiary {
flex-shrink: 1;
}
} }
} }

@ -1306,7 +1306,8 @@ img.modal-warning {
width: 600px; width: 600px;
background: $ui-base-color; background: $ui-base-color;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow-x: hidden;
overflow-y: auto;
position: relative; position: relative;
display: block; display: block;
padding: 20px; padding: 20px;

@ -6,6 +6,14 @@ en:
skins: skins:
vanilla: vanilla:
default: Default default: Default
cs:
flavours:
vanilla:
description: Standardní rozhraní Mastodonu. Některé funkce GlitchSoc v něm nejsou podporované.
name: Standardní Mastodon
skins:
vanilla:
default: Výchozí
pl: pl:
flavours: flavours:
vanilla: vanilla:

@ -6,6 +6,7 @@ import { registrationsOpen, me } from 'mastodon/initial_state';
import Avatar from 'mastodon/components/avatar'; import Avatar from 'mastodon/components/avatar';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
const Account = connect(state => ({ const Account = connect(state => ({
account: state.getIn(['accounts', me]), account: state.getIn(['accounts', me]),
@ -15,7 +16,14 @@ const Account = connect(state => ({
</Link> </Link>
)); ));
export default @withRouter const mapDispatchToProps = (dispatch) => ({
openClosedRegistrationsModal() {
dispatch(openModal('CLOSED_REGISTRATIONS'));
},
});
export default @connect(null, mapDispatchToProps)
@withRouter
class Header extends React.PureComponent { class Header extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -23,12 +31,13 @@ class Header extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
openClosedRegistrationsModal: PropTypes.func,
location: PropTypes.object, location: PropTypes.object,
}; };
render () { render () {
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
const { location } = this.props; const { location, openClosedRegistrationsModal } = this.props;
let content; let content;
@ -40,10 +49,26 @@ class Header extends React.PureComponent {
</> </>
); );
} else { } else {
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href='/auth/sign_up' className='button button-tertiary'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
}
content = ( content = (
<> <>
<a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
<a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> {signupButton}
</> </>
); );
} }

@ -2,6 +2,10 @@ en:
skins: skins:
glitch: glitch:
contrast: High contrast contrast: High contrast
cs:
skins:
glitch:
contrast: Vysoký kontrast
es: es:
skins: skins:
glitch: glitch:

@ -2,6 +2,10 @@ en:
skins: skins:
glitch: glitch:
mastodon-light: Mastodon (light) mastodon-light: Mastodon (light)
cs:
skins:
glitch:
mastodon-light: Mastodon (světlý)
es: es:
skins: skins:
glitch: glitch:

@ -2,6 +2,10 @@ en:
skins: skins:
vanilla: vanilla:
contrast: High contrast contrast: High contrast
cs:
skins:
vanilla:
contrast: Vysoký kontrast
es: es:
skins: skins:
vanilla: vanilla:

@ -2,6 +2,10 @@ en:
skins: skins:
vanilla: vanilla:
mastodon-light: Mastodon (light) mastodon-light: Mastodon (light)
cs:
skins:
vanilla:
mastodon-light: Mastodon (světlý)
es: es:
skins: skins:
glitch: glitch:

@ -2,3 +2,7 @@ en:
skins: skins:
vanilla: vanilla:
win95: 95 win95: 95
cs:
skins:
vanilla:
win95: Windows 95

@ -2231,6 +2231,7 @@ $ui-header-height: 55px;
z-index: 2; z-index: 2;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
overflow: hidden;
&__logo { &__logo {
display: inline-flex; display: inline-flex;
@ -2247,10 +2248,15 @@ $ui-header-height: 55px;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 0 10px; padding: 0 10px;
overflow: hidden;
.button { .button {
flex: 0 0 auto; flex: 0 0 auto;
} }
.button-tertiary {
flex-shrink: 1;
}
} }
} }
@ -7156,10 +7162,12 @@ noscript {
.verified { .verified {
border: 1px solid rgba($valid-value-color, 0.5); border: 1px solid rgba($valid-value-color, 0.5);
margin-top: -1px;
&:first-child { &:first-child {
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
margin-top: 0;
} }
&:last-child { &:last-child {
@ -7991,7 +7999,8 @@ noscript {
width: 600px; width: 600px;
background: $ui-base-color; background: $ui-base-color;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow-x: hidden;
overflow-y: auto;
position: relative; position: relative;
display: block; display: block;
padding: 20px; padding: 20px;

@ -10,7 +10,7 @@ class DeliveryFailureTracker
end end
def track_failure! def track_failure!
redis.sadd(exhausted_deliveries_key, today) redis.sadd?(exhausted_deliveries_key, today)
UnavailableDomain.create(domain: @host) if reached_failure_threshold? UnavailableDomain.create(domain: @host) if reached_failure_threshold?
end end

@ -322,24 +322,24 @@ class FeedManager
def clean_feeds!(type, ids) def clean_feeds!(type, ids)
reblogged_id_sets = {} reblogged_id_sets = {}
redis.pipelined do redis.pipelined do |pipeline|
ids.each do |feed_id| ids.each do |feed_id|
redis.del(key(type, feed_id)) pipeline.del(key(type, feed_id))
reblog_key = key(type, feed_id, 'reblogs') reblog_key = key(type, feed_id, 'reblogs')
# We collect a future for this: we don't block while getting # We collect a future for this: we don't block while getting
# it, but we can iterate over it later. # it, but we can iterate over it later.
reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1) reblogged_id_sets[feed_id] = pipeline.zrange(reblog_key, 0, -1)
redis.del(reblog_key) pipeline.del(reblog_key)
end end
end end
# Remove all of the reblog tracking keys we just removed the # Remove all of the reblog tracking keys we just removed the
# references to. # references to.
redis.pipelined do redis.pipelined do |pipeline|
reblogged_id_sets.each do |feed_id, future| reblogged_id_sets.each do |feed_id, future|
future.value.each do |reblogged_id| future.value.each do |reblogged_id|
reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}") reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}")
redis.del(reblog_set_key) pipeline.del(reblog_set_key)
end end
end end
end end
@ -519,7 +519,7 @@ class FeedManager
# REBLOG_FALLOFF most recent statuses, so we note that this # REBLOG_FALLOFF most recent statuses, so we note that this
# is an "extra" reblog, by storing it in reblog_set_key. # is an "extra" reblog, by storing it in reblog_set_key.
reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}") reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}")
redis.sadd(reblog_set_key, status.id) redis.sadd?(reblog_set_key, status.id)
return false return false
end end
else else
@ -556,7 +556,7 @@ class FeedManager
# 2. Remove reblog from set of this status's reblogs. # 2. Remove reblog from set of this status's reblogs.
reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}") reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}")
redis.srem(reblog_set_key, status.id) redis.srem?(reblog_set_key, status.id)
redis.zrem(reblog_key, status.reblog_of_id) redis.zrem(reblog_key, status.reblog_of_id)
# 3. Re-insert another reblog or original into the feed if one # 3. Re-insert another reblog or original into the feed if one
# remains in the set. We could pick a random element, but this # remains in the set. We could pick a random element, but this

@ -42,6 +42,6 @@ class Vacuum::StatusesVacuum
end end
def remove_from_search_index(status_ids) def remove_from_search_index(status_ids)
with_redis { |redis| redis.sadd('chewy:queue:StatusesIndex', status_ids) } with_redis { |redis| redis.sadd?('chewy:queue:StatusesIndex', status_ids) }
end end
end end

@ -59,7 +59,7 @@ class AccountMigration < ApplicationRecord
def set_target_account def set_target_account
self.target_account = ResolveAccountService.new.call(acct, skip_cache: true) self.target_account = ResolveAccountService.new.call(acct, skip_cache: true)
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error, Addressable::URI::InvalidURIError
# Validation will take care of it # Validation will take care of it
end end

@ -14,7 +14,6 @@ module DomainMaterializable
Instance.refresh Instance.refresh
count_unique_subdomains! count_unique_subdomains!
end end
def count_unique_subdomains! def count_unique_subdomains!

@ -54,7 +54,7 @@ class CustomFilter < ApplicationRecord
end end
def irreversible=(value) def irreversible=(value)
self.action = value ? :hide : :warn self.action = ActiveModel::Type::Boolean.new.cast(value) ? :hide : :warn
end end
def irreversible? def irreversible?

@ -19,9 +19,9 @@ class FollowRecommendationSuppression < ApplicationRecord
private private
def remove_follow_recommendations def remove_follow_recommendations
redis.pipelined do redis.pipelined do |pipeline|
I18n.available_locales.each do |locale| I18n.available_locales.each do |locale|
redis.zrem("follow_recommendations:#{locale}", account_id) pipeline.zrem("follow_recommendations:#{locale}", account_id)
end end
end end
end end

@ -32,7 +32,7 @@ class Form::Redirect
def set_target_account def set_target_account
@target_account = ResolveAccountService.new.call(acct, skip_cache: true) @target_account = ResolveAccountService.new.call(acct, skip_cache: true)
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error, Addressable::URI::InvalidURIError
# Validation will take care of it # Validation will take care of it
end end

@ -60,7 +60,7 @@ class Trends::Base
end end
def record_used_id(id, at_time = Time.now.utc) def record_used_id(id, at_time = Time.now.utc)
redis.sadd(used_key(at_time), id) redis.sadd?(used_key(at_time), id)
redis.expire(used_key(at_time), 1.day.seconds) redis.expire(used_key(at_time), 1.day.seconds)
end end

@ -48,9 +48,9 @@ class BatchedRemoveStatusService < BaseService
# Cannot be batched # Cannot be batched
@status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago) @status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago)
redis.pipelined do redis.pipelined do |pipeline|
statuses.each do |status| statuses.each do |status|
unpush_from_public_timelines(status) unpush_from_public_timelines(pipeline, status)
end end
end end
end end
@ -73,22 +73,22 @@ class BatchedRemoveStatusService < BaseService
end end
end end
def unpush_from_public_timelines(status) def unpush_from_public_timelines(pipeline, status)
return unless status.public_visibility? && status.id > @status_id_cutoff return unless status.public_visibility? && status.id > @status_id_cutoff
payload = Oj.dump(event: :delete, payload: status.id.to_s) payload = Oj.dump(event: :delete, payload: status.id.to_s)
redis.publish('timeline:public', payload) pipeline.publish('timeline:public', payload)
redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
if status.media_attachments.any? if status.media_attachments.any?
redis.publish('timeline:public:media', payload) pipeline.publish('timeline:public:media', payload)
redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
end end
status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag| status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag|
redis.publish("timeline:hashtag:#{hashtag}", payload) pipeline.publish("timeline:hashtag:#{hashtag}", payload)
redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
end end
end end

@ -16,7 +16,7 @@ class Scheduler::IndexingScheduler
type.import!(ids) type.import!(ids)
redis.pipelined do |pipeline| redis.pipelined do |pipeline|
ids.each { |id| pipeline.srem("chewy:queue:#{type.name}", id) } ids.each { |id| pipeline.srem?("chewy:queue:#{type.name}", id) }
end end
end end
end end

@ -71,6 +71,7 @@ module Mastodon
:af, :af,
:ar, :ar,
:ast, :ast,
:be,
:bg, :bg,
:bn, :bn,
:br, :br,

@ -2,6 +2,7 @@ default: &default
adapter: postgresql adapter: postgresql
pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %> pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
timeout: 5000 timeout: 5000
connect_timeout: 15
encoding: unicode encoding: unicode
sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %> sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>

@ -0,0 +1,41 @@
---
cs:
admin:
custom_emojis:
batch_copy_error: 'Při kopírování některých emoji došlo k chybě: %{message}'
batch_error: 'Došlo k chybě: %{message}'
settings:
captcha_enabled:
desc_html: Tato funkce používá externí skripty služby hCaptcha, což může být problém z hlediska bezpečí a ochrany dat. Také to může <strong>některým (hlavně postiženým) lidem registrační proces výrazně zkomplikovat</strong>. Z tohoto důvodu prosím raději zvažte jiné možnosti, jako je schvalování registrací nebo registrace pouze pro zvané.<br>Uživatelům pozvaným skrze omezenou pozvánku se CAPTCHA nezobrazí.
title: Vyžadovat po nových uživatelích opsání textu z obrázku (CAPTCHA)
enable_keybase:
desc_html: Uživatelé budou moci potvrdit svou identitu pomocí Keybase
title: Zapnout potvrzování pomocí Keybase
flavour_and_skin:
title: Rozhraní a styl
other:
preamble: Různá nastavení glitch-soc, která se nevešla do jiných kategorií.
title: Jiné
outgoing_spoilers:
desc_html: Při federování příspěvků se přidá toto varování o obsahu příspěvkům, které žádné nemají. To může být užitečné, pokud je váš server zaměřen na specifický obsah, pro který by jiné servery mohly varování o obsahu vyžadovat. Připojená média budou označena jako citlivá.
title: Varování o obsahu pro odesílané příspěvky
hide_followers_count:
desc_html: Nezobrazovat na uživatelských profilech počet sledujících
title: Schovat počet sledujících
show_reblogs_in_public_timelines:
desc_html: Veřejné boosty veřejných příspěvků se zobrazí na místní a federované časové ose.
title: Zobrazovat ve veřejných časových osách boosty
show_replies_in_public_timelines:
desc_html: Na místní a federované časové ose se kromě odpovědí autora na vlastní příspěvky (vláken) zobrazí i ostatní veřejné odpovědi.
title: Zobrazovat ve veřejných časových osách odpovědi
trending_status_cw:
desc_html: Zobrazovat v rámci trendů (pokud jsou zapnuté) i příspěvky s varováním o obsahu. Změny tohoto nastavení se neprojeví retroaktivně.
title: Povolit v trendech příspěvky s varováním o obsahu
auth:
captcha_confirmation:
hint_html: Už jen poslední krok! Pro potvrzení svého účtu opište prosím text z obrázku (CAPTCHA). Pokud máte dotazy nebo potřebujete s potvrzením pomoct, můžete <a href="/about/more">kontaktovat administrátora</a>.
title: Ověření uživatele
generic:
use_this: Použít
settings:
flavours: Rozhraní

@ -0,0 +1,27 @@
---
cs:
simple_form:
glitch_only: glitch-soc
hints:
defaults:
fields: Na svém profilu můžete mít zobrazeno několik položek (max. %{count}) jako tabulku
setting_default_content_type_html: Předpokládat, že nové příspěvky jsou napsané v HTML, pokud není uvedeno jinak
setting_default_content_type_markdown: Předpokládat, že nové příspěvky používají pro formátování Markdown, pokud není uvedeno jinak
setting_default_content_type_plain: Předpokládat, že nové příspěvky nejsou nijak formátované, pokud není uvedeno jinak (standardní chování Mastodonu)
setting_default_language: Jazyk vašich příspěvků lze detekovat automaticky, ale není to vždycky přesné
setting_hide_followers_count: Počet vašich sledujících se nebude nikomu zobrazovat, ani vám. Některé aplikace mohou zobrazit negativní počet sledujících.
setting_skin: Aplikuje barevný styl na zvolené rozhraní Mastodonu
labels:
defaults:
setting_default_content_type: Výchozí formát příspěvků
setting_default_content_type_html: HTML
setting_default_content_type_markdown: Markdown
setting_default_content_type_plain: Prostý text
setting_favourite_modal: Před oblíbením příspěvku zobrazit potvrzovací dialog (pouze pro rozhraní Glitch)
setting_hide_followers_count: Skrýt počet vašich sledujících
setting_skin: Styl
setting_system_emoji_font: Použít výchozí emoji systému (pouze pro rozhraní Glitch)
notification_emails:
trending_tag: Nový populární hashtag vyžaduje schválení
trending_link: Nový populární odkaz vyžaduje schválení
trending_status: Nový populární příspěvek vyžaduje schválení

@ -79,69 +79,72 @@ class BackfillAdminActionLogs < ActiveRecord::Migration[6.1]
safety_assured do safety_assured do
AdminActionLog.includes(:account).where(target_type: 'Account', human_identifier: nil).find_each do |log| AdminActionLog.includes(:account).where(target_type: 'Account', human_identifier: nil).find_each do |log|
next if log.account.nil? next if log.account.nil?
log.update(human_identifier: log.account.acct) log.update_attribute('human_identifier', log.account.acct)
end end
AdminActionLog.includes(user: :account).where(target_type: 'User', human_identifier: nil).find_each do |log| AdminActionLog.includes(user: :account).where(target_type: 'User', human_identifier: nil).find_each do |log|
next if log.user.nil? next if log.user.nil?
log.update(human_identifier: log.user.account.acct, route_param: log.user.account_id) log.update_attribute('human_identifier', log.user.account.acct)
log.update_attribute('route_param', log.user.account_id)
end end
Admin::ActionLog.where(target_type: 'Report', human_identifier: nil).in_batches.update_all('human_identifier = target_id::text') Admin::ActionLog.where(target_type: 'Report', human_identifier: nil).in_batches.update_all('human_identifier = target_id::text')
AdminActionLog.includes(:domain_block).where(target_type: 'DomainBlock').find_each do |log| AdminActionLog.includes(:domain_block).where(target_type: 'DomainBlock').find_each do |log|
next if log.domain_block.nil? next if log.domain_block.nil?
log.update(human_identifier: log.domain_block.domain) log.update_attribute('human_identifier', log.domain_block.domain)
end end
AdminActionLog.includes(:domain_allow).where(target_type: 'DomainAllow').find_each do |log| AdminActionLog.includes(:domain_allow).where(target_type: 'DomainAllow').find_each do |log|
next if log.domain_allow.nil? next if log.domain_allow.nil?
log.update(human_identifier: log.domain_allow.domain) log.update_attribute('human_identifier', log.domain_allow.domain)
end end
AdminActionLog.includes(:email_domain_block).where(target_type: 'EmailDomainBlock').find_each do |log| AdminActionLog.includes(:email_domain_block).where(target_type: 'EmailDomainBlock').find_each do |log|
next if log.email_domain_block.nil? next if log.email_domain_block.nil?
log.update(human_identifier: log.email_domain_block.domain) log.update_attribute('human_identifier', log.email_domain_block.domain)
end end
AdminActionLog.includes(:unavailable_domain).where(target_type: 'UnavailableDomain').find_each do |log| AdminActionLog.includes(:unavailable_domain).where(target_type: 'UnavailableDomain').find_each do |log|
next if log.unavailable_domain.nil? next if log.unavailable_domain.nil?
log.update(human_identifier: log.unavailable_domain.domain) log.update_attribute('human_identifier', log.unavailable_domain.domain)
end end
AdminActionLog.includes(status: :account).where(target_type: 'Status', human_identifier: nil).find_each do |log| AdminActionLog.includes(status: :account).where(target_type: 'Status', human_identifier: nil).find_each do |log|
next if log.status.nil? next if log.status.nil?
log.update(human_identifier: log.status.account.acct, permalink: log.status.uri) log.update_attribute('human_identifier', log.status.account.acct)
log.update_attribute('permalink', log.status.uri)
end end
AdminActionLog.includes(account_warning: :account).where(target_type: 'AccountWarning', human_identifier: nil).find_each do |log| AdminActionLog.includes(account_warning: :account).where(target_type: 'AccountWarning', human_identifier: nil).find_each do |log|
next if log.account_warning.nil? next if log.account_warning.nil?
log.update(human_identifier: log.account_warning.account.acct) log.update_attribute('human_identifier', log.account_warning.account.acct)
end end
AdminActionLog.includes(:announcement).where(target_type: 'Announcement', human_identifier: nil).find_each do |log| AdminActionLog.includes(:announcement).where(target_type: 'Announcement', human_identifier: nil).find_each do |log|
next if log.announcement.nil? next if log.announcement.nil?
log.update(human_identifier: log.announcement.text) log.update_attribute('human_identifier', log.announcement.text)
end end
AdminActionLog.includes(:ip_block).where(target_type: 'IpBlock', human_identifier: nil).find_each do |log| AdminActionLog.includes(:ip_block).where(target_type: 'IpBlock', human_identifier: nil).find_each do |log|
next if log.ip_block.nil? next if log.ip_block.nil?
log.update(human_identifier: "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}") log.update_attribute('human_identifier', "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}")
end end
AdminActionLog.includes(:custom_emoji).where(target_type: 'CustomEmoji', human_identifier: nil).find_each do |log| AdminActionLog.includes(:custom_emoji).where(target_type: 'CustomEmoji', human_identifier: nil).find_each do |log|
next if log.custom_emoji.nil? next if log.custom_emoji.nil?
log.update(human_identifier: log.custom_emoji.shortcode) log.update_attribute('human_identifier', log.custom_emoji.shortcode)
end end
AdminActionLog.includes(:canonical_email_block).where(target_type: 'CanonicalEmailBlock', human_identifier: nil).find_each do |log| AdminActionLog.includes(:canonical_email_block).where(target_type: 'CanonicalEmailBlock', human_identifier: nil).find_each do |log|
next if log.canonical_email_block.nil? next if log.canonical_email_block.nil?
log.update(human_identifier: log.canonical_email_block.canonical_email_hash) log.update_attribute('human_identifier', log.canonical_email_block.canonical_email_hash)
end end
AdminActionLog.includes(appeal: :account).where(target_type: 'Appeal', human_identifier: nil).find_each do |log| AdminActionLog.includes(appeal: :account).where(target_type: 'Appeal', human_identifier: nil).find_each do |log|
next if log.appeal.nil? next if log.appeal.nil?
log.update(human_identifier: log.appeal.account.acct, route_param: log.appeal.account_warning_id) log.update_attribute('human_identifier', log.appeal.account.acct)
log.update_attribute('route_param', log.appeal.account_warning_id)
end end
end end
end end

@ -0,0 +1,153 @@
# frozen_string_literal: true
class BackfillAdminActionLogsAgain < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
class Account < ApplicationRecord
# Dummy class, to make migration possible across version changes
has_one :user, inverse_of: :account
def local?
domain.nil?
end
def acct
local? ? username : "#{username}@#{domain}"
end
end
class User < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account
end
class Status < ApplicationRecord
include RoutingHelper
# Dummy class, to make migration possible across version changes
belongs_to :account
def local?
attributes['local'] || attributes['uri'].nil?
end
def uri
local? ? activity_account_status_url(account, self) : attributes['uri']
end
end
class DomainBlock < ApplicationRecord; end
class DomainAllow < ApplicationRecord; end
class EmailDomainBlock < ApplicationRecord; end
class UnavailableDomain < ApplicationRecord; end
class AccountWarning < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account
end
class Announcement < ApplicationRecord; end
class IpBlock < ApplicationRecord; end
class CustomEmoji < ApplicationRecord; end
class CanonicalEmailBlock < ApplicationRecord; end
class Appeal < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account
end
class AdminActionLog < ApplicationRecord
# Dummy class, to make migration possible across version changes
# Cannot use usual polymorphic support because of namespacing issues
belongs_to :status, foreign_key: :target_id
belongs_to :account, foreign_key: :target_id
belongs_to :user, foreign_key: :user_id
belongs_to :domain_block, foreign_key: :target_id
belongs_to :domain_allow, foreign_key: :target_id
belongs_to :email_domain_block, foreign_key: :target_id
belongs_to :unavailable_domain, foreign_key: :target_id
belongs_to :account_warning, foreign_key: :target_id
belongs_to :announcement, foreign_key: :target_id
belongs_to :ip_block, foreign_key: :target_id
belongs_to :custom_emoji, foreign_key: :target_id
belongs_to :canonical_email_block, foreign_key: :target_id
belongs_to :appeal, foreign_key: :target_id
end
def up
safety_assured do
AdminActionLog.includes(:account).where(target_type: 'Account', human_identifier: nil).find_each do |log|
next if log.account.nil?
log.update_attribute('human_identifier', log.account.acct)
end
AdminActionLog.includes(user: :account).where(target_type: 'User', human_identifier: nil).find_each do |log|
next if log.user.nil?
log.update_attribute('human_identifier', log.user.account.acct)
log.update_attribute('route_param', log.user.account_id)
end
Admin::ActionLog.where(target_type: 'Report', human_identifier: nil).in_batches.update_all('human_identifier = target_id::text')
AdminActionLog.includes(:domain_block).where(target_type: 'DomainBlock').find_each do |log|
next if log.domain_block.nil?
log.update_attribute('human_identifier', log.domain_block.domain)
end
AdminActionLog.includes(:domain_allow).where(target_type: 'DomainAllow').find_each do |log|
next if log.domain_allow.nil?
log.update_attribute('human_identifier', log.domain_allow.domain)
end
AdminActionLog.includes(:email_domain_block).where(target_type: 'EmailDomainBlock').find_each do |log|
next if log.email_domain_block.nil?
log.update_attribute('human_identifier', log.email_domain_block.domain)
end
AdminActionLog.includes(:unavailable_domain).where(target_type: 'UnavailableDomain').find_each do |log|
next if log.unavailable_domain.nil?
log.update_attribute('human_identifier', log.unavailable_domain.domain)
end
AdminActionLog.includes(status: :account).where(target_type: 'Status', human_identifier: nil).find_each do |log|
next if log.status.nil?
log.update_attribute('human_identifier', log.status.account.acct)
log.update_attribute('permalink', log.status.uri)
end
AdminActionLog.includes(account_warning: :account).where(target_type: 'AccountWarning', human_identifier: nil).find_each do |log|
next if log.account_warning.nil?
log.update_attribute('human_identifier', log.account_warning.account.acct)
end
AdminActionLog.includes(:announcement).where(target_type: 'Announcement', human_identifier: nil).find_each do |log|
next if log.announcement.nil?
log.update_attribute('human_identifier', log.announcement.text)
end
AdminActionLog.includes(:ip_block).where(target_type: 'IpBlock', human_identifier: nil).find_each do |log|
next if log.ip_block.nil?
log.update_attribute('human_identifier', "#{log.ip_block.ip}/#{log.ip_block.ip.prefix}")
end
AdminActionLog.includes(:custom_emoji).where(target_type: 'CustomEmoji', human_identifier: nil).find_each do |log|
next if log.custom_emoji.nil?
log.update_attribute('human_identifier', log.custom_emoji.shortcode)
end
AdminActionLog.includes(:canonical_email_block).where(target_type: 'CanonicalEmailBlock', human_identifier: nil).find_each do |log|
next if log.canonical_email_block.nil?
log.update_attribute('human_identifier', log.canonical_email_block.canonical_email_hash)
end
AdminActionLog.includes(appeal: :account).where(target_type: 'Appeal', human_identifier: nil).find_each do |log|
next if log.appeal.nil?
log.update_attribute('human_identifier', log.appeal.account.acct)
log.update_attribute('route_param', log.appeal.account_warning_id)
end
end
end
def down; end
end

@ -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: 2022_11_24_114030) do ActiveRecord::Schema.define(version: 2022_12_06_114142) 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"
@ -781,16 +781,6 @@ ActiveRecord::Schema.define(version: 2022_11_24_114030) do
t.index ["status_id", "preview_card_id"], name: "index_preview_cards_statuses_on_status_id_and_preview_card_id" t.index ["status_id", "preview_card_id"], name: "index_preview_cards_statuses_on_status_id_and_preview_card_id"
end end
create_table "reactions", force: :cascade do |t|
t.string "emoji"
t.bigint "status_id", null: false
t.bigint "account_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_reactions_on_account_id"
t.index ["status_id"], name: "index_reactions_on_status_id"
end
create_table "relays", force: :cascade do |t| create_table "relays", force: :cascade do |t|
t.string "inbox_url", default: "", null: false t.string "inbox_url", default: "", null: false
t.string "follow_activity_id" t.string "follow_activity_id"
@ -1221,8 +1211,6 @@ ActiveRecord::Schema.define(version: 2022_11_24_114030) do
add_foreign_key "polls", "accounts", on_delete: :cascade add_foreign_key "polls", "accounts", on_delete: :cascade
add_foreign_key "polls", "statuses", on_delete: :cascade add_foreign_key "polls", "statuses", on_delete: :cascade
add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade
add_foreign_key "reactions", "accounts"
add_foreign_key "reactions", "statuses"
add_foreign_key "report_notes", "accounts", on_delete: :cascade add_foreign_key "report_notes", "accounts", on_delete: :cascade
add_foreign_key "report_notes", "reports", on_delete: :cascade add_foreign_key "report_notes", "reports", on_delete: :cascade
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify

@ -17,7 +17,7 @@ module Chewy
RedisConfiguration.with do |redis| RedisConfiguration.with do |redis|
redis.pipelined do |pipeline| redis.pipelined do |pipeline|
@stash.each do |type, ids| @stash.each do |type, ids|
pipeline.sadd("chewy:queue:#{type.name}", ids) pipeline.sadd?("chewy:queue:#{type.name}", ids)
end end
end end
end end

@ -54,8 +54,8 @@ module Mastodon
def clear def clear
keys = redis.keys('feed:*') keys = redis.keys('feed:*')
redis.pipelined do redis.pipelined do |pipeline|
keys.each { |key| redis.del(key) } keys.each { |key| pipeline.del(key) }
end end
say('OK', :green) say('OK', :green)

@ -25,7 +25,7 @@ module Mastodon
end end
def suffix_version def suffix_version
'+1.0.10' '+1.0.11'
end end
def post_suffix def post_suffix

@ -43,6 +43,16 @@ namespace :tests do
puts 'CustomFilterKeyword records not created as expected' puts 'CustomFilterKeyword records not created as expected'
exit(1) exit(1)
end end
unless Admin::ActionLog.find_by(target_type: 'DomainBlock', target_id: 1).human_identifier == 'example.org'
puts 'Admin::ActionLog domain block records not updated as expected'
exit(1)
end
unless Admin::ActionLog.find_by(target_type: 'EmailDomainBlock', target_id: 1).human_identifier == 'example.org'
puts 'Admin::ActionLog email domain block records not updated as expected'
exit(1)
end
end end
desc 'Populate the database with test data for 2.4.3' desc 'Populate the database with test data for 2.4.3'
@ -84,8 +94,8 @@ namespace :tests do
VALUES VALUES
(1, 'destroy', 'Account', 1, now(), now()), (1, 'destroy', 'Account', 1, now(), now()),
(1, 'destroy', 'User', 1, now(), now()), (1, 'destroy', 'User', 1, now(), now()),
(1, 'destroy', 'DomainBlock', 1312, now(), now()), (1, 'destroy', 'DomainBlock', 1, now(), now()),
(1, 'destroy', 'EmailDomainBlock', 1312, now(), now()), (1, 'destroy', 'EmailDomainBlock', 1, now(), now()),
(1, 'destroy', 'Status', 1, now(), now()), (1, 'destroy', 'Status', 1, now(), now()),
(1, 'destroy', 'CustomEmoji', 3, now(), now()); (1, 'destroy', 'CustomEmoji', 3, now(), now());
SQL SQL

@ -147,6 +147,87 @@ RSpec.describe Admin::AccountsController, type: :controller do
end end
end end
describe 'POST #approve' do
subject { post :approve, params: { id: account.id } }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { user.account }
let(:user) { Fabricate(:user) }
before do
account.user.update(approved: false)
end
context 'when user is admin' do
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in approving account' do
is_expected.to redirect_to admin_accounts_path(status: 'pending')
expect(user.reload).to be_approved
end
it 'logs action' do
is_expected.to have_http_status :found
log_item = Admin::ActionLog.last
expect(log_item).to_not be_nil
expect(log_item.action).to eq :approve
expect(log_item.account_id).to eq current_user.account_id
expect(log_item.target_id).to eq account.user.id
end
end
context 'when user is not admin' do
let(:role) { UserRole.everyone }
it 'fails to approve account' do
is_expected.to have_http_status :forbidden
expect(user.reload).not_to be_approved
end
end
end
describe 'POST #reject' do
subject { post :reject, params: { id: account.id } }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { user.account }
let(:user) { Fabricate(:user) }
before do
account.user.update(approved: false)
end
context 'when user is admin' do
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in rejecting account' do
is_expected.to redirect_to admin_accounts_path(status: 'pending')
end
it 'logs action' do
is_expected.to have_http_status :found
log_item = Admin::ActionLog.last
expect(log_item).to_not be_nil
expect(log_item.action).to eq :reject
expect(log_item.account_id).to eq current_user.account_id
expect(log_item.target_id).to eq account.user.id
end
end
context 'when user is not admin' do
let(:role) { UserRole.everyone }
it 'fails to reject account' do
is_expected.to have_http_status :forbidden
expect(user.reload).not_to be_approved
end
end
end
describe 'POST #redownload' do describe 'POST #redownload' do
subject { post :redownload, params: { id: account.id } } subject { post :redownload, params: { id: account.id } }

@ -100,6 +100,15 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
it 'approves user' do it 'approves user' do
expect(account.reload.user_approved?).to be true expect(account.reload.user_approved?).to be true
end end
it 'logs action' do
log_item = Admin::ActionLog.last
expect(log_item).to_not be_nil
expect(log_item.action).to eq :approve
expect(log_item.account_id).to eq user.account_id
expect(log_item.target_id).to eq account.user.id
end
end end
describe 'POST #reject' do describe 'POST #reject' do
@ -118,6 +127,15 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
it 'removes user' do it 'removes user' do
expect(User.where(id: account.user.id).count).to eq 0 expect(User.where(id: account.user.id).count).to eq 0
end end
it 'logs action' do
log_item = Admin::ActionLog.last
expect(log_item).to_not be_nil
expect(log_item.action).to eq :reject
expect(log_item.account_id).to eq user.account_id
expect(log_item.target_id).to eq account.user.id
end
end end
describe 'POST #enable' do describe 'POST #enable' do

@ -22,9 +22,11 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
describe 'POST #create' do describe 'POST #create' do
let(:scopes) { 'write:filters' } let(:scopes) { 'write:filters' }
let(:irreversible) { true }
let(:whole_word) { false }
before do before do
post :create, params: { phrase: 'magic', context: %w(home), irreversible: true } post :create, params: { phrase: 'magic', context: %w(home), irreversible: irreversible, whole_word: whole_word }
end end
it 'returns http success' do it 'returns http success' do
@ -34,11 +36,29 @@ RSpec.describe Api::V1::FiltersController, type: :controller do
it 'creates a filter' do it 'creates a filter' do
filter = user.account.custom_filters.first filter = user.account.custom_filters.first
expect(filter).to_not be_nil expect(filter).to_not be_nil
expect(filter.keywords.pluck(:keyword)).to eq ['magic'] expect(filter.keywords.pluck(:keyword, :whole_word)).to eq [['magic', whole_word]]
expect(filter.context).to eq %w(home) expect(filter.context).to eq %w(home)
expect(filter.irreversible?).to be true expect(filter.irreversible?).to be irreversible
expect(filter.expires_at).to be_nil expect(filter.expires_at).to be_nil
end end
context 'with different parameters' do
let(:irreversible) { false }
let(:whole_word) { true }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'creates a filter' do
filter = user.account.custom_filters.first
expect(filter).to_not be_nil
expect(filter.keywords.pluck(:keyword, :whole_word)).to eq [['magic', whole_word]]
expect(filter.context).to eq %w(home)
expect(filter.irreversible?).to be irreversible
expect(filter.expires_at).to be_nil
end
end
end end
describe 'GET #show' do describe 'GET #show' do

@ -22,7 +22,7 @@ describe DeliveryFailureTracker do
describe '#track_failure!' do describe '#track_failure!' do
it 'marks URL as unavailable after 7 days of being called' do it 'marks URL as unavailable after 7 days of being called' do
6.times { |i| redis.sadd('exhausted_deliveries:example.com', i) } 6.times { |i| redis.sadd?('exhausted_deliveries:example.com', i) }
subject.track_failure! subject.track_failure!
expect(subject.days).to eq 7 expect(subject.days).to eq 7

@ -11,7 +11,7 @@ RSpec.describe Vacuum::FeedsVacuum do
redis.zadd(feed_key_for(inactive_user), 1, 1) redis.zadd(feed_key_for(inactive_user), 1, 1)
redis.zadd(feed_key_for(active_user), 1, 1) redis.zadd(feed_key_for(active_user), 1, 1)
redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2) redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3) redis.sadd?(feed_key_for(inactive_user, 'reblogs:2'), 3)
subject.perform subject.perform
end end

@ -1,5 +1,48 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe AccountMigration, type: :model do RSpec.describe AccountMigration, type: :model do
describe 'validations' do
let(:source_account) { Fabricate(:account) }
let(:target_acct) { target_account.acct }
let(:subject) { AccountMigration.new(account: source_account, acct: target_acct) }
context 'with valid properties' do
let(:target_account) { Fabricate(:account, username: 'target', domain: 'remote.org') }
before do
target_account.aliases.create!(acct: source_account.acct)
service_double = double
allow(ResolveAccountService).to receive(:new).and_return(service_double)
allow(service_double).to receive(:call).with(target_acct, anything).and_return(target_account)
end
it 'passes validations' do
expect(subject).to be_valid
end
end
context 'with unresolveable account' do
let(:target_acct) { 'target@remote' }
before do
service_double = double
allow(ResolveAccountService).to receive(:new).and_return(service_double)
allow(service_double).to receive(:call).with(target_acct, anything).and_return(nil)
end
it 'has errors on acct field' do
expect(subject).to model_have_error_on_field(:acct)
end
end
context 'with a space in the domain part' do
let(:target_acct) { 'target@remote. org' }
it 'has errors on acct field' do
expect(subject).to model_have_error_on_field(:acct)
end
end
end
end end

Loading…
Cancel
Save