Merge branch 'main' of https://github.com/glitch-soc/mastodon
commit
adaca2c33d
|
@ -129,6 +129,13 @@ jobs:
|
|||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby3.0:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:3.0-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
|
@ -187,34 +194,28 @@ jobs:
|
|||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby3.0:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:3.0-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-webui:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:12-buster
|
||||
- image: circleci/node:14-buster
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run:
|
||||
name: Run jest
|
||||
command: yarn test:jest
|
||||
|
||||
check-i18n:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Check locale file normalization
|
||||
command: bundle exec i18n-tasks check-normalized
|
||||
- run:
|
||||
name: Check for unused strings
|
||||
command: bundle exec i18n-tasks unused -l en
|
||||
- run:
|
||||
name: Check for wrong string interpolations
|
||||
command: bundle exec i18n-tasks check-consistent-interpolations
|
||||
- run:
|
||||
name: Check that all required locale files exist
|
||||
command: bundle exec rake repo:check_locales_files
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
|
@ -227,6 +228,10 @@ workflows:
|
|||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- install-ruby3.0:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- build:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
|
@ -241,9 +246,10 @@ workflows:
|
|||
requires:
|
||||
- install-ruby2.6
|
||||
- build
|
||||
- test-ruby3.0:
|
||||
requires:
|
||||
- install-ruby3.0
|
||||
- build
|
||||
- test-webui:
|
||||
requires:
|
||||
- install
|
||||
- check-i18n:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
|
|
|
@ -30,7 +30,7 @@ plugins:
|
|||
channel: eslint-7
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: rubocop-1-70
|
||||
channel: rubocop-1-9-1
|
||||
sass-lint:
|
||||
enabled: true
|
||||
exclude_patterns:
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
version = 1
|
||||
|
||||
test_patterns = ["app/javascript/mastodon/**/__tests__/**"]
|
||||
|
||||
exclude_patterns = [
|
||||
"db/migrate/**",
|
||||
"db/post_migrate/**"
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = "ruby"
|
||||
enabled = true
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
environment = [
|
||||
"browser",
|
||||
"jest",
|
||||
"nodejs"
|
||||
]
|
|
@ -1,6 +1,10 @@
|
|||
.bundle
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.github
|
||||
public/system
|
||||
public/assets
|
||||
public/packs
|
||||
|
@ -13,3 +17,4 @@ vendor/bundle
|
|||
postgres
|
||||
redis
|
||||
elasticsearch
|
||||
chart
|
||||
|
|
|
@ -228,6 +228,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
|||
# CAS_LOCATION_KEY='location'
|
||||
# CAS_IMAGE_KEY='image'
|
||||
# CAS_PHONE_KEY='phone'
|
||||
# CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
|
||||
|
||||
# Optional SAML authentication (cf. omniauth-saml)
|
||||
# SAML_ENABLED=true
|
||||
|
|
|
@ -254,6 +254,12 @@ MAX_PROFILE_FIELDS=4
|
|||
# Maximum allowed display name characters
|
||||
MAX_DISPLAY_NAME_CHARS=30
|
||||
|
||||
# Maximum allowed poll options
|
||||
MAX_POLL_OPTIONS=5
|
||||
|
||||
# Maximum allowed poll option characters
|
||||
MAX_POLL_OPTION_CHARS=100
|
||||
|
||||
# Maximum image and video/audio upload sizes
|
||||
# Units are in bytes
|
||||
# 1048576 bytes equals 1 megabyte
|
||||
|
@ -263,3 +269,10 @@ MAX_DISPLAY_NAME_CHARS=30
|
|||
# Maximum search results to display
|
||||
# Only relevant when elasticsearch is installed
|
||||
# MAX_SEARCH_RESULTS=20
|
||||
|
||||
# Maximum custom emoji file sizes
|
||||
# If undefined or smaller than MAX_EMOJI_SIZE, the value
|
||||
# of MAX_EMOJI_SIZE will be used for MAX_REMOTE_EMOJI_SIZE
|
||||
# Units are in bytes
|
||||
MAX_EMOJI_SIZE=51200
|
||||
MAX_REMOTE_EMOJI_SIZE=204800
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# CODEOWNERS for tootsuite/mastodon
|
||||
# CODEOWNERS for mastodon/mastodon
|
||||
|
||||
# Translators
|
||||
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
name: Bug Report
|
||||
description: If something isn't working as expected
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||
|
||||
Please use a concise and distinct title for the issue.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: What should have happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Actual behaviour
|
||||
description: What happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the problem
|
||||
description: What were you trying to do?
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Specifications
|
||||
description: |
|
||||
What version or commit hash of Mastodon did you find this bug in?
|
||||
|
||||
If a front-end issue, what browser and operating systems were you using?
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,21 @@
|
|||
name: Feature Request
|
||||
description: I have a suggestion
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please use a concise and distinct title for the issue.
|
||||
|
||||
Consider: Could it be implemented as a 3rd party app using the REST API instead?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Pitch
|
||||
description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why do you think this feature is needed? Who would benefit from it?
|
||||
validations:
|
||||
required: true
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: Support
|
||||
about: Ask for help with your deployment
|
||||
|
||||
title: DO NOT CREATE THIS ISSUE
|
||||
---
|
||||
|
||||
We primarily use GitHub as a bug and feature tracker. For usage questions, troubleshooting of deployments and other individual technical assistance, please use one of the resources below:
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: If something isn't working as expected
|
||||
labels: bug
|
||||
---
|
||||
|
||||
[Issue text goes here].
|
||||
|
||||
* * * *
|
||||
|
||||
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
|
||||
- [ ] This bugs also occur on vanilla Mastodon
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: I have a suggestion
|
||||
---
|
||||
|
||||
<!-- Please use a concise and distinct title for the issue -->
|
||||
|
||||
<!-- Consider: Could it be implemented as a 3rd party app using the REST API instead? -->
|
||||
|
||||
### Pitch
|
||||
|
||||
<!-- Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before -->
|
||||
|
||||
### Motivation
|
||||
|
||||
<!-- Why do you think this feature is needed? Who would benefit from it? -->
|
|
@ -0,0 +1,34 @@
|
|||
name: Check i18n
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
|
||||
jobs:
|
||||
check-i18n:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true
|
||||
- name: Check locale file normalization
|
||||
run: bundle exec i18n-tasks check-normalized
|
||||
- name: Check for unused strings
|
||||
run: bundle exec i18n-tasks unused -l en
|
||||
- name: Check for wrong string interpolations
|
||||
run: bundle exec i18n-tasks check-consistent-interpolations
|
||||
- name: Check that all required locale files exist
|
||||
run: bundle exec rake repo:check_locales_files
|
|
@ -43,10 +43,8 @@
|
|||
/redis
|
||||
/elasticsearch
|
||||
|
||||
# ignore Helm lockfile, dependency charts, and local values file
|
||||
/chart/Chart.lock
|
||||
# ignore Helm dependency charts
|
||||
/chart/charts/*.tgz
|
||||
/chart/values.yaml
|
||||
|
||||
# Ignore Apple files
|
||||
.DS_Store
|
||||
|
|
|
@ -2,7 +2,8 @@ require:
|
|||
- rubocop-rails
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
TargetRubyVersion: 2.5
|
||||
NewCops: disable
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'db/**/*'
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.7.2
|
||||
2.7.4
|
||||
|
|
517
AUTHORS.md
517
AUTHORS.md
File diff suppressed because it is too large
Load Diff
2
Aptfile
2
Aptfile
|
@ -22,7 +22,7 @@ libpixman-1-0
|
|||
librsvg2-2
|
||||
libthai-data
|
||||
libthai0
|
||||
libvpx5
|
||||
libvpx[5-9]
|
||||
libxcb-render0
|
||||
libxcb-shm0
|
||||
libxrender1
|
||||
|
|
2580
CHANGELOG.md
2580
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -48,7 +48,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
|
|||
|
||||
## Bug reports
|
||||
|
||||
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
|
||||
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
|
||||
|
||||
## Translations
|
||||
|
||||
|
@ -58,9 +58,17 @@ You can submit translations via [Crowdin](https://crowdin.com/project/mastodon).
|
|||
|
||||
## Pull requests
|
||||
|
||||
Please use clean, concise titles for your pull requests. We use commit squashing, so the final commit in the master branch will carry the title of the pull request.
|
||||
**Please use clean, concise titles for your pull requests.** Unless the pull request is about refactoring code, updating dependencies or other internal tasks, assume that the person reading the pull request title is not a programmer or Mastodon developer, but instead a Mastodon user or server administrator, and **try to describe your change or fix from their perspective**. We use commit squashing, so the final commit in the main branch will carry the title of the pull request, and commits from the main branch are fed into the changelog. The changelog is separated into [keepachangelog.com categories](https://keepachangelog.com/en/1.0.0/), and while that spec does not prescribe how the entries ought to be named, for easier sorting, start your pull request titles using one of the verbs "Add", "Change", "Deprecate", "Remove", or "Fix" (present tense).
|
||||
|
||||
The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged. Splitting tasks into multiple smaller pull requests is often preferable.
|
||||
Example:
|
||||
|
||||
|Not ideal|Better|
|
||||
|---|----|
|
||||
|Fixed NoMethodError in RemovalWorker|Fix nil error when removing statuses caused by race condition|
|
||||
|
||||
It is not always possible to phrase every change in such a manner, but it is desired.
|
||||
|
||||
**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable.
|
||||
|
||||
**Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind:
|
||||
|
||||
|
@ -70,6 +78,6 @@ The smaller the set of changes in the pull request is, the quicker it can be rev
|
|||
|
||||
## Documentation
|
||||
|
||||
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/docs](https://source.joinmastodon.org/mastodon/docs).
|
||||
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).
|
||||
|
||||
</blockquote>
|
||||
|
|
79
Dockerfile
79
Dockerfile
|
@ -1,10 +1,10 @@
|
|||
FROM ubuntu:20.04 as build-dep
|
||||
|
||||
# Use bash for the shell
|
||||
SHELL ["bash", "-c"]
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Install Node v12 (LTS)
|
||||
ENV NODE_VER="12.20.0"
|
||||
# Install Node v14 (LTS)
|
||||
ENV NODE_VER="14.17.6"
|
||||
RUN ARCH= && \
|
||||
dpkgArch="$(dpkg --print-architecture)" && \
|
||||
case "${dpkgArch##*-}" in \
|
||||
|
@ -17,35 +17,19 @@ RUN ARCH= && \
|
|||
*) echo "unsupported architecture"; exit 1 ;; \
|
||||
esac && \
|
||||
echo "Etc/UTC" > /etc/localtime && \
|
||||
apt update && \
|
||||
apt -y install wget python && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates wget python && \
|
||||
cd ~ && \
|
||||
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||
|
||||
# Install jemalloc
|
||||
ENV JE_VER="5.2.1"
|
||||
RUN apt update && \
|
||||
apt -y install make autoconf gcc g++ && \
|
||||
cd ~ && \
|
||||
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
|
||||
tar xf $JE_VER.tar.gz && \
|
||||
cd jemalloc-$JE_VER && \
|
||||
./autogen.sh && \
|
||||
./configure --prefix=/opt/jemalloc && \
|
||||
make -j$(nproc) > /dev/null && \
|
||||
make install_bin install_include install_lib && \
|
||||
cd .. && rm -rf jemalloc-$JE_VER $JE_VER.tar.gz
|
||||
|
||||
# Install Ruby
|
||||
ENV RUBY_VER="2.7.2"
|
||||
ENV CPPFLAGS="-I/opt/jemalloc/include"
|
||||
ENV LDFLAGS="-L/opt/jemalloc/lib/"
|
||||
RUN apt update && \
|
||||
apt -y install build-essential \
|
||||
bison libyaml-dev libgdbm-dev libreadline-dev \
|
||||
ENV RUBY_VER="2.7.4"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
|
||||
cd ~ && \
|
||||
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
|
||||
|
@ -55,25 +39,24 @@ RUN apt update && \
|
|||
--with-jemalloc \
|
||||
--with-shared \
|
||||
--disable-install-doc && \
|
||||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
||||
make -j$(nproc) > /dev/null && \
|
||||
make -j"$(nproc)" > /dev/null && \
|
||||
make install && \
|
||||
cd .. && rm -rf ruby-$RUBY_VER.tar.gz ruby-$RUBY_VER
|
||||
rm -rf ../ruby-$RUBY_VER.tar.gz ../ruby-$RUBY_VER
|
||||
|
||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
||||
|
||||
RUN npm install -g yarn && \
|
||||
gem install bundler && \
|
||||
apt update && \
|
||||
apt -y install git libicu-dev libidn11-dev \
|
||||
libpq-dev libprotobuf-dev protobuf-compiler
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
||||
libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info
|
||||
|
||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||
|
||||
RUN cd /opt/mastodon && \
|
||||
bundle config set deployment 'true' && \
|
||||
bundle config set without 'development test' && \
|
||||
bundle install -j$(nproc) && \
|
||||
bundle install -j"$(nproc)" && \
|
||||
yarn install --pure-lockfile
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
@ -81,7 +64,6 @@ FROM ubuntu:20.04
|
|||
# Copy over all the langs needed for runtime
|
||||
COPY --from=build-dep /opt/node /opt/node
|
||||
COPY --from=build-dep /opt/ruby /opt/ruby
|
||||
COPY --from=build-dep /opt/jemalloc /opt/jemalloc
|
||||
|
||||
# Add more PATHs to the PATH
|
||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
|
||||
|
@ -89,35 +71,26 @@ ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
|
|||
# Create the mastodon user
|
||||
ARG UID=991
|
||||
ARG GID=991
|
||||
RUN apt update && \
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN apt-get update && \
|
||||
echo "Etc/UTC" > /etc/localtime && \
|
||||
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
|
||||
apt install -y whois wget && \
|
||||
apt-get install -y --no-install-recommends whois wget && \
|
||||
addgroup --gid $GID mastodon && \
|
||||
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
|
||||
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
|
||||
echo "mastodon:$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256)" | chpasswd && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install mastodon runtime deps
|
||||
RUN apt -y --no-install-recommends install \
|
||||
libssl1.1 libpq5 imagemagick ffmpeg \
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
||||
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
||||
file ca-certificates tzdata libreadline8 && \
|
||||
apt -y install gcc && \
|
||||
file ca-certificates tzdata libreadline8 gcc tini && \
|
||||
ln -s /opt/mastodon /mastodon && \
|
||||
gem install bundler && \
|
||||
rm -rf /var/cache && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add tini
|
||||
ENV TINI_VERSION="0.19.0"
|
||||
RUN dpkgArch="$(dpkg --print-architecture)" && \
|
||||
ARCH=$dpkgArch && \
|
||||
wget https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH \
|
||||
https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH.sha256sum && \
|
||||
cat tini-$ARCH.sha256sum | sha256sum -c - && \
|
||||
mv tini-$ARCH /tini && rm tini-$ARCH.sha256sum && \
|
||||
chmod +x /tini
|
||||
|
||||
# Copy over mastodon source, and dependencies from building, and set permissions
|
||||
COPY --chown=mastodon:mastodon . /opt/mastodon
|
||||
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
|
||||
|
@ -140,5 +113,5 @@ RUN cd ~ && \
|
|||
|
||||
# Set the work dir and the container entry point
|
||||
WORKDIR /opt/mastodon
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
EXPOSE 3000 4000
|
||||
|
|
100
Gemfile
100
Gemfile
|
@ -1,40 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '>= 2.5.0', '< 3.0.0'
|
||||
ruby '>= 2.5.0', '< 3.1.0'
|
||||
|
||||
gem 'pkg-config', '~> 1.4'
|
||||
|
||||
gem 'puma', '~> 5.1'
|
||||
gem 'rails', '~> 5.2.4.4'
|
||||
gem 'puma', '~> 5.4'
|
||||
gem 'rails', '~> 6.1.4'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 1.0'
|
||||
gem 'thor', '~> 1.1'
|
||||
gem 'rack', '~> 2.2.3'
|
||||
|
||||
gem 'hamlit-rails', '~> 0.2'
|
||||
gem 'pg', '~> 1.2'
|
||||
gem 'makara', '~> 0.4'
|
||||
gem 'pghero', '~> 2.7'
|
||||
gem 'makara', '~> 0.5'
|
||||
gem 'pghero', '~> 2.8'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.87', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.103', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
||||
gem 'streamio-ffmpeg', '~> 3.0'
|
||||
gem 'kt-paperclip', '~> 7.0'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.7'
|
||||
gem 'bootsnap', '~> 1.5', require: false
|
||||
gem 'addressable', '~> 2.8'
|
||||
gem 'bootsnap', '~> 1.9.1', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.1'
|
||||
gem 'cld3', '~> 3.4.1'
|
||||
gem 'devise', '~> 4.7'
|
||||
gem 'devise-two-factor', '~> 3.1'
|
||||
gem 'chewy', '~> 5.2'
|
||||
gem 'cld3', '~> 3.4.2'
|
||||
gem 'devise', '~> 4.8'
|
||||
gem 'devise-two-factor', '~> 4.0'
|
||||
|
||||
group :pam_authentication, optional: true do
|
||||
gem 'devise_pam_authenticatable2', '~> 9.2'
|
||||
|
@ -48,70 +46,68 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
|||
|
||||
gem 'color_diff', '~> 0.1'
|
||||
gem 'discard', '~> 1.2'
|
||||
gem 'doorkeeper', '~> 5.4'
|
||||
gem 'doorkeeper', '~> 5.5'
|
||||
gem 'ed25519', '~> 1.2'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'hiredis', '~> 0.6'
|
||||
gem 'redis-namespace', '~> 1.8'
|
||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 4.4'
|
||||
gem 'http', '~> 5.0'
|
||||
gem 'http_accept_language', '~> 2.1'
|
||||
gem 'httplog', '~> 1.4.3'
|
||||
gem 'httplog', '~> 1.5.0'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
|
||||
gem 'nokogiri', '~> 1.11'
|
||||
gem 'nokogiri', '~> 1.12'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.10'
|
||||
gem 'oj', '~> 3.13'
|
||||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
gem 'parallel', '~> 1.20'
|
||||
gem 'parallel', '~> 1.21'
|
||||
gem 'posix-spawn'
|
||||
gem 'pundit', '~> 2.1'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.3'
|
||||
gem 'rack-attack', '~> 6.5'
|
||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-i18n', '~> 6.0'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'redis', '~> 4.4', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'rqrcode', '~> 1.2'
|
||||
gem 'rqrcode', '~> 2.1'
|
||||
gem 'ruby-progressbar', '~> 1.11'
|
||||
gem 'sanitize', '~> 5.2'
|
||||
gem 'sanitize', '~> 6.0'
|
||||
gem 'scenic', '~> 1.5'
|
||||
gem 'sidekiq', '~> 6.1'
|
||||
gem 'sidekiq-scheduler', '~> 3.0'
|
||||
gem 'sidekiq-unique-jobs', '~> 6.0'
|
||||
gem 'sidekiq', '~> 6.2'
|
||||
gem 'sidekiq-scheduler', '~> 3.1'
|
||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||
gem 'sidekiq-bulk', '~>0.2.0'
|
||||
gem 'simple-navigation', '~> 4.1'
|
||||
gem 'simple_form', '~> 5.0'
|
||||
gem 'simple-navigation', '~> 4.3'
|
||||
gem 'simple_form', '~> 5.1'
|
||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.2.1'
|
||||
gem 'strong_migrations', '~> 0.7'
|
||||
gem 'tty-prompt', '~> 0.23', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2020'
|
||||
gem 'webpacker', '~> 5.2'
|
||||
gem 'webpush'
|
||||
gem 'twitter-text', '~> 3.1.0'
|
||||
gem 'tzinfo-data', '~> 1.2021'
|
||||
gem 'webpacker', '~> 5.4'
|
||||
gem 'webpush', '~> 0.3'
|
||||
gem 'webauthn', '~> 3.0.0.alpha1'
|
||||
|
||||
gem 'json-ld'
|
||||
gem 'json-ld-preloaded', '~> 3.1'
|
||||
gem 'rdf-normalize', '~> 0.4'
|
||||
|
||||
gem 'redcarpet', '~> 3.4'
|
||||
gem 'redcarpet', '~> 3.5'
|
||||
|
||||
group :development, :test do
|
||||
gem 'fabrication', '~> 2.21'
|
||||
gem 'fabrication', '~> 2.22'
|
||||
gem 'fuubar', '~> 2.5'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.9'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 4.0'
|
||||
gem 'rspec-rails', '~> 5.0'
|
||||
end
|
||||
|
||||
group :production, :test do
|
||||
|
@ -119,15 +115,15 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.34'
|
||||
gem 'capybara', '~> 3.35'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.15'
|
||||
gem 'faker', '~> 2.19'
|
||||
gem 'microformats', '~> 4.2'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.1'
|
||||
gem 'simplecov', '~> 0.21', require: false
|
||||
gem 'webmock', '~> 3.11'
|
||||
gem 'parallel_tests', '~> 3.4'
|
||||
gem 'webmock', '~> 3.14'
|
||||
gem 'parallel_tests', '~> 3.7'
|
||||
gem 'rspec_junit_formatter', '~> 0.4'
|
||||
end
|
||||
|
||||
|
@ -135,17 +131,17 @@ group :development do
|
|||
gem 'active_record_query_trace', '~> 1.8'
|
||||
gem 'annotate', '~> 3.1'
|
||||
gem 'better_errors', '~> 2.9'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'binding_of_caller', '~> 1.0'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 1.7', require: false
|
||||
gem 'rubocop-rails', '~> 2.9', require: false
|
||||
gem 'brakeman', '~> 4.10', require: false
|
||||
gem 'bundler-audit', '~> 0.7', require: false
|
||||
gem 'rubocop', '~> 1.21', require: false
|
||||
gem 'rubocop-rails', '~> 2.12', require: false
|
||||
gem 'brakeman', '~> 5.1', require: false
|
||||
gem 'bundler-audit', '~> 0.9', require: false
|
||||
|
||||
gem 'capistrano', '~> 3.14'
|
||||
gem 'capistrano', '~> 3.16'
|
||||
gem 'capistrano-rails', '~> 1.6'
|
||||
gem 'capistrano-rbenv', '~> 2.2'
|
||||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
@ -155,11 +151,9 @@ end
|
|||
|
||||
group :production do
|
||||
gem 'lograge', '~> 0.11'
|
||||
gem 'redis-rails', '~> 5.0'
|
||||
end
|
||||
|
||||
gem 'concurrent-ruby', require: false
|
||||
gem 'connection_pool', require: false
|
||||
|
||||
gem 'xorcist', '~> 1.1'
|
||||
gem 'pluck_each', '~> 0.1.3'
|
||||
|
|
590
Gemfile.lock
590
Gemfile.lock
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
web: env PORT=3000 bundle exec puma -C config/puma.rb
|
||||
sidekiq: env PORT=3000 bundle exec sidekiq
|
||||
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
||||
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
||||
stream: env PORT=4000 yarn run start
|
||||
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
The version of Mastodon running on the [Jubilife Global Terminal](https://jubi.life/about).
|
||||
|
||||
|
||||
It's mostly just the glitch-soc fork with some slight alterations
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.1.x | :white_check_mark: |
|
||||
| < 3.1 | :x: |
|
||||
| 3.4.x | :white_check_mark: |
|
||||
| 3.3.x | :white_check_mark: |
|
||||
| < 3.3 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ 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'
|
||||
|
||||
# Add repo for NodeJS
|
||||
curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -
|
||||
|
||||
# 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"]}
|
||||
|
@ -72,10 +72,12 @@ bundle install
|
|||
yarn install
|
||||
|
||||
# Build Mastodon
|
||||
export RAILS_ENV=development
|
||||
export $(cat ".env.vagrant" | xargs)
|
||||
bundle exec rails db:setup
|
||||
|
||||
# Configure automatic loading of environment variable
|
||||
echo 'export RAILS_ENV=development' >> ~/.bash_profile
|
||||
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
|
||||
|
||||
SCRIPT
|
||||
|
|
4
app.json
4
app.json
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "Mastodon",
|
||||
"description": "A GNU Social-compatible microblogging server",
|
||||
"repository": "https://github.com/tootsuite/mastodon",
|
||||
"logo": "https://github.com/tootsuite.png",
|
||||
"repository": "https://github.com/mastodon/mastodon",
|
||||
"logo": "https://github.com/mastodon.png",
|
||||
"env": {
|
||||
"HEROKU": {
|
||||
"description": "Leave this as true",
|
||||
|
|
|
@ -22,6 +22,7 @@ class AboutController < ApplicationController
|
|||
|
||||
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
|
||||
|
||||
@rules = Rule.ordered
|
||||
@contents = toc_generator.html
|
||||
@table_of_contents = toc_generator.toc
|
||||
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
|
||||
|
|
|
@ -78,11 +78,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def only_media_scope
|
||||
Status.where(id: account_media_status_ids)
|
||||
end
|
||||
|
||||
def account_media_status_ids
|
||||
@account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
|
||||
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
|
||||
end
|
||||
|
||||
def no_replies_scope
|
||||
|
@ -136,15 +132,15 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def media_requested?
|
||||
request.path.split('.').first.ends_with?('/media') && !tag_requested?
|
||||
request.path.split('.').first.end_with?('/media') && !tag_requested?
|
||||
end
|
||||
|
||||
def replies_requested?
|
||||
request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
|
||||
request.path.split('.').first.end_with?('/with_replies') && !tag_requested?
|
||||
end
|
||||
|
||||
def tag_requested?
|
||||
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||
end
|
||||
|
||||
def cached_filtered_status_page
|
||||
|
|
|
@ -19,11 +19,11 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
|
|||
private
|
||||
|
||||
def uri_prefix
|
||||
signed_request_account.uri[/http(s?):\/\/[^\/]+\//]
|
||||
signed_request_account.uri[Account::URL_PREFIX_RE]
|
||||
end
|
||||
|
||||
def set_items
|
||||
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri)
|
||||
@items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri)
|
||||
end
|
||||
|
||||
def collection_presenter
|
||||
|
|
|
@ -5,7 +5,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
include JsonLdHelper
|
||||
include AccountOwnedConcern
|
||||
|
||||
before_action :skip_unknown_actor_delete
|
||||
before_action :skip_unknown_actor_activity
|
||||
before_action :require_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
|
@ -18,13 +18,13 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
|
||||
private
|
||||
|
||||
def skip_unknown_actor_delete
|
||||
head 202 if unknown_deleted_account?
|
||||
def skip_unknown_actor_activity
|
||||
head 202 if unknown_affected_account?
|
||||
end
|
||||
|
||||
def unknown_deleted_account?
|
||||
def unknown_affected_account?
|
||||
json = Oj.load(body, mode: :strict)
|
||||
json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
|
||||
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
|
||||
rescue Oj::ParseError
|
||||
false
|
||||
end
|
||||
|
|
|
@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
|
||||
if page_requested?
|
||||
expires_in(1.minute, public: public_fetch_mode? && signed_request_account.nil?)
|
||||
else
|
||||
expires_in(3.minutes, public: public_fetch_mode?)
|
||||
end
|
||||
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
|
@ -20,7 +24,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
def outbox_presenter
|
||||
if page_requested?
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: outbox_url(page_params),
|
||||
id: outbox_url(**page_params),
|
||||
type: :ordered,
|
||||
part_of: outbox_url,
|
||||
prev: prev_page,
|
||||
|
@ -29,7 +33,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
)
|
||||
else
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: account_outbox_url(@account),
|
||||
id: outbox_url,
|
||||
type: :ordered,
|
||||
size: @account.statuses_count,
|
||||
first: outbox_url(page: true),
|
||||
|
@ -47,11 +51,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
end
|
||||
|
||||
def next_page
|
||||
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
|
||||
outbox_url(page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
|
||||
end
|
||||
|
||||
def prev_page
|
||||
account_outbox_url(@account, page: true, min_id: @statuses.first.id) unless @statuses.empty?
|
||||
outbox_url(page: true, min_id: @statuses.first.id) unless @statuses.empty?
|
||||
end
|
||||
|
||||
def set_statuses
|
||||
|
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
def set_account
|
||||
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'sidekiq/api'
|
|||
module Admin
|
||||
class DashboardController < BaseController
|
||||
def index
|
||||
@system_checks = Admin::SystemCheck.perform
|
||||
@users_count = User.count
|
||||
@pending_users_count = User.pending.count
|
||||
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
|
||||
|
@ -35,7 +36,6 @@ module Admin
|
|||
@profile_directory = Setting.profile_directory
|
||||
@timeline_preview = Setting.timeline_preview
|
||||
@keybase_integration = Setting.enable_keybase
|
||||
@spam_check_enabled = Setting.spam_check_enabled
|
||||
@trends_enabled = Setting.trends
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ module Admin
|
|||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
|
||||
@domain_block.errors[:domain].clear
|
||||
@domain_block.errors.delete(:domain)
|
||||
render :new
|
||||
else
|
||||
if existing_domain_block.present?
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class FollowRecommendationsController < BaseController
|
||||
before_action :set_language
|
||||
|
||||
def show
|
||||
authorize :follow_recommendation, :show?
|
||||
|
||||
@form = Form::AccountBatch.new
|
||||
@accounts = filtered_follow_recommendations
|
||||
end
|
||||
|
||||
def update
|
||||
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
# Do nothing
|
||||
ensure
|
||||
redirect_to admin_follow_recommendations_path(filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_language
|
||||
@language = follow_recommendation_filter.language
|
||||
end
|
||||
|
||||
def filtered_follow_recommendations
|
||||
follow_recommendation_filter.results
|
||||
end
|
||||
|
||||
def follow_recommendation_filter
|
||||
@follow_recommendation_filter ||= FollowRecommendationFilter.new(filter_params)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
params.require(:form_account_batch).permit(:action, account_ids: [])
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(*FollowRecommendationFilter::KEYS).permit(*FollowRecommendationFilter::KEYS)
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:suppress]
|
||||
'suppress_follow_recommendation'
|
||||
elsif params[:unsuppress]
|
||||
'unsuppress_follow_recommendation'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,8 @@
|
|||
module Admin
|
||||
class InstancesController < BaseController
|
||||
before_action :set_instances, only: :index
|
||||
before_action :set_instance, only: :show
|
||||
before_action :set_instance, except: :index
|
||||
before_action :set_exhausted_deliveries_days, only: :show
|
||||
|
||||
def index
|
||||
authorize :instance, :index?
|
||||
|
@ -13,14 +14,55 @@ module Admin
|
|||
authorize :instance, :show?
|
||||
end
|
||||
|
||||
def clear_delivery_errors
|
||||
authorize :delivery, :clear_delivery_errors?
|
||||
|
||||
@instance.delivery_failure_tracker.clear_failures!
|
||||
redirect_to admin_instance_path(@instance.domain)
|
||||
end
|
||||
|
||||
def restart_delivery
|
||||
authorize :delivery, :restart_delivery?
|
||||
|
||||
last_unavailable_domain = unavailable_domain
|
||||
|
||||
if last_unavailable_domain.present?
|
||||
@instance.delivery_failure_tracker.track_success!
|
||||
log_action :destroy, last_unavailable_domain
|
||||
end
|
||||
|
||||
redirect_to admin_instance_path(@instance.domain)
|
||||
end
|
||||
|
||||
def stop_delivery
|
||||
authorize :delivery, :stop_delivery?
|
||||
|
||||
UnavailableDomain.create(domain: @instance.domain)
|
||||
log_action :create, unavailable_domain
|
||||
redirect_to admin_instance_path(@instance.domain)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_instance
|
||||
@instance = Instance.find(params[:id])
|
||||
end
|
||||
|
||||
def set_exhausted_deliveries_days
|
||||
@exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days
|
||||
end
|
||||
|
||||
def set_instances
|
||||
@instances = filtered_instances.page(params[:page])
|
||||
warning_domains_map = DeliveryFailureTracker.warning_domains_map
|
||||
|
||||
@instances.each do |instance|
|
||||
instance.failure_days = warning_domains_map[instance.domain]
|
||||
end
|
||||
end
|
||||
|
||||
def unavailable_domain
|
||||
UnavailableDomain.find_by(domain: @instance.domain)
|
||||
end
|
||||
|
||||
def filtered_instances
|
||||
|
|
|
@ -6,9 +6,9 @@ module Admin
|
|||
|
||||
def create
|
||||
authorize @user, :reset_password?
|
||||
@user.send_reset_password_instructions
|
||||
@user.reset_password!
|
||||
log_action :reset_password, @user
|
||||
redirect_to admin_accounts_path
|
||||
redirect_to admin_account_path(@user.account_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class RulesController < BaseController
|
||||
before_action :set_rule, except: [:index, :create]
|
||||
|
||||
def index
|
||||
authorize :rule, :index?
|
||||
|
||||
@rules = Rule.ordered
|
||||
@rule = Rule.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :rule, :create?
|
||||
|
||||
@rule = Rule.new(resource_params)
|
||||
|
||||
if @rule.save
|
||||
redirect_to admin_rules_path
|
||||
else
|
||||
@rules = Rule.ordered
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @rule, :update?
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @rule, :update?
|
||||
|
||||
if @rule.update(resource_params)
|
||||
redirect_to admin_rules_path
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @rule, :destroy?
|
||||
|
||||
@rule.discard
|
||||
|
||||
redirect_to admin_rules_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_rule
|
||||
@rule = Rule.find(params[:id])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:rule).permit(:text, :priority)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SignInTokenAuthenticationsController < BaseController
|
||||
before_action :set_target_user
|
||||
|
||||
def create
|
||||
authorize @user, :enable_sign_in_token_auth?
|
||||
@user.update(skip_sign_in_token: false)
|
||||
log_action :enable_sign_in_token_auth, @user
|
||||
redirect_to admin_account_path(@user.account_id)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @user, :disable_sign_in_token_auth?
|
||||
@user.update(skip_sign_in_token: true)
|
||||
log_action :disable_sign_in_token_auth, @user
|
||||
redirect_to admin_account_path(@user.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_target_user
|
||||
@user = User.find(params[:user_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,8 +14,7 @@ module Admin
|
|||
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
|
||||
|
||||
if params[:media]
|
||||
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id)
|
||||
@statuses.merge!(Status.where(id: account_media_status_ids))
|
||||
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id))
|
||||
end
|
||||
|
||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
||||
|
|
|
@ -59,8 +59,8 @@ module Admin
|
|||
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
|
||||
.joins(:account)
|
||||
.group('accounts.domain')
|
||||
.reorder('statuses_count desc')
|
||||
.pluck('accounts.domain, count(*) AS statuses_count')
|
||||
.reorder(statuses_count: :desc)
|
||||
.pluck(Arel.sql('accounts.domain, count(*) AS statuses_count'))
|
||||
end
|
||||
|
||||
def set_counters
|
||||
|
|
|
@ -9,7 +9,7 @@ module Admin
|
|||
@user.disable_two_factor!
|
||||
log_action :disable_2fa, @user
|
||||
UserMailer.two_factor_disabled(@user).deliver_later!
|
||||
redirect_to admin_accounts_path
|
||||
redirect_to admin_account_path(@user.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -40,7 +40,12 @@ class Api::BaseController < ApplicationController
|
|||
render json: { error: 'This action is not allowed' }, status: 403
|
||||
end
|
||||
|
||||
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight do
|
||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||
Rails.logger.warn "Storage server error: #{e}"
|
||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||
end
|
||||
|
||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
|
||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::LookupController < Api::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
|
||||
before_action :set_account
|
||||
|
||||
def show
|
||||
render json: @account, serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
|
@ -27,13 +27,15 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
|
||||
self.response_body = Oj.dump(response.body)
|
||||
self.status = response.status
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def follow
|
||||
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
|
||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
|
||||
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options)
|
||||
end
|
||||
|
||||
def block
|
||||
|
@ -42,7 +44,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def mute
|
||||
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration] || 0))
|
||||
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0))
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
|
@ -68,7 +70,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def account_params
|
||||
|
|
|
@ -12,7 +12,7 @@ class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
|
|||
private
|
||||
|
||||
def set_claim_results
|
||||
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
|
||||
@claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }
|
||||
end
|
||||
|
||||
def resource_params
|
||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
|
|||
end
|
||||
|
||||
def set_query_results
|
||||
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
|
||||
@query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) }
|
||||
end
|
||||
|
||||
def account_ids
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Emails::ConfirmationsController < Api::BaseController
|
||||
before_action :doorkeeper_authorize!
|
||||
before_action :require_user_owned_by_application!
|
||||
before_action :require_user_not_confirmed!
|
||||
|
||||
def create
|
||||
current_user.update!(email: params[:email]) if params.key?(:email)
|
||||
current_user.resend_confirmation_instructions
|
||||
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_user_owned_by_application!
|
||||
render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id
|
||||
end
|
||||
|
||||
def require_user_not_confirmed!
|
||||
render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden if current_user.confirmed? || current_user.unconfirmed_email.blank?
|
||||
end
|
||||
end
|
|
@ -29,7 +29,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options)
|
||||
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::RulesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||
|
||||
before_action :set_rules
|
||||
|
||||
def index
|
||||
render json: @rules, each_serializer: REST::RuleSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_rules
|
||||
@rules = Rule.ordered
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ class Api::V1::MarkersController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
@markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker }
|
||||
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
|
||||
render json: serialize_map(@markers)
|
||||
end
|
||||
|
||||
|
|
|
@ -40,12 +40,13 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_notifications
|
||||
cache_collection_paginated_by_id(
|
||||
browserable_account_notifications,
|
||||
Notification,
|
||||
notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id(
|
||||
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
|
||||
cache_collection(target_statuses, Status)
|
||||
end
|
||||
end
|
||||
|
||||
def browserable_account_notifications
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :push }
|
||||
before_action :require_user!
|
||||
before_action :set_web_push_subscription
|
||||
before_action :check_web_push_subscription, only: [:show, :update]
|
||||
before_action :set_push_subscription
|
||||
before_action :check_push_subscription, only: [:show, :update]
|
||||
|
||||
def create
|
||||
@web_subscription&.destroy!
|
||||
@push_subscription&.destroy!
|
||||
|
||||
@web_subscription = ::Web::PushSubscription.create!(
|
||||
@push_subscription = Web::PushSubscription.create!(
|
||||
endpoint: subscription_params[:endpoint],
|
||||
key_p256dh: subscription_params[:keys][:p256dh],
|
||||
key_auth: subscription_params[:keys][:auth],
|
||||
|
@ -18,31 +18,31 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
access_token_id: doorkeeper_token.id
|
||||
)
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@web_subscription.update!(data: data_params)
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
@push_subscription.update!(data: data_params)
|
||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@web_subscription&.destroy!
|
||||
@push_subscription&.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_web_push_subscription
|
||||
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||
def set_push_subscription
|
||||
@push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||
end
|
||||
|
||||
def check_web_push_subscription
|
||||
not_found if @web_subscription.nil?
|
||||
def check_push_subscription
|
||||
not_found if @push_subscription.nil?
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
|
@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
|
||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||
params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,20 +5,20 @@ class Api::V1::SuggestionsController < Api::BaseController
|
|||
|
||||
before_action -> { doorkeeper_authorize! :read }
|
||||
before_action :require_user!
|
||||
before_action :set_accounts
|
||||
|
||||
def index
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
suggestions = suggestions_source.get(current_account, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||
render json: suggestions.map(&:account), each_serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
PotentialFriendshipTracker.remove(current_account.id, params[:id])
|
||||
suggestions_source.remove(current_account, params[:id])
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = PotentialFriendshipTracker.get(current_account.id, limit: limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||
def suggestions_source
|
||||
AccountSuggestions::PastInteractionsSource.new
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V2::SuggestionsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read }
|
||||
before_action :require_user!
|
||||
before_action :set_suggestions
|
||||
|
||||
def index
|
||||
render json: @suggestions, each_serializer: REST::SuggestionSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_suggestions
|
||||
@suggestions = AccountSuggestions.get(current_account, limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_push_subscription, only: :update
|
||||
|
||||
def create
|
||||
active_session = current_session
|
||||
|
@ -15,9 +16,11 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
|
||||
|
||||
data = {
|
||||
policy: 'all',
|
||||
|
||||
alerts: {
|
||||
follow: alerts_enabled,
|
||||
follow_request: false,
|
||||
follow_request: alerts_enabled,
|
||||
favourite: alerts_enabled,
|
||||
reblog: alerts_enabled,
|
||||
mention: alerts_enabled,
|
||||
|
@ -28,7 +31,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
|
||||
data.deep_merge!(data_params) if params[:data]
|
||||
|
||||
web_subscription = ::Web::PushSubscription.create!(
|
||||
push_subscription = ::Web::PushSubscription.create!(
|
||||
endpoint: subscription_params[:endpoint],
|
||||
key_p256dh: subscription_params[:keys][:p256dh],
|
||||
key_auth: subscription_params[:keys][:auth],
|
||||
|
@ -37,27 +40,27 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
access_token_id: active_session.access_token_id
|
||||
)
|
||||
|
||||
active_session.update!(web_push_subscription: web_subscription)
|
||||
active_session.update!(web_push_subscription: push_subscription)
|
||||
|
||||
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
params.require([:id])
|
||||
|
||||
web_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
web_subscription.update!(data: data_params)
|
||||
|
||||
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
@push_subscription.update!(data: data_params)
|
||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_push_subscription
|
||||
@push_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||
end
|
||||
|
||||
def data_params
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||
@data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,17 +2,16 @@
|
|||
|
||||
class Api::Web::SettingsController < Api::Web::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_setting
|
||||
|
||||
def update
|
||||
setting.data = params[:data]
|
||||
setting.save!
|
||||
|
||||
@setting.update!(data: params[:data])
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setting
|
||||
@_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
|
||||
def set_setting
|
||||
@setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,6 @@ class ApplicationController < ActionController::Base
|
|||
# For APIs, you may want to use :null_session instead.
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
force_ssl if: :https_enabled?
|
||||
|
||||
include Localized
|
||||
include UserTrackingConcern
|
||||
include SessionTrackingConcern
|
||||
|
@ -21,17 +19,21 @@ class ApplicationController < ActionController::Base
|
|||
helper_method :use_seamless_external_login?
|
||||
helper_method :whitelist_mode?
|
||||
|
||||
rescue_from ActionController::RoutingError, with: :not_found
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||
rescue_from ActionController::ParameterMissing, with: :bad_request
|
||||
rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, with: :service_unavailable
|
||||
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||
|
||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
|
||||
|
||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||
Rails.logger.warn "Storage server error: #{e}"
|
||||
service_unavailable
|
||||
end
|
||||
|
||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
||||
before_action :require_functional!, if: :user_signed_in?
|
||||
|
||||
|
@ -43,10 +45,6 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
private
|
||||
|
||||
def https_enabled?
|
||||
Rails.env.production? && !request.path.start_with?('/health')
|
||||
end
|
||||
|
||||
def authorized_fetch_mode?
|
||||
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode
|
||||
end
|
||||
|
|
|
@ -22,7 +22,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
end
|
||||
|
||||
def require_unconfirmed!
|
||||
redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
||||
if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
||||
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
|
||||
end
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
|
|
|
@ -10,6 +10,15 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
||||
|
||||
if @user.persisted?
|
||||
LoginActivity.create(
|
||||
user: @user,
|
||||
success: true,
|
||||
authentication_method: :omniauth,
|
||||
provider: provider,
|
||||
ip: request.remote_ip,
|
||||
user_agent: request.user_agent
|
||||
)
|
||||
|
||||
sign_in_and_redirect @user, event: :authentication
|
||||
set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
|
||||
else
|
||||
|
|
|
@ -27,9 +27,11 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
|
||||
def create
|
||||
super do |resource|
|
||||
resource.update_sign_in!(request, new_sign_in: true)
|
||||
remember_me(resource)
|
||||
flash.delete(:notice)
|
||||
# We only need to call this if this hasn't already been
|
||||
# called from one of the two-factor or sign-in token
|
||||
# authentication methods
|
||||
|
||||
on_authentication_success(resource, :password) unless @on_authentication_success_called
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -42,11 +44,12 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
end
|
||||
|
||||
def webauthn_options
|
||||
user = find_user
|
||||
user = User.find_by(id: session[:attempt_user_id])
|
||||
|
||||
if user.webauthn_enabled?
|
||||
if user&.webauthn_enabled?
|
||||
options_for_get = WebAuthn::Credential.options_for_get(
|
||||
allow: user.webauthn_credentials.pluck(:external_id)
|
||||
allow: user.webauthn_credentials.pluck(:external_id),
|
||||
user_verification: 'discouraged'
|
||||
)
|
||||
|
||||
session[:webauthn_challenge] = options_for_get.challenge
|
||||
|
@ -60,16 +63,20 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
protected
|
||||
|
||||
def find_user
|
||||
if session[:attempt_user_id]
|
||||
if user_params[:email].present?
|
||||
find_user_from_params
|
||||
elsif session[:attempt_user_id]
|
||||
User.find_by(id: session[:attempt_user_id])
|
||||
else
|
||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||
user ||= User.find_for_authentication(email: user_params[:email])
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
def find_user_from_params
|
||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||
user ||= User.find_for_authentication(email: user_params[:email])
|
||||
user
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
|
||||
end
|
||||
|
@ -142,4 +149,34 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
session.delete(:attempt_user_id)
|
||||
session.delete(:attempt_user_updated_at)
|
||||
end
|
||||
|
||||
def on_authentication_success(user, security_measure)
|
||||
@on_authentication_success_called = true
|
||||
|
||||
clear_attempt_from_session
|
||||
|
||||
user.update_sign_in!(request, new_sign_in: true)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
flash.delete(:notice)
|
||||
|
||||
LoginActivity.create(
|
||||
user: user,
|
||||
success: true,
|
||||
authentication_method: security_measure,
|
||||
ip: request.remote_ip,
|
||||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
|
||||
def on_authentication_failure(user, security_measure, failure_reason)
|
||||
LoginActivity.create(
|
||||
user: user,
|
||||
success: false,
|
||||
authentication_method: security_measure,
|
||||
failure_reason: failure_reason,
|
||||
ip: request.remote_ip,
|
||||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,21 +31,23 @@ module CacheConcern
|
|||
def cache_collection(raw, klass)
|
||||
return raw unless klass.respond_to?(:with_includes)
|
||||
|
||||
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||
return [] if raw.empty?
|
||||
|
||||
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
|
||||
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
|
||||
|
||||
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
||||
|
||||
unless uncached_ids.empty?
|
||||
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
|
||||
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
|
||||
|
||||
uncached.each_value do |item|
|
||||
Rails.cache.write(item, item)
|
||||
end
|
||||
end
|
||||
|
||||
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
|
||||
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
|
||||
end
|
||||
|
||||
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
||||
|
|
|
@ -16,23 +16,26 @@ module SignInTokenAuthenticationConcern
|
|||
end
|
||||
|
||||
def authenticate_with_sign_in_token
|
||||
user = self.resource = find_user
|
||||
if user_params[:email].present?
|
||||
user = self.resource = find_user_from_params
|
||||
prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password])
|
||||
elsif session[:attempt_user_id]
|
||||
user = self.resource = User.find_by(id: session[:attempt_user_id])
|
||||
return if user.nil?
|
||||
|
||||
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
|
||||
restart_session
|
||||
elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id]
|
||||
authenticate_with_sign_in_token_attempt(user)
|
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||
prompt_for_sign_in_token(user)
|
||||
if session[:attempt_user_updated_at] != user.updated_at.to_s
|
||||
restart_session
|
||||
elsif user_params.key?(:sign_in_token_attempt)
|
||||
authenticate_with_sign_in_token_attempt(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_sign_in_token_attempt(user)
|
||||
if valid_sign_in_token_attempt?(user)
|
||||
clear_attempt_from_session
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
on_authentication_success(user, :sign_in_token)
|
||||
else
|
||||
on_authentication_failure(user, :sign_in_token, :invalid_sign_in_token)
|
||||
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
||||
prompt_for_sign_in_token(user)
|
||||
end
|
||||
|
|
|
@ -133,6 +133,7 @@ module SignatureVerification
|
|||
|
||||
def verify_body_digest!
|
||||
return unless signed_headers.include?('digest')
|
||||
raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
|
||||
|
||||
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
||||
sha256 = digests.assoc('sha-256')
|
||||
|
|
|
@ -35,16 +35,20 @@ module TwoFactorAuthenticationConcern
|
|||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
if user_params[:email].present?
|
||||
user = self.resource = find_user_from_params
|
||||
prompt_for_two_factor(user) if user&.external_or_valid_password?(user_params[:password])
|
||||
elsif session[:attempt_user_id]
|
||||
user = self.resource = User.find_by(id: session[:attempt_user_id])
|
||||
return if user.nil?
|
||||
|
||||
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
|
||||
restart_session
|
||||
elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id]
|
||||
authenticate_with_two_factor_via_webauthn(user)
|
||||
elsif user_params.key?(:otp_attempt) && session[:attempt_user_id]
|
||||
authenticate_with_two_factor_via_otp(user)
|
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||
prompt_for_two_factor(user)
|
||||
if session[:attempt_user_updated_at] != user.updated_at.to_s
|
||||
restart_session
|
||||
elsif user.webauthn_enabled? && user_params.key?(:credential)
|
||||
authenticate_with_two_factor_via_webauthn(user)
|
||||
elsif user_params.key?(:otp_attempt)
|
||||
authenticate_with_two_factor_via_otp(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -52,21 +56,19 @@ module TwoFactorAuthenticationConcern
|
|||
webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential])
|
||||
|
||||
if valid_webauthn_credential?(user, webauthn_credential)
|
||||
clear_attempt_from_session
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
on_authentication_success(user, :webauthn)
|
||||
render json: { redirect_path: root_path }, status: :ok
|
||||
else
|
||||
on_authentication_failure(user, :webauthn, :invalid_credential)
|
||||
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if valid_otp_attempt?(user)
|
||||
clear_attempt_from_session
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
on_authentication_success(user, :otp)
|
||||
else
|
||||
on_authentication_failure(user, :otp, :invalid_otp_token)
|
||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
|
|
|
@ -3,11 +3,16 @@
|
|||
class CustomCssController < ApplicationController
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :update_user_sign_in
|
||||
skip_before_action :set_session_activity
|
||||
|
||||
skip_around_action :set_locale
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
expires_in 3.minutes, public: true
|
||||
request.session_options[:skip] = true
|
||||
render plain: Setting.custom_css || '', content_type: 'text/css'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,6 @@ class DirectoriesController < ApplicationController
|
|||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :require_enabled!
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_tag, only: :show
|
||||
before_action :set_accounts
|
||||
before_action :set_pack
|
||||
|
||||
|
@ -16,10 +15,6 @@ class DirectoriesController < ApplicationController
|
|||
render :index
|
||||
end
|
||||
|
||||
def show
|
||||
render :index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
|
@ -30,13 +25,8 @@ class DirectoriesController < ApplicationController
|
|||
return not_found unless Setting.profile_directory
|
||||
end
|
||||
|
||||
def set_tag
|
||||
@tag = Tag.discoverable.find_normalized!(params[:id])
|
||||
end
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
|
||||
query.merge!(Account.tagged_with(@tag.id)) if @tag
|
||||
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,7 +86,7 @@ class FollowerAccountsController < ApplicationController
|
|||
if page_requested? || !@account.user_hides_network?
|
||||
# Return all fields
|
||||
else
|
||||
%i(id type totalItems)
|
||||
%i(id type total_items)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,7 +86,7 @@ class FollowingAccountsController < ApplicationController
|
|||
if page_requested? || !@account.user_hides_network?
|
||||
# Return all fields
|
||||
else
|
||||
%i(id type totalItems)
|
||||
%i(id type total_items)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HealthController < ActionController::Base
|
||||
def show
|
||||
render plain: 'OK'
|
||||
end
|
||||
end
|
|
@ -16,30 +16,7 @@ class HomeController < ApplicationController
|
|||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in?
|
||||
|
||||
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
|
||||
|
||||
if matches
|
||||
case matches[1]
|
||||
when 'statuses'
|
||||
status = Status.find_by(id: matches[2])
|
||||
|
||||
if status&.distributable?
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(status))
|
||||
return
|
||||
end
|
||||
when 'accounts'
|
||||
account = Account.find_by(id: matches[2])
|
||||
|
||||
if account
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(account))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
|
||||
|
||||
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
|
||||
redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
|
||||
end
|
||||
|
||||
def set_pack
|
||||
|
|
|
@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
|
|||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(-99)
|
||||
@account = Account.representative
|
||||
end
|
||||
|
||||
def restrict_fields_to
|
||||
|
|
|
@ -37,7 +37,7 @@ class MediaProxyController < ApplicationController
|
|||
end
|
||||
|
||||
def version
|
||||
if request.path.ends_with?('/small')
|
||||
if request.path.end_with?('/small')
|
||||
:small
|
||||
else
|
||||
:original
|
||||
|
@ -45,7 +45,7 @@ class MediaProxyController < ApplicationController
|
|||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
|
||||
{ redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
|
||||
end
|
||||
|
||||
def reject_media?
|
||||
|
|
|
@ -42,7 +42,7 @@ class Settings::DeletesController < Settings::BaseController
|
|||
end
|
||||
|
||||
def destroy_account!
|
||||
current_account.suspend!(origin: :local)
|
||||
current_account.suspend!(origin: :local, block_email: false)
|
||||
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||
sign_out
|
||||
end
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::LoginActivitiesController < Settings::BaseController
|
||||
def index
|
||||
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
end
|
|
@ -21,7 +21,8 @@ module Settings
|
|||
display_name: current_user.account.username,
|
||||
id: current_user.webauthn_id,
|
||||
},
|
||||
exclude: current_user.webauthn_credentials.pluck(:external_id)
|
||||
exclude: current_user.webauthn_credentials.pluck(:external_id),
|
||||
authenticator_selection: { user_verification: 'discouraged' }
|
||||
)
|
||||
|
||||
session[:webauthn_challenge] = options_for_create.challenge
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatusesCleanupController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_policy
|
||||
before_action :set_body_classes
|
||||
before_action :set_pack
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
if @policy.update(resource_params)
|
||||
redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render action: :show
|
||||
end
|
||||
rescue ActionController::ParameterMissing
|
||||
# Do nothing
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
|
||||
def set_policy
|
||||
@policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false)
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
|
@ -8,7 +8,7 @@ class StatusesController < ApplicationController
|
|||
|
||||
layout 'public'
|
||||
|
||||
before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_status
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_link_headers
|
||||
|
@ -16,7 +16,6 @@ class StatusesController < ApplicationController
|
|||
before_action :set_referrer_policy_header, only: :show
|
||||
before_action :set_cache_headers
|
||||
before_action :set_body_classes
|
||||
before_action :set_autoplay, only: :embed
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
|
||||
|
@ -85,8 +84,4 @@ class StatusesController < ApplicationController
|
|||
def set_referrer_policy_header
|
||||
response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
|
||||
end
|
||||
|
||||
def set_autoplay
|
||||
@autoplay = truthy_param?(:autoplay)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@ module WellKnown
|
|||
class WebfingerController < ActionController::Base
|
||||
include RoutingHelper
|
||||
|
||||
before_action { response.headers['Vary'] = 'Accept' }
|
||||
before_action :set_account
|
||||
before_action :check_account_suspension
|
||||
|
||||
|
@ -39,10 +38,12 @@ module WellKnown
|
|||
end
|
||||
|
||||
def bad_request
|
||||
expires_in(3.minutes, public: true)
|
||||
head 400
|
||||
end
|
||||
|
||||
def not_found
|
||||
expires_in(3.minutes, public: true)
|
||||
head 404
|
||||
end
|
||||
|
||||
|
|
|
@ -84,19 +84,19 @@ module AccountsHelper
|
|||
def account_description(account)
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
number_to_human(account.statuses_count, precision: 3, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
number_to_human(account.following_count, precision: 3, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
number_to_human(account.followers_count, precision: 3, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module Admin::ActionLogsHelper
|
|||
record.shortcode
|
||||
when 'Report'
|
||||
link_to "##{record.id}", admin_report_path(record)
|
||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
|
||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
||||
link_to record.domain, "https://#{record.domain}"
|
||||
when 'Status'
|
||||
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
|
||||
|
@ -38,7 +38,7 @@ module Admin::ActionLogsHelper
|
|||
case type
|
||||
when 'CustomEmoji'
|
||||
attributes['shortcode']
|
||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
|
||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
|
||||
|
|
|
@ -14,6 +14,17 @@ module ApplicationHelper
|
|||
ku
|
||||
).freeze
|
||||
|
||||
def friendly_number_to_human(number, **options)
|
||||
# By default, the number of precision digits used by number_to_human
|
||||
# is looked up from the locales definition, and rails-i18n comes with
|
||||
# values that don't seem to make much sense for many languages, so
|
||||
# override these values with a default of 3 digits of precision.
|
||||
options[:precision] = 3
|
||||
options[:strip_insignificant_zeros] = true
|
||||
|
||||
number_to_human(number, **options)
|
||||
end
|
||||
|
||||
def active_nav_class(*paths)
|
||||
paths.any? { |path| current_page?(path) } ? 'active' : ''
|
||||
end
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module EmailHelper
|
||||
def self.included(base)
|
||||
base.extend(self)
|
||||
end
|
||||
|
||||
def email_to_canonical_email(str)
|
||||
username, domain = str.downcase.split('@', 2)
|
||||
username, = username.gsub('.', '').split('+', 2)
|
||||
|
||||
"#{username}@#{domain}"
|
||||
end
|
||||
|
||||
def email_to_canonical_email_hash(str)
|
||||
Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str))
|
||||
end
|
||||
end
|
|
@ -67,7 +67,7 @@ module JsonLdHelper
|
|||
unless id
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
|
||||
return unless json
|
||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||
|
||||
uri = json['id']
|
||||
end
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MascotHelper
|
||||
def mascot_url
|
||||
full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instance_presenter
|
||||
@instance_presenter ||= InstancePresenter.new
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module SettingsHelper
|
||||
HUMAN_LOCALES = {
|
||||
af: 'Afrikaans',
|
||||
ar: 'العربية',
|
||||
ast: 'Asturianu',
|
||||
bg: 'Български',
|
||||
|
@ -17,6 +18,7 @@ module SettingsHelper
|
|||
en: 'English',
|
||||
eo: 'Esperanto',
|
||||
'es-AR': 'Español (Argentina)',
|
||||
'es-MX': 'Español (México)',
|
||||
es: 'Español',
|
||||
et: 'Eesti',
|
||||
eu: 'Euskara',
|
||||
|
@ -24,6 +26,7 @@ module SettingsHelper
|
|||
fi: 'Suomi',
|
||||
fr: 'Français',
|
||||
ga: 'Gaeilge',
|
||||
gd: 'Gàidhlig',
|
||||
gl: 'Galego',
|
||||
he: 'עברית',
|
||||
hi: 'हिन्दी',
|
||||
|
@ -59,6 +62,7 @@ module SettingsHelper
|
|||
ru: 'Русский',
|
||||
sa: 'संस्कृतम्',
|
||||
sc: 'Sardu',
|
||||
si: 'සිංහල',
|
||||
sk: 'Slovenčina',
|
||||
sl: 'Slovenščina',
|
||||
sq: 'Shqip',
|
||||
|
|
|
@ -130,4 +130,84 @@ module StatusesHelper
|
|||
def embedded_view?
|
||||
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
|
||||
end
|
||||
|
||||
def render_video_component(status, **options)
|
||||
video = status.media_attachments.first
|
||||
|
||||
meta = video.file.meta || {}
|
||||
|
||||
component_params = {
|
||||
sensitive: sensitized?(status, current_account),
|
||||
src: full_asset_url(video.file.url(:original)),
|
||||
preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)),
|
||||
alt: video.description,
|
||||
blurhash: video.blurhash,
|
||||
frameRate: meta.dig('original', 'frame_rate'),
|
||||
inline: true,
|
||||
media: [
|
||||
ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer),
|
||||
].as_json,
|
||||
}.merge(**options)
|
||||
|
||||
react_component :video, component_params do
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
end
|
||||
end
|
||||
|
||||
def render_audio_component(status, **options)
|
||||
audio = status.media_attachments.first
|
||||
|
||||
meta = audio.file.meta || {}
|
||||
|
||||
component_params = {
|
||||
src: full_asset_url(audio.file.url(:original)),
|
||||
poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url),
|
||||
alt: audio.description,
|
||||
backgroundColor: meta.dig('colors', 'background'),
|
||||
foregroundColor: meta.dig('colors', 'foreground'),
|
||||
accentColor: meta.dig('colors', 'accent'),
|
||||
duration: meta.dig('original', 'duration'),
|
||||
}.merge(**options)
|
||||
|
||||
react_component :audio, component_params do
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
end
|
||||
end
|
||||
|
||||
def render_media_gallery_component(status, **options)
|
||||
component_params = {
|
||||
sensitive: sensitized?(status, current_account),
|
||||
autoplay: prefers_autoplay?,
|
||||
media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json },
|
||||
}.merge(**options)
|
||||
|
||||
react_component :media_gallery, component_params do
|
||||
render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
end
|
||||
end
|
||||
|
||||
def render_card_component(status, **options)
|
||||
component_params = {
|
||||
sensitive: sensitized?(status, current_account),
|
||||
maxDescription: 160,
|
||||
card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json,
|
||||
}.merge(**options)
|
||||
|
||||
react_component :card, component_params
|
||||
end
|
||||
|
||||
def render_poll_component(status, **options)
|
||||
component_params = {
|
||||
disabled: true,
|
||||
poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json,
|
||||
}.merge(**options)
|
||||
|
||||
react_component :poll, component_params do
|
||||
render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
|
||||
end
|
||||
end
|
||||
|
||||
def prefers_autoplay?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
|||
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
||||
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
|
||||
|
||||
export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
|
||||
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
|
||||
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
|
||||
|
||||
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
|
||||
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
|
||||
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
|
||||
|
@ -104,6 +108,34 @@ export function fetchAccount(id) {
|
|||
};
|
||||
};
|
||||
|
||||
export const lookupAccount = acct => (dispatch, getState) => {
|
||||
dispatch(lookupAccountRequest(acct));
|
||||
|
||||
api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
|
||||
dispatch(fetchRelationships([response.data.id]));
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
dispatch(lookupAccountSuccess());
|
||||
}).catch(error => {
|
||||
dispatch(lookupAccountFail(acct, error));
|
||||
});
|
||||
};
|
||||
|
||||
export const lookupAccountRequest = (acct) => ({
|
||||
type: ACCOUNT_LOOKUP_REQUEST,
|
||||
acct,
|
||||
});
|
||||
|
||||
export const lookupAccountSuccess = () => ({
|
||||
type: ACCOUNT_LOOKUP_SUCCESS,
|
||||
});
|
||||
|
||||
export const lookupAccountFail = (acct, error) => ({
|
||||
type: ACCOUNT_LOOKUP_FAIL,
|
||||
acct,
|
||||
error,
|
||||
skipAlert: true,
|
||||
});
|
||||
|
||||
export function fetchAccountRequest(id) {
|
||||
return {
|
||||
type: ACCOUNT_FETCH_REQUEST,
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { openModal } from './modal';
|
||||
|
||||
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
|
||||
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
|
||||
|
||||
export function initBoostModal(props) {
|
||||
return (dispatch, getState) => {
|
||||
const default_privacy = getState().getIn(['compose', 'default_privacy']);
|
||||
|
||||
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
|
||||
|
||||
dispatch({
|
||||
type: BOOSTS_INIT_MODAL,
|
||||
privacy
|
||||
});
|
||||
|
||||
dispatch(openModal('BOOST', props));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function changeBoostPrivacy(privacy) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: BOOSTS_CHANGE_PRIVACY,
|
||||
privacy,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -10,6 +10,7 @@ import { importFetchedAccounts } from './importer';
|
|||
import { updateTimeline } from './timelines';
|
||||
import { showAlertForError } from './alerts';
|
||||
import { showAlert } from './alerts';
|
||||
import { openModal } from './modal';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
|
||||
|
@ -68,6 +69,11 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
|
|||
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
|
||||
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
|
||||
|
||||
export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
|
||||
|
||||
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
||||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
|
@ -77,7 +83,7 @@ const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
|
|||
|
||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
|
||||
routerHistory.push('/statuses/new');
|
||||
routerHistory.push('/publish');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -170,7 +176,8 @@ export function submitCompose(routerHistory) {
|
|||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
}).then(function (response) {
|
||||
if (routerHistory && routerHistory.location.pathname === '/statuses/new'
|
||||
if (routerHistory
|
||||
&& (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
|
||||
&& window.history.state
|
||||
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
|
||||
routerHistory.goBack();
|
||||
|
@ -339,6 +346,32 @@ export const uploadThumbnailFail = error => ({
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
export function initMediaEditModal(id) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: INIT_MEDIA_EDIT_MODAL,
|
||||
id,
|
||||
});
|
||||
|
||||
dispatch(openModal('FOCAL_POINT', { id }));
|
||||
};
|
||||
};
|
||||
|
||||
export function onChangeMediaDescription(description) {
|
||||
return {
|
||||
type: COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||
description,
|
||||
};
|
||||
};
|
||||
|
||||
export function onChangeMediaFocus(focusX, focusY) {
|
||||
return {
|
||||
type: COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||
focusX,
|
||||
focusY,
|
||||
};
|
||||
};
|
||||
|
||||
export function changeUploadCompose(id, params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(changeUploadComposeRequest());
|
||||
|
|
|
@ -24,6 +24,7 @@ export function normalizeAccount(account) {
|
|||
|
||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
||||
account.note_emojified = emojify(account.note, emojiMap);
|
||||
account.note_plain = unescapeHTML(account.note);
|
||||
|
||||
if (account.fields) {
|
||||
account.fields = account.fields.map(pair => ({
|
||||
|
|
|
@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
|||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||
|
||||
export function reblog(status) {
|
||||
export function reblog(status, visibility) {
|
||||
return function (dispatch, getState) {
|
||||
dispatch(reblogRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
|
||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||
dispatch(importFetchedStatus(response.data.reblog));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||
import IntlMessageFormat from 'intl-messageformat';
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { fetchFollowRequests, fetchRelationships } from './accounts';
|
||||
import {
|
||||
importFetchedAccount,
|
||||
importFetchedAccounts,
|
||||
|
@ -90,6 +90,10 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
filtered = regex && regex.test(searchIndex);
|
||||
}
|
||||
|
||||
if (['follow_request'].includes(notification.type)) {
|
||||
dispatch(fetchFollowRequests());
|
||||
}
|
||||
|
||||
dispatch(submitMarkers());
|
||||
|
||||
if (showInColumn) {
|
||||
|
|
|
@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
|
|||
* @param {MediaProps} props
|
||||
* @return {object}
|
||||
*/
|
||||
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
|
||||
type: PICTURE_IN_PICTURE_DEPLOY,
|
||||
statusId,
|
||||
accountId,
|
||||
playerType,
|
||||
props,
|
||||
});
|
||||
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
|
||||
return (dispatch, getState) => {
|
||||
// Do not open a player for a toot that does not exist
|
||||
if (getState().hasIn(['statuses', statusId])) {
|
||||
dispatch({
|
||||
type: PICTURE_IN_PICTURE_DEPLOY,
|
||||
statusId,
|
||||
accountId,
|
||||
playerType,
|
||||
props,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* @return {object}
|
||||
|
|
|
@ -32,6 +32,7 @@ export function submitSearch() {
|
|||
const value = getState().getIn(['search', 'value']);
|
||||
|
||||
if (value.length === 0) {
|
||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Iterable, fromJS } from 'immutable';
|
||||
import { hydrateCompose } from './compose';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { saveSettings } from './settings';
|
||||
|
||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||
|
@ -9,9 +10,22 @@ const convertState = rawState =>
|
|||
fromJS(rawState, (k, v) =>
|
||||
Iterable.isIndexed(v) ? v.toList() : v.toMap());
|
||||
|
||||
const applyMigrations = (state) => {
|
||||
return state.withMutations(state => {
|
||||
// Migrate glitch-soc local-only “Show unread marker” setting to Mastodon's setting
|
||||
if (state.getIn(['local_settings', 'notifications', 'show_unread']) !== undefined) {
|
||||
// Only change if the Mastodon setting does not deviate from default
|
||||
if (state.getIn(['settings', 'notifications', 'showUnread']) !== false) {
|
||||
state.setIn(['settings', 'notifications', 'showUnread'], state.getIn(['local_settings', 'notifications', 'show_unread']));
|
||||
}
|
||||
state.removeIn(['local_settings', 'notifications', 'show_unread'])
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function hydrateStore(rawState) {
|
||||
return dispatch => {
|
||||
const state = convertState(rawState);
|
||||
const state = applyMigrations(convertState(rawState));
|
||||
|
||||
dispatch({
|
||||
type: STORE_HYDRATE,
|
||||
|
@ -20,5 +34,6 @@ export function hydrateStore(rawState) {
|
|||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
|
||||
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
||||
|
@ -7,13 +8,17 @@ export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
|
|||
|
||||
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
|
||||
|
||||
export function fetchSuggestions() {
|
||||
export function fetchSuggestions(withRelationships = false) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchSuggestionsRequest());
|
||||
|
||||
api(getState).get('/api/v1/suggestions').then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
api(getState).get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchSuggestionsSuccess(response.data));
|
||||
|
||||
if (withRelationships) {
|
||||
dispatch(fetchRelationships(response.data.map(item => item.account.id)));
|
||||
}
|
||||
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
||||
};
|
||||
};
|
||||
|
@ -25,10 +30,10 @@ export function fetchSuggestionsRequest() {
|
|||
};
|
||||
};
|
||||
|
||||
export function fetchSuggestionsSuccess(accounts) {
|
||||
export function fetchSuggestionsSuccess(suggestions) {
|
||||
return {
|
||||
type: SUGGESTIONS_FETCH_SUCCESS,
|
||||
accounts,
|
||||
suggestions,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
@ -48,5 +53,12 @@ export const dismissSuggestion = accountId => (dispatch, getState) => {
|
|||
id: accountId,
|
||||
});
|
||||
|
||||
api(getState).delete(`/api/v1/suggestions/${accountId}`);
|
||||
api(getState).delete(`/api/v1/suggestions/${accountId}`).then(() => {
|
||||
dispatch(fetchSuggestionsRequest());
|
||||
|
||||
api(getState).get('/api/v2/suggestions').then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchSuggestionsSuccess(response.data));
|
||||
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
|
|
@ -20,6 +20,8 @@ export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
|
|||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
timeline,
|
||||
|
@ -31,6 +33,13 @@ export function updateTimeline(timeline, status, accept) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getState().getIn(['timelines', timeline, 'isPartial'])) {
|
||||
// Prevent new items from being added to a partial timeline,
|
||||
// since it will be reloaded anyway
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = getFiltersRegex(getState(), { contextType: timeline });
|
||||
const dropRegex = filters[0];
|
||||
const regex = filters[1];
|
||||
|
@ -198,3 +207,8 @@ export const disconnectTimeline = timeline => ({
|
|||
timeline,
|
||||
usePendingItems: preferPendingItems,
|
||||
});
|
||||
|
||||
export const markAsPartial = timeline => ({
|
||||
type: TIMELINE_MARK_AS_PARTIAL,
|
||||
timeline,
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue