diff --git a/.circleci/config.yml b/.circleci/config.yml index e8bfde299..2355d9d7d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -174,8 +174,7 @@ jobs: steps: - *attach_workspace - run: bundle exec i18n-tasks check-normalized - - run: bundle exec i18n-tasks unused - - run: bundle exec i18n-tasks missing -t plural + - run: bundle exec i18n-tasks unused -l en - run: bundle exec i18n-tasks check-consistent-interpolations workflows: diff --git a/.codeclimate.yml b/.codeclimate.yml index a70459764..571507a54 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -30,8 +30,8 @@ plugins: channel: eslint-5 rubocop: enabled: true - channel: rubocop-0-54 - scss-lint: + channel: rubocop-0-71 + sass-lint: enabled: true exclude_patterns: - spec/ diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 000000000..07929aa07 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,10 @@ +version: 1 + +update_configs: + - package_manager: "ruby:bundler" + directory: "/" + update_schedule: "weekly" + + - package_manager: "javascript" + directory: "/" + update_schedule: "weekly" diff --git a/.env.production.sample b/.env.production.sample index d1164efdc..d66b05050 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -10,6 +10,7 @@ DB_NAME=postgres DB_PASS= DB_PORT=5432 # Optional ElasticSearch configuration +# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set) # ES_ENABLED=true # ES_HOST=es # ES_PORT=9200 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..91ee92a2e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: mastodon +open_collective: mastodon diff --git a/.rubocop.yml b/.rubocop.yml index f1095e022..8bd4c867f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,6 @@ +require: + - rubocop-rails + AllCops: TargetRubyVersion: 2.3 Exclude: @@ -82,6 +85,9 @@ Rails/Exit: - 'lib/mastodon/*' - 'lib/cli.rb' +Rails/HelperInstanceVariable: + Enabled: false + Style/ClassAndModuleChildren: Enabled: false diff --git a/.sass-lint.yml b/.sass-lint.yml new file mode 100644 index 000000000..a84adff3f --- /dev/null +++ b/.sass-lint.yml @@ -0,0 +1,37 @@ +# Linter Documentation: +# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options + +files: + include: app/javascript/styles/**/*.scss + ignore: + - app/javascript/styles/mastodon/reset.scss + +rules: + # Disallows + no-color-literals: 0 + no-css-comments: 0 + no-duplicate-properties: 0 + no-ids: 0 + no-important: 0 + no-mergeable-selectors: 0 + no-misspelled-properties: 0 + no-qualifying-elements: 0 + no-transition-all: 0 + no-vendor-prefixes: 0 + + # Nesting + force-element-nesting: 0 + force-attribute-nesting: 0 + force-pseudo-nesting: 0 + + # Name Formats + class-name-format: 0 + leading-zero: 0 + + # Style Guide + attribute-quotes: 0 + hex-length: 0 + indentation: 0 + nesting-depth: 0 + property-sort-order: 0 + quotes: 0 diff --git a/.scss-lint.yml b/.scss-lint.yml deleted file mode 100644 index 5d7cc4da5..000000000 --- a/.scss-lint.yml +++ /dev/null @@ -1,264 +0,0 @@ -# Linter Documentation: -# https://github.com/brigade/scss-lint/blob/v0.42.2/lib/scss_lint/linter/README.md - -scss_files: 'app/javascript/styles/**/*.scss' - -exclude: - - app/javascript/styles/reset.scss - -linters: - # Reports when you use improper spacing around ! (the "bang") in !default, - # !global, !important, and !optional flags. - BangFormat: - enabled: false - - # Whether or not to prefer `border: 0` over `border: none`. - BorderZero: - enabled: false - - # Reports when you define a rule set using a selector with chained classes - # (a.k.a. adjoining classes). - ChainedClasses: - enabled: false - - # Prefer hexadecimal color codes over color keywords. - # (e.g. `color: green` is a color keyword) - ColorKeyword: - enabled: false - - # Prefer color literals (keywords or hexadecimal codes) to be used only in - # variable declarations. They should be referred to via variables everywhere - # else. - ColorVariable: - enabled: true - - # Which form of comments to prefer in CSS. - Comment: - enabled: false - - # Reports @debug statements (which you probably left behind accidentally). - DebugStatement: - enabled: false - - # Rule sets should be ordered as follows: - # - @extend declarations - # - @include declarations without inner @content - # - properties, @include declarations with inner @content - # - nested rule sets. - DeclarationOrder: - enabled: false - - # `scss-lint:disable` control comments should be preceded by a comment - # explaining why these linters are being disabled for this file. - # See https://github.com/brigade/scss-lint#disabling-linters-via-source for - # more information. - DisableLinterReason: - enabled: true - - # Reports when you define the same property twice in a single rule set. - DuplicateProperty: - enabled: false - - # Separate rule, function, and mixin declarations with empty lines. - EmptyLineBetweenBlocks: - enabled: true - - # Reports when you have an empty rule set. - EmptyRule: - enabled: true - - # Reports when you have an @extend directive. - ExtendDirective: - enabled: false - - # Files should always have a final newline. This results in better diffs - # when adding lines to the file, since SCM systems such as git won't - # think that you touched the last line. - FinalNewline: - enabled: false - - # HEX colors should use three-character values where possible. - HexLength: - enabled: false - - # HEX color values should use lower-case colors to differentiate between - # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. - HexNotation: - enabled: true - - # Avoid using ID selectors. - IdSelector: - enabled: false - - # The basenames of @imported SCSS partials should not begin with an - # underscore and should not include the filename extension. - ImportPath: - enabled: false - - # Avoid using !important in properties. It is usually indicative of a - # misunderstanding of CSS specificity and can lead to brittle code. - ImportantRule: - enabled: false - - # Indentation should always be done in increments of 2 spaces. - Indentation: - enabled: true - width: 2 - - # Don't write leading zeros for numeric values with a decimal point. - LeadingZero: - enabled: false - - # Reports when you define the same selector twice in a single sheet. - MergeableSelector: - enabled: false - - # Functions, mixins, variables, and placeholders should be declared - # with all lowercase letters and hyphens instead of underscores. - NameFormat: - enabled: false - - # Avoid nesting selectors too deeply. - NestingDepth: - enabled: false - - # Always use placeholder selectors in @extend. - PlaceholderInExtend: - enabled: false - - # Sort properties in a strict order. - PropertySortOrder: - enabled: false - - # Reports when you use an unknown or disabled CSS property - # (ignoring vendor-prefixed properties). - PropertySpelling: - enabled: false - - # Configure which units are allowed for property values. - PropertyUnits: - enabled: false - - # Pseudo-elements, like ::before, and ::first-letter, should be declared - # with two colons. Pseudo-classes, like :hover and :first-child, should - # be declared with one colon. - PseudoElement: - enabled: true - - # Avoid qualifying elements in selectors (also known as "tag-qualifying"). - QualifyingElement: - enabled: false - - # Don't write selectors with a depth of applicability greater than 3. - SelectorDepth: - enabled: false - - # Selectors should always use hyphenated-lowercase, rather than camelCase or - # snake_case. - SelectorFormat: - enabled: false - convention: hyphenated_lowercase - - # Prefer the shortest shorthand form possible for properties that support it. - Shorthand: - enabled: true - - # Each property should have its own line, except in the special case of - # single line rulesets. - SingleLinePerProperty: - enabled: true - allow_single_line_rule_sets: true - - # Split selectors onto separate lines after each comma, and have each - # individual selector occupy a single line. - SingleLinePerSelector: - enabled: true - - # Commas in lists should be followed by a space. - SpaceAfterComma: - enabled: false - - # Properties should be formatted with a single space separating the colon - # from the property's value. - SpaceAfterPropertyColon: - enabled: true - - # Properties should be formatted with no space between the name and the - # colon. - SpaceAfterPropertyName: - enabled: true - - # Variables should be formatted with a single space separating the colon - # from the variable's value. - SpaceAfterVariableColon: - enabled: true - - # Variables should be formatted with no space between the name and the - # colon. - SpaceAfterVariableName: - enabled: false - - # Operators should be formatted with a single space on both sides of an - # infix operator. - SpaceAroundOperator: - enabled: true - - # Opening braces should be preceded by a single space. - SpaceBeforeBrace: - enabled: true - - # Parentheses should not be padded with spaces. - SpaceBetweenParens: - enabled: false - - # Enforces that string literals should be written with a consistent form - # of quotes (single or double). - StringQuotes: - enabled: false - - # Property values, @extend, @include, and @import directives, and variable - # declarations should always end with a semicolon. - TrailingSemicolon: - enabled: true - - # Reports lines containing trailing whitespace. - TrailingWhitespace: - enabled: true - - # Don't write trailing zeros for numeric values with a decimal point. - TrailingZero: - enabled: false - - # Don't use the `all` keyword to specify transition properties. - TransitionAll: - enabled: false - - # Numeric values should not contain unnecessary fractional portions. - UnnecessaryMantissa: - enabled: false - - # Do not use parent selector references (&) when they would otherwise - # be unnecessary. - UnnecessaryParentReference: - enabled: false - - # URLs should be valid and not contain protocols or domain names. - UrlFormat: - enabled: true - - # URLs should always be enclosed within quotes. - UrlQuotes: - enabled: true - - # Properties, like color and font, are easier to read and maintain - # when defined using variables rather than literals. - VariableForProperty: - enabled: false - - # Avoid vendor prefixes. Or rather: don't write them yourself. - VendorPrefix: - enabled: false - - # Omit length units on zero values, e.g. `0px` vs. `0`. - ZeroUnit: - enabled: true diff --git a/.yarnclean b/.yarnclean index f2de52869..0cc2b50d7 100644 --- a/.yarnclean +++ b/.yarnclean @@ -43,4 +43,4 @@ Gruntfile.js # for specific ignore !.svgo.yml - +!sass-lint/**/*.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 222b7411d..539fec531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,132 @@ Changelog All notable changes to this project will be documented in this file. +## [2.9.2] - 2019-06-22 +### Added + +- Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/11146)) + +### Changed + +- Change camera icon to paperclip icon in upload form ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/11149)) + +### Fixed + +- Fix audio-only OGG and WebM files not being processed as such ([Gargron](https://github.com/tootsuite/mastodon/pull/11151)) +- Fix audio not being downloaded from remote servers ([Gargron](https://github.com/tootsuite/mastodon/pull/11145)) + +## [2.9.1] - 2019-06-22 +### Added + +- Add moderation API ([Gargron](https://github.com/tootsuite/mastodon/pull/9387)) +- Add audio uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/11123), [Gargron](https://github.com/tootsuite/mastodon/pull/11141)) + +### Changed + +- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/tootsuite/mastodon/pull/11138)) +- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/tootsuite/mastodon/pull/11083)) + +### Removed + +- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11139)) + +### Fixed + +- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/tootsuite/mastodon/pull/11130)) +- Fix layout of identity proofs settings ([acid-chicken](https://github.com/tootsuite/mastodon/pull/11126)) +- Fix active scope only returning suspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/11111)) +- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/tootsuite/mastodon/pull/10836)) +- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/tootsuite/mastodon/pull/11121)) +- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ThibG](https://github.com/tootsuite/mastodon/pull/11113)) +- Fix scrolling behaviour in compose form ([ThibG](https://github.com/tootsuite/mastodon/pull/11093)) + +## [2.9.0] - 2019-06-13 +### Added + +- **Add single-column mode in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/10807), [Gargron](https://github.com/tootsuite/mastodon/pull/10848), [Gargron](https://github.com/tootsuite/mastodon/pull/11003), [Gargron](https://github.com/tootsuite/mastodon/pull/10961), [Hanage999](https://github.com/tootsuite/mastodon/pull/10915), [noellabo](https://github.com/tootsuite/mastodon/pull/10917), [abcang](https://github.com/tootsuite/mastodon/pull/10859), [Gargron](https://github.com/tootsuite/mastodon/pull/10820), [Gargron](https://github.com/tootsuite/mastodon/pull/10835), [Gargron](https://github.com/tootsuite/mastodon/pull/10809), [Gargron](https://github.com/tootsuite/mastodon/pull/10963), [noellabo](https://github.com/tootsuite/mastodon/pull/10883), [Hanage999](https://github.com/tootsuite/mastodon/pull/10839)) +- Add waiting time to the list of pending accounts in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10985)) +- Add a keyboard shortcut to hide/show media in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10647), [Gargron](https://github.com/tootsuite/mastodon/pull/10838), [ThibG](https://github.com/tootsuite/mastodon/pull/10872)) +- Add `account_id` param to `GET /api/v1/notifications` ([pwoolcoc](https://github.com/tootsuite/mastodon/pull/10796)) +- Add confirmation modal for unboosting toots in web UI ([aurelien-reeves](https://github.com/tootsuite/mastodon/pull/10287)) +- Add emoji suggestions to content warning and poll option fields in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10555)) +- Add `source` attribute to response of `DELETE /api/v1/statuses/:id` ([ThibG](https://github.com/tootsuite/mastodon/pull/10669)) +- Add some caching for HTML versions of public status pages ([ThibG](https://github.com/tootsuite/mastodon/pull/10701)) +- Add button to conveniently copy OAuth code ([ThibG](https://github.com/tootsuite/mastodon/pull/11065)) + +### Changed + +- **Change default layout to single column in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/10847)) +- **Change light theme** ([Gargron](https://github.com/tootsuite/mastodon/pull/10992), [Gargron](https://github.com/tootsuite/mastodon/pull/10996), [yuzulabo](https://github.com/tootsuite/mastodon/pull/10754), [Gargron](https://github.com/tootsuite/mastodon/pull/10845)) +- **Change preferences page into appearance, notifications, and other** ([Gargron](https://github.com/tootsuite/mastodon/pull/10977), [Gargron](https://github.com/tootsuite/mastodon/pull/10988)) +- Change priority of delete activity forwards for replies and reblogs ([Gargron](https://github.com/tootsuite/mastodon/pull/11002)) +- Change Mastodon logo to use primary text color of the given theme ([Gargron](https://github.com/tootsuite/mastodon/pull/10994)) +- Change reblogs counter to be updated when boosted privately ([Gargron](https://github.com/tootsuite/mastodon/pull/10964)) +- Change bio limit from 160 to 500 characters ([trwnh](https://github.com/tootsuite/mastodon/pull/10790)) +- Change API rate limiting to reduce allowed unauthenticated requests ([ThibG](https://github.com/tootsuite/mastodon/pull/10860), [hinaloe](https://github.com/tootsuite/mastodon/pull/10868), [mayaeh](https://github.com/tootsuite/mastodon/pull/10867)) +- Change help text of `tootctl emoji import` command to specify a gzipped TAR archive is required ([dariusk](https://github.com/tootsuite/mastodon/pull/11000)) +- Change web UI to hide poll options behind content warnings ([ThibG](https://github.com/tootsuite/mastodon/pull/10983)) +- Change silencing to ensure local effects and remote effects are the same for silenced local users ([ThibG](https://github.com/tootsuite/mastodon/pull/10575)) +- Change `tootctl domains purge` to remove custom emoji as well ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10721)) +- Change Docker image to keep `apt` working ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10830)) + +### Removed + +- Remove `dist-upgrade` from Docker image ([SuperSandro2000](https://github.com/tootsuite/mastodon/pull/10822)) + +### Fixed + +- Fix RTL layout not being RTL within the columns area in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10990)) +- Fix display of alternative text when a media attachment is not available in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10981)) +- Fix not being able to directly switch between list timelines in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10973)) +- Fix media sensitivity not being maintained in delete & redraft in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10980)) +- Fix emoji picker being always displayed in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/10979), [yuzulabo](https://github.com/tootsuite/mastodon/pull/10801), [wcpaez](https://github.com/tootsuite/mastodon/pull/10978)) +- Fix potential private status leak through caching ([ThibG](https://github.com/tootsuite/mastodon/pull/10969)) +- Fix refreshing featured toots when the new collection is empty in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10971)) +- Fix undoing domain block also undoing individual moderation on users from before the domain block ([ThibG](https://github.com/tootsuite/mastodon/pull/10660)) +- Fix time not being local in the audit log ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10751)) +- Fix statuses removed by moderation re-appearing on subsequent fetches ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10732)) +- Fix misattribution of inlined announces if `attributedTo` isn't present in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/10967)) +- Fix `GET /api/v1/polls/:id` not requiring authentication for non-public polls ([Gargron](https://github.com/tootsuite/mastodon/pull/10960)) +- Fix handling of blank poll options in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/10946)) +- Fix avatar preview aspect ratio on edit profile page ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10931)) +- Fix web push notifications not being sent for polls ([ThibG](https://github.com/tootsuite/mastodon/pull/10864)) +- Fix cut off letters in last paragraph of statuses in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/10821)) +- Fix list not being automatically unpinned when it returns 404 in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11045)) +- Fix login sometimes redirecting to paths that are not pages ([Gargron](https://github.com/tootsuite/mastodon/pull/11019)) + +## [2.8.4] - 2019-05-24 +### Fixed + +- Fix delivery not retrying on some inbox errors that should be retriable ([ThibG](https://github.com/tootsuite/mastodon/pull/10812)) +- Fix unnecessary 5 minute cooldowns on signature verifications in some cases ([ThibG](https://github.com/tootsuite/mastodon/pull/10813)) +- Fix possible race condition when processing statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10815)) + +### Security + +- Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ThibG](https://github.com/tootsuite/mastodon/pull/10818)) + +## [2.8.3] - 2019-05-19 +### Added + +- Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/tootsuite/mastodon/pull/10779)) +- Add clickable area below avatar in statuses in web UI ([Dar13](https://github.com/tootsuite/mastodon/pull/10766)) +- Add crossed-out eye icon on account gallery in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10715)) +- Add media description tooltip to thumbnails in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10713)) + +### Changed + +- Change "mark as sensitive" button into a checkbox for clarity ([ThibG](https://github.com/tootsuite/mastodon/pull/10748)) + +### Fixed + +- Fix bug allowing users to publicly boost their private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10775), [ThibG](https://github.com/tootsuite/mastodon/pull/10783)) +- Fix performance in formatter by a little ([ThibG](https://github.com/tootsuite/mastodon/pull/10765)) +- Fix some colors in the light theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10754)) +- Fix some colors of the high contrast theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10711)) +- Fix ambivalent active state of poll refresh button in web UI ([MaciekBaron](https://github.com/tootsuite/mastodon/pull/10720)) +- Fix duplicate posting being possible from web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/10785)) +- Fix "invited by" not showing up in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10791)) + ## [2.8.2] - 2019-05-05 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65366be5f..76f512198 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,9 +18,9 @@ Bug reports and feature suggestions can be submitted to [GitHub Issues](https:// ## Translations -You can submit translations via [Weblate](https://weblate.joinmastodon.org/). They are periodically merged into the codebase. +You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase. -[![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] ## Pull requests diff --git a/Dockerfile b/Dockerfile index 6373172fc..3acbc9d4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ SHELL ["bash", "-c"] ENV NODE_VER="8.15.0" RUN echo "Etc/UTC" > /etc/localtime && \ apt update && \ - apt -y dist-upgrade && \ apt -y install wget make gcc g++ python && \ cd ~ && \ wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \ @@ -80,13 +79,12 @@ ARG GID=991 RUN apt update && \ echo "Etc/UTC" > /etc/localtime && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \ - apt -y dist-upgrade && \ apt install -y 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 -# Install masto runtime deps +# Install mastodon runtime deps RUN apt -y --no-install-recommends install \ libssl1.1 libpq5 imagemagick ffmpeg \ libicu60 libprotobuf10 libidn11 libyaml-0-2 \ @@ -95,7 +93,7 @@ RUN apt -y --no-install-recommends install \ ln -s /opt/mastodon /mastodon && \ gem install bundler && \ rm -rf /var/cache && \ - rm -rf /var/lib/apt + rm -rf /var/lib/apt/lists/* # Add tini ENV TINI_VERSION="0.18.0" @@ -104,11 +102,11 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin RUN echo "$TINI_SUM tini" | sha256sum -c - RUN chmod +x /tini -# Copy over masto source, and dependencies from building, and set permissions +# 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 -# Run masto services in prod mode +# Run mastodon services in prod mode ENV RAILS_ENV="production" ENV NODE_ENV="production" diff --git a/Gemfile b/Gemfile index db00c24fb..b72f550eb 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.2' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.36', require: false +gem 'aws-sdk-s3', '~> 1.42', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -53,7 +53,7 @@ gem 'htmlentities', '~> 4.3' gem 'http', '~> 3.3' gem 'http_accept_language', '~> 2.1' gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' -gem 'httplog', '~> 1.2' +gem 'httplog', '~> 1.3' gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' gem 'link_header', '~> 0.0' @@ -62,7 +62,7 @@ gem 'nokogiri', '~> 1.10' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.7' gem 'ostatus2', '~> 2.0' -gem 'ox', '~> 2.10' +gem 'ox', '~> 2.11' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 2.0' gem 'premailer-rails' @@ -82,9 +82,9 @@ gem 'simple-navigation', '~> 4.0' gem 'simple_form', '~> 4.1' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'stoplight', '~> 2.1.3' -gem 'strong_migrations', '~> 0.3' +gem 'strong_migrations', '~> 0.4' gem 'tty-command', '~> 0.8', require: false -gem 'tty-prompt', '~> 0.18', require: false +gem 'tty-prompt', '~> 0.19', require: false gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2019' gem 'webpacker', '~> 4.0' @@ -96,7 +96,7 @@ gem 'rdf-normalize', '~> 0.3' group :development, :test do gem 'fabrication', '~> 2.20' - gem 'fuubar', '~> 2.3' + gem 'fuubar', '~> 2.4' gem 'i18n-tasks', '~> 0.9', require: false gem 'pry-byebug', '~> 3.7' gem 'pry-rails', '~> 0.3' @@ -108,15 +108,15 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.18' + gem 'capybara', '~> 3.24' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.9' gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false - gem 'webmock', '~> 3.5' - gem 'parallel_tests', '~> 2.28' + gem 'webmock', '~> 3.6' + gem 'parallel_tests', '~> 2.29' end group :development do @@ -128,10 +128,10 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' - gem 'rubocop', '~> 0.68', require: false + gem 'rubocop', '~> 0.71', require: false + gem 'rubocop-rails', '~> 2.0', require: false gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false - gem 'scss_lint', '~> 0.58', require: false gem 'capistrano', '~> 3.11' gem 'capistrano-rails', '~> 1.4' diff --git a/Gemfile.lock b/Gemfile.lock index 59b34a185..43d98bbdc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,20 +75,20 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) - aws-eventstream (1.0.2) - aws-partitions (1.151.0) - aws-sdk-core (3.48.4) + aws-eventstream (1.0.3) + aws-partitions (1.175.0) + aws-sdk-core (3.55.0) aws-eventstream (~> 1.0, >= 1.0.2) aws-partitions (~> 1.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.17.0) - aws-sdk-core (~> 3, >= 3.48.2) + aws-sdk-kms (1.21.0) + aws-sdk-core (~> 3, >= 3.53.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.36.1) - aws-sdk-core (~> 3, >= 3.48.2) + aws-sdk-s3 (1.42.0) + aws-sdk-core (~> 3, >= 3.53.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.0) + aws-sigv4 (~> 1.1) aws-sigv4 (1.1.0) aws-eventstream (~> 1.0, >= 1.0.2) bcrypt (3.1.12) @@ -103,7 +103,7 @@ GEM ffi (~> 1.10.0) bootsnap (1.4.4) msgpack (~> 1.0) - brakeman (4.5.0) + brakeman (4.5.1) browser (2.5.3) builder (3.2.3) bullet (6.0.0) @@ -129,13 +129,13 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.18.0) + capybara (3.24.0) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.2) + regexp_parser (~> 1.5) xpath (~> 3.2) case_transform (0.2) activesupport @@ -231,7 +231,7 @@ GEM fugit (1.1.6) et-orbi (~> 1.1, >= 1.1.6) raabro (~> 1.1) - fuubar (2.3.2) + fuubar (2.4.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) get_process_mem (0.2.3) @@ -253,7 +253,7 @@ GEM railties (>= 4.0.1) hamster (3.0.0) concurrent-ruby (~> 1.0) - hashdiff (0.3.7) + hashdiff (0.4.0) hashie (3.6.0) heapy (0.1.4) highline (2.0.1) @@ -269,7 +269,7 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_accept_language (2.1.1) - httplog (1.2.2) + httplog (1.3.1) rack (>= 1.0) rainbow (>= 2.0.0) i18n (1.6.0) @@ -320,7 +320,7 @@ GEM letter_opener (~> 1.0) railties (>= 3.2) link_header (0.0.8) - lograge (0.11.0) + lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -351,7 +351,7 @@ GEM msgpack (1.2.10) multi_json (1.13.1) multipart-post (2.0.0) - necromancer (0.4.0) + necromancer (0.5.0) net-ldap (0.16.1) net-scp (1.2.1) net-ssh (>= 2.6.5) @@ -382,7 +382,7 @@ GEM addressable (~> 2.5) http (~> 3.0) nokogiri (~> 1.8) - ox (2.10.0) + ox (2.11.0) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -393,7 +393,7 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.17.0) - parallel_tests (2.28.0) + parallel_tests (2.29.0) parallel parser (2.6.3.0) ast (~> 2.4.0) @@ -401,7 +401,7 @@ GEM equatable (~> 0.5.0) tty-color (~> 0.4.0) pg (1.1.4) - pghero (2.2.0) + pghero (2.2.1) activerecord pkg-config (1.3.7) premailer (1.11.1) @@ -420,7 +420,7 @@ GEM pry (~> 0.10) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (3.0.3) + public_suffix (3.1.0) puma (3.12.1) pundit (2.0.1) activesupport (>= 3.0.0) @@ -470,15 +470,12 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (12.3.2) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) - ffi (~> 1.0) rdf (3.0.9) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.3) rdf (>= 2.2, < 4.0) - redis (4.1.0) + redis (4.1.2) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) redis-rack (>= 1, < 3) @@ -497,7 +494,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.5.0) redis (>= 2.2, < 5) - regexp_parser (1.4.0) + regexp_parser (1.5.1) request_store (1.4.1) rack (>= 1.4) responders (2.4.1) @@ -527,31 +524,26 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.8.0) - rubocop (0.68.1) + rubocop (0.71.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.5, != 2.5.1.1) + parser (>= 2.6) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.6) - ruby-progressbar (1.10.0) + unicode-display_width (>= 1.4.0, < 1.7) + rubocop-rails (2.0.1) + rack (>= 1.1) + rubocop (>= 0.70.0) + ruby-progressbar (1.10.1) ruby-saml (1.9.0) nokogiri (>= 1.5.10) rufus-scheduler (3.5.2) fugit (~> 1.1, >= 1.1.5) - safe_yaml (1.0.4) + safe_yaml (1.0.5) sanitize (5.0.0) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - scss_lint (0.58.0) - rake (>= 0.9, < 13) - sass (~> 3.5, >= 3.5.5) sidekiq (5.2.7) connection_pool (~> 2.2, >= 2.2.2) rack (>= 1.5.0) @@ -593,8 +585,8 @@ GEM stoplight (2.1.3) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) - strong_migrations (0.3.1) - activerecord (>= 3.2.0) + strong_migrations (0.4.0) + activerecord (>= 5) temple (0.8.1) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) @@ -603,22 +595,19 @@ GEM thor (0.20.3) thread_safe (0.3.6) tilt (2.0.9) - timers (4.2.0) tty-color (0.4.3) tty-command (0.8.2) pastel (~> 0.7.0) - tty-cursor (0.6.0) - tty-prompt (0.18.1) - necromancer (~> 0.4.0) + tty-cursor (0.7.0) + tty-prompt (0.19.0) + necromancer (~> 0.5.0) pastel (~> 0.7.0) - timers (~> 4.0) - tty-cursor (~> 0.6.0) - tty-reader (~> 0.5.0) - tty-reader (0.5.0) - tty-cursor (~> 0.6.0) - tty-screen (~> 0.6.4) + tty-reader (~> 0.6.0) + tty-reader (0.6.0) + tty-cursor (~> 0.7) + tty-screen (~> 0.7) wisper (~> 2.0.0) - tty-screen (0.6.5) + tty-screen (0.7.0) twitter-text (1.14.7) unf (~> 0.1.0) tzinfo (1.2.5) @@ -628,15 +617,15 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.5) - unicode-display_width (1.5.0) + unicode-display_width (1.6.0) uniform_notifier (1.12.1) warden (1.2.8) rack (>= 2.0.6) - webmock (3.5.1) + webmock (3.6.0) addressable (>= 2.3.6) crack (>= 0.3.2) - hashdiff - webpacker (4.0.2) + hashdiff (>= 0.4.0, < 2.0.0) + webpacker (4.0.7) activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) @@ -658,7 +647,7 @@ DEPENDENCIES active_record_query_trace (~> 1.6) addressable (~> 2.6) annotate (~> 2.7) - aws-sdk-s3 (~> 1.36) + aws-sdk-s3 (~> 1.42) better_errors (~> 2.5) binding_of_caller (~> 0.7) blurhash (~> 0.1) @@ -671,7 +660,7 @@ DEPENDENCIES capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 3.18) + capybara (~> 3.24) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.4) @@ -689,7 +678,7 @@ DEPENDENCIES fastimage fog-core (<= 2.1.0) fog-openstack (~> 0.3) - fuubar (~> 2.3) + fuubar (~> 2.4) goldfinger (~> 2.1) hamlit-rails (~> 0.2) hiredis (~> 0.6) @@ -697,7 +686,7 @@ DEPENDENCIES http (~> 3.3) http_accept_language (~> 2.1) http_parser.rb (~> 0.6)! - httplog (~> 1.2) + httplog (~> 1.3) i18n-tasks (~> 0.9) idn-ruby iso-639 @@ -721,10 +710,10 @@ DEPENDENCIES omniauth-cas (~> 1.1) omniauth-saml (~> 1.10) ostatus2 (~> 2.0) - ox (~> 2.10) + ox (~> 2.11) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) - parallel_tests (~> 2.28) + parallel_tests (~> 2.29) pg (~> 1.1) pghero (~> 2.2) pkg-config (~> 1.3) @@ -748,9 +737,9 @@ DEPENDENCIES rqrcode (~> 0.10) rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) - rubocop (~> 0.68) + rubocop (~> 0.71) + rubocop-rails (~> 2.0) sanitize (~> 5.0) - scss_lint (~> 0.58) sidekiq (~> 5.2) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) @@ -762,13 +751,13 @@ DEPENDENCIES stackprof stoplight (~> 2.1.3) streamio-ffmpeg (~> 3.0) - strong_migrations (~> 0.3) + strong_migrations (~> 0.4) thor (~> 0.20) tty-command (~> 0.8) - tty-prompt (~> 0.18) + tty-prompt (~> 0.19) twitter-text (~> 1.14) tzinfo-data (~> 1.2019) - webmock (~> 3.5) + webmock (~> 3.6) webpacker (~> 4.0) webpush diff --git a/README.md b/README.md index 03c8472b9..2d18a4ee2 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ [![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases] [![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] -[![Translation status](https://weblate.joinmastodon.org/widgets/mastodon/-/svg-badge.svg)][weblate] +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker] [releases]: https://github.com/tootsuite/mastodon/releases [circleci]: https://circleci.com/gh/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon -[weblate]: https://weblate.joinmastodon.org/engage/mastodon/ +[crowdin]: https://crowdin.com/project/mastodon [docker]: https://hub.docker.com/r/tootsuite/mastodon/ Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub! diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index abc68d2a4..73a4b1859 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -46,8 +46,6 @@ class AccountsController < ApplicationController end format.json do - mark_cacheable! - render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 853f4f907..012c3c538 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -9,8 +9,6 @@ class ActivityPub::CollectionsController < Api::BaseController before_action :set_cache_headers def show - skip_session! - render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new( collection_presenter, diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 438fa226e..5147afbf7 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -10,10 +10,7 @@ class ActivityPub::OutboxesController < Api::BaseController before_action :set_cache_headers def show - unless page_requested? - skip_session! - expires_in 1.minute, public: true - end + expires_in 1.minute, public: true unless page_requested? render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e7795e95c..0c7760d77 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -48,13 +48,13 @@ module Admin def approve authorize @account.user, :approve? @account.user.approve! - redirect_to admin_accounts_path(pending: '1') + redirect_to admin_pending_accounts_path end def reject authorize @account.user, :reject? SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) - redirect_to admin_accounts_path(pending: '1') + redirect_to admin_pending_accounts_path end def unsilence @@ -127,6 +127,7 @@ module Admin :by_domain, :active, :pending, + :disabled, :silenced, :suspended, :username, diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index dd3f83389..377cac8ad 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -13,7 +13,7 @@ module Admin authorize :domain_block, :create? @domain_block = DomainBlock.new(resource_params) - existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil + existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save @@ -41,7 +41,7 @@ module Admin def destroy authorize @domain_block, :destroy? - UnblockDomainService.new.call(@domain_block, retroactive_unblock?) + UnblockDomainService.new.call(@domain_block) log_action :destroy, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg') end @@ -53,11 +53,7 @@ module Admin end def resource_params - params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive) - end - - def retroactive_unblock? - ActiveRecord::Type.lookup(:boolean).cast(resource_params[:retroactive]) + params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports) end end end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 6dd659a30..7888e844f 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -18,7 +18,7 @@ module Admin @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) - @domain_block = DomainBlock.find_by(domain: params[:id]) + @domain_block = DomainBlock.rule_for(params[:id]) end private diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb new file mode 100644 index 000000000..29c9b7107 --- /dev/null +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountActionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' } + before_action :require_staff! + before_action :set_account + + def create + account_action = Admin::AccountAction.new(resource_params) + account_action.target_account = @account + account_action.current_account = current_account + account_action.save! + + render_empty + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def resource_params + params.permit( + :type, + :report_id, + :warning_preset_id, + :text, + :send_email_notification + ) + end +end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb new file mode 100644 index 000000000..c306180ca --- /dev/null +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] + before_action :require_staff! + before_action :set_accounts, only: :index + before_action :set_account, except: :index + before_action :require_local_account!, only: [:enable, :approve, :reject] + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + local + remote + by_domain + active + pending + disabled + silenced + suspended + username + display_name + email + ip + staff + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :account, :index? + render json: @accounts, each_serializer: REST::Admin::AccountSerializer + end + + def show + authorize @account, :show? + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def enable + authorize @account.user, :enable? + @account.user.enable! + log_action :enable, @account.user + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def approve + authorize @account.user, :approve? + @account.user.approve! + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def reject + authorize @account.user, :reject? + SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsilence + authorize @account, :unsilence? + @account.unsilence! + log_action :unsilence, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsuspend + authorize @account, :unsuspend? + @account.unsuspend! + log_action :unsuspend, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + private + + def set_accounts + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_account + @account = Account.find(params[:id]) + end + + def filtered_accounts + AccountFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def require_local_account! + forbidden unless @account.local? && @account.user.present? + end +end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb new file mode 100644 index 000000000..1d48d3160 --- /dev/null +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +class Api::V1::Admin::ReportsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show] + before_action :require_staff! + before_action :set_reports, only: :index + before_action :set_report, except: :index + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + resolved + account_id + target_account_id + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :report, :index? + render json: @reports, each_serializer: REST::Admin::ReportSerializer + end + + def show + authorize @report, :show? + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def assign_to_self + authorize @report, :update? + @report.update!(assigned_account_id: current_account.id) + log_action :assigned_to_self, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def unassign + authorize @report, :update? + @report.update!(assigned_account_id: nil) + log_action :unassigned, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def reopen + authorize @report, :update? + @report.unresolve! + log_action :reopen, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def resolve + authorize @report, :update? + @report.resolve!(current_account) + log_action :resolve, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + private + + def set_reports + @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_report + @report = Report.find(params[:id]) + end + + def filtered_reports + ReportFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? + end + + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id + end + + def records_continue? + @reports.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index e2dec62af..bf3002e79 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -44,7 +44,7 @@ class Api::V1::NotificationsController < Api::BaseController end def browserable_account_notifications - current_account.notifications.browserable(exclude_types) + current_account.notifications.browserable(exclude_types, from_account) end def target_statuses_from_notifications @@ -81,6 +81,10 @@ class Api::V1::NotificationsController < Api::BaseController val end + def from_account + params[:account_id] + end + def pagination_params(core_params) params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params) end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 4f4a6858d..031e6d42d 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -1,13 +1,28 @@ # frozen_string_literal: true class Api::V1::PollsController < Api::BaseController + include Authorization + before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show + before_action :set_poll + before_action :refresh_poll respond_to :json def show - @poll = Poll.attached.find(params[:id]) - ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale? render json: @poll, serializer: REST::PollSerializer, include_results: true end + + private + + def set_poll + @poll = Poll.attached.find(params[:id]) + authorize @poll.status, :show? + rescue Mastodon::NotPermittedError + raise ActiveRecord::RecordNotFound + end + + def refresh_poll + ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale? + end end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 1a19bd0ef..1b658f870 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def data_params return {} if params[:data].blank? - params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) + params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index f9506971a..b0e134554 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -65,7 +65,7 @@ class Api::V1::StatusesController < Api::BaseController RemovalWorker.perform_async(@status.id) - render_empty + render json: @status, serializer: REST::StatusSerializer, source_requested: true end private diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index fe8e42580..d8153e082 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController favourite: alerts_enabled, reblog: alerts_enabled, mention: alerts_enabled, + poll: alerts_enabled, }, } @@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) + @data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 990aff857..9274d85a9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -152,11 +152,6 @@ class ApplicationController < ActionController::Base end def mark_cacheable! - skip_session! expires_in 0, public: true end - - def skip_session! - request.session_options[:skip] = true - end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 4f28941ae..1c422096c 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -70,7 +70,6 @@ module AccountControllerConcern def check_account_suspension if @account.suspended? - skip_session! expires_in(3.minutes, public: true) gone end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 91566c4fa..90a57197c 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -43,13 +43,7 @@ module SignatureVerification return end - account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) } - .with_fallback { nil } - .with_threshold(1) - .with_cool_off_time(5.minutes.seconds) - .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) } - - account = account_stoplight.run + account = account_from_key_id(signature_params['keyId']) if account.nil? @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}" @@ -62,13 +56,7 @@ module SignatureVerification return account unless verify_signature(account, signature, compare_signed_string).nil? - account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) } - .with_fallback { nil } - .with_threshold(1) - .with_cool_off_time(5.minutes.seconds) - .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) } - - account = account_stoplight.run + account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) } if account.nil? @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}" @@ -136,14 +124,23 @@ module SignatureVerification def account_from_key_id(key_id) if key_id.start_with?('acct:') - ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) + stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) - account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) + account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } account end end + def stoplight_wrap_request(&block) + Stoplight("source:#{request.remote_ip}", &block) + .with_fallback { nil } + .with_threshold(1) + .with_cool_off_time(5.minutes.seconds) + .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) } + .run + end + def account_refresh_key(account) return if account.local? || !account.activitypub? ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true) diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 31e501609..6e80feaf8 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true class CustomCssController < ApplicationController + skip_before_action :store_current_location + before_action :set_cache_headers def show - skip_session! render plain: Setting.custom_css || '', content_type: 'text/css' end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 5d306e600..3feb08132 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -7,8 +7,6 @@ class EmojisController < ApplicationController def show respond_to do |format| format.json do - skip_session! - render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 713365ea5..415abe10c 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -19,10 +19,7 @@ class FollowerAccountsController < ApplicationController format.json do raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? - if params[:page].blank? - skip_session! - expires_in 3.minutes, public: true - end + expires_in 3.minutes, public: true if params[:page].blank? render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 1bfd901cf..948725664 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -19,10 +19,7 @@ class FollowingAccountsController < ApplicationController format.json do raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? - if params[:page].blank? - skip_session! - expires_in 3.minutes, public: true - end + expires_in 3.minutes, public: true if params[:page].blank? render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d1bd0601e..85622a7b5 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -58,7 +58,7 @@ class HomeController < ApplicationController if request.path.start_with?('/web') new_user_session_path elsif single_user_mode? - short_account_path(Account.local.where(suspended: false).first) + short_account_path(Account.local.without_suspended.first) else about_path end diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index ac267c229..332d845d8 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ManifestsController < ApplicationController + skip_before_action :store_current_location + def show render json: InstancePresenter.new, serializer: ManifestSerializer end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 8e1624ce1..d44b52d26 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,8 +3,12 @@ class MediaController < ApplicationController include Authorization + skip_before_action :store_current_location + before_action :set_media_attachment before_action :verify_permitted_status! + before_action :check_playable, only: :player + before_action :allow_iframing, only: :player content_security_policy only: :player do |p| p.frame_ancestors(false) @@ -16,8 +20,6 @@ class MediaController < ApplicationController def player @body_classes = 'player' - response.headers['X-Frame-Options'] = 'ALLOWALL' - raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv? end private @@ -32,4 +34,12 @@ class MediaController < ApplicationController # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end + + def check_playable + not_found unless @media_attachment.larger_media_format? + end + + def allow_iframing + response.headers['X-Frame-Options'] = 'ALLOWALL' + end end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index d820b257e..8fc18dd06 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -3,6 +3,8 @@ class MediaProxyController < ApplicationController include RoutingHelper + skip_before_action :store_current_location + def show RedisLock.acquire(lock_options) do |lock| if lock.acquired? @@ -37,6 +39,6 @@ class MediaProxyController < ApplicationController end def reject_media? - DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? + DomainBlock.reject_media?(@media_attachment.account.domain) end end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index e22b4d9be..a749d8020 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -56,8 +56,4 @@ class Settings::IdentityProofsController < Settings::BaseController def post_params params.require(:account_identity_proof).permit(:post_status, :status_text) end - - def set_body_classes - @body_classes = '' - end end diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb deleted file mode 100644 index b2ce83e42..000000000 --- a/app/controllers/settings/notifications_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class Settings::NotificationsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - - def show; end - - def update - user_settings.update(user_settings_params.to_h) - - if current_user.save - redirect_to settings_notifications_path, notice: I18n.t('generic.changes_saved_msg') - else - render :show - end - end - - private - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_settings_params - params.require(:user).permit( - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), - interactions: %i(must_be_follower must_be_following must_be_following_dm) - ) - end -end diff --git a/app/controllers/settings/preferences/appearance_controller.rb b/app/controllers/settings/preferences/appearance_controller.rb new file mode 100644 index 000000000..80ea57bd2 --- /dev/null +++ b/app/controllers/settings/preferences/appearance_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::Preferences::AppearanceController < Settings::PreferencesController + private + + def after_update_redirect_path + settings_preferences_appearance_path + end +end diff --git a/app/controllers/settings/preferences/notifications_controller.rb b/app/controllers/settings/preferences/notifications_controller.rb new file mode 100644 index 000000000..a16ae6a67 --- /dev/null +++ b/app/controllers/settings/preferences/notifications_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::Preferences::NotificationsController < Settings::PreferencesController + private + + def after_update_redirect_path + settings_preferences_notifications_path + end +end diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb new file mode 100644 index 000000000..07eb89a76 --- /dev/null +++ b/app/controllers/settings/preferences/other_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Settings::Preferences::OtherController < Settings::PreferencesController + private + + def after_update_redirect_path + settings_preferences_other_path + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 5afdf0eec..110debd6e 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -12,7 +12,7 @@ class Settings::PreferencesController < Settings::BaseController if current_user.update(user_params) I18n.locale = current_user.locale - redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg') + redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') else render :show end @@ -20,6 +20,10 @@ class Settings::PreferencesController < Settings::BaseController private + def after_update_redirect_path + settings_preferences_path + end + def user_settings UserSettingsDecorator.new(current_user) end @@ -49,8 +53,9 @@ class Settings::PreferencesController < Settings::BaseController :setting_hide_network, :setting_aggregate_reblogs, :setting_show_application, + :setting_advanced_layout, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), - interactions: %i(must_be_follower must_be_following) + interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index fc44d5fb1..ef26691b2 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -27,7 +27,7 @@ class StatusesController < ApplicationController def show respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 10.seconds, public: true if current_account.nil? @body_classes = 'with-modals' @@ -38,8 +38,6 @@ class StatusesController < ApplicationController end format.json do - mark_cacheable! unless @stream_entry.hidden? - render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) end @@ -48,8 +46,6 @@ class StatusesController < ApplicationController end def activity - skip_session! - render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) end @@ -58,7 +54,6 @@ class StatusesController < ApplicationController def embed raise ActiveRecord::RecordNotFound if @status.hidden? - skip_session! expires_in 180, public: true response.headers['X-Frame-Options'] = 'ALLOWALL' @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) @@ -67,8 +62,6 @@ class StatusesController < ApplicationController end def replies - skip_session! - render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 8568b151c..0f7e9e0f5 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -15,14 +15,13 @@ class StreamEntriesController < ApplicationController def show respond_to do |format| format.html do - redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status' + expires_in 5.minutes, public: true unless @stream_entry.hidden? + + redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) end format.atom do - unless @stream_entry.hidden? - skip_session! - expires_in 3.minutes, public: true - end + expires_in 3.minutes, public: true unless @stream_entry.hidden? render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) end @@ -50,7 +49,7 @@ class StreamEntriesController < ApplicationController def set_stream_entry @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id]) - @type = @stream_entry.activity_type.downcase + @type = 'status' raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? authorize @stream_entry.activity, :show? if @stream_entry.hidden? diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index d56efbfb9..02a860a74 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -16,24 +16,32 @@ module StreamEntriesHelper if user_signed_in? if account.id == current_user.account_id link_to settings_profile_url, class: 'button logo-button' do - safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('settings.edit_profile')]) + safe_join([svg_logo, t('settings.edit_profile')]) end elsif current_account.following?(account) || current_account.requested?(account) link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')]) + safe_join([svg_logo, t('accounts.unfollow')]) end elsif !(account.memorial? || account.moved?) link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) + safe_join([svg_logo, t('accounts.follow')]) end end elsif !(account.memorial? || account.moved?) link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')]) + safe_join([svg_logo, t('accounts.follow')]) end end end + def svg_logo + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') + end + + def svg_logo_full + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') + end + def account_badge(account, all: false) if account.bot? content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') diff --git a/app/javascript/images/logo_full.svg b/app/javascript/images/logo_full.svg index c33883342..03bcf93e3 100644 --- a/app/javascript/images/logo_full.svg +++ b/app/javascript/images/logo_full.svg @@ -1 +1 @@ - + diff --git a/app/javascript/images/logo_transparent.svg b/app/javascript/images/logo_transparent.svg index abd6d1f67..a1e7b403e 100644 --- a/app/javascript/images/logo_transparent.svg +++ b/app/javascript/images/logo_transparent.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index b2c7ab76a..ef2500e7b 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -8,6 +8,7 @@ const messages = defineMessages({ export const ALERT_SHOW = 'ALERT_SHOW'; export const ALERT_DISMISS = 'ALERT_DISMISS'; export const ALERT_CLEAR = 'ALERT_CLEAR'; +export const ALERT_NOOP = 'ALERT_NOOP'; export function dismissAlert(alert) { return { @@ -36,7 +37,7 @@ export function showAlertForError(error) { if (status === 404 || status === 410) { // Skip these errors as they are reflected in the UI - return {}; + return { type: ALERT_NOOP }; } let message = statusText; diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 0ee663766..300fb48a9 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -63,6 +63,14 @@ const messages = defineMessages({ uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, }); +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'); + } +}; + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) { status: status, }); - if (!getState().getIn(['compose', 'mounted'])) { - routerHistory.push('/statuses/new'); - } + ensureComposeIsVisible(getState, routerHistory); }; }; @@ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) { account: account, }); - if (!getState().getIn(['compose', 'mounted'])) { - routerHistory.push('/statuses/new'); - } + ensureComposeIsVisible(getState, routerHistory); }; }; @@ -115,9 +119,7 @@ export function directCompose(account, routerHistory) { account: account, }); - if (!getState().getIn(['compose', 'mounted'])) { - routerHistory.push('/statuses/new'); - } + ensureComposeIsVisible(getState, routerHistory); }; }; @@ -383,7 +385,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) { }; }; -export function selectComposeSuggestion(position, token, suggestion) { +export function selectComposeSuggestion(position, token, suggestion, path) { return (dispatch, getState) => { let completion, startPosition; @@ -405,6 +407,7 @@ export function selectComposeSuggestion(position, token, suggestion) { position: startPosition, token, completion, + path, }); }; }; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index b0861fc6b..88788eec9 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -48,9 +48,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; if (notification.type === 'mention') { + const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); const regex = regexFromFilters(filters); const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + if (dropRegex && dropRegex.test(searchIndex)) { + return; + } + filtered = regex && regex.test(searchIndex); } diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 1794538e2..06a19afc3 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; +import { ensureComposeIsVisible } from './compose'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -131,14 +132,15 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status) { +export function redraft(status, raw_text) { return { type: REDRAFT, status, + raw_text, }; }; -export function deleteStatus(id, router, withRedraft = false) { +export function deleteStatus(id, routerHistory, withRedraft = false) { return (dispatch, getState) => { let status = getState().getIn(['statuses', id]); @@ -148,17 +150,14 @@ export function deleteStatus(id, router, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api(getState).delete(`/api/v1/statuses/${id}`).then(() => { + api(getState).delete(`/api/v1/statuses/${id}`).then(response => { evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); if (withRedraft) { - dispatch(redraft(status)); - - if (!getState().getIn(['compose', 'mounted'])) { - router.push('/statuses/new'); - } + dispatch(redraft(status, response.data.text)); + ensureComposeIsVisible(getState, routerHistory); } }).catch(error => { dispatch(deleteStatusFail(id, error)); diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js new file mode 100644 index 000000000..c7d965b53 --- /dev/null +++ b/app/javascript/mastodon/components/autosuggest_input.js @@ -0,0 +1,229 @@ +import React from 'react'; +import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; +import AutosuggestEmoji from './autosuggest_emoji'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { isRtl } from '../rtl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import classNames from 'classnames'; +import { List as ImmutableList } from 'immutable'; + +const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { + let word; + + let left = str.slice(0, caretPosition).search(/\S+$/); + let right = str.slice(caretPosition).search(/\s/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) { + return [null, null]; + } + + word = word.trim().toLowerCase(); + + if (word.length > 0) { + return [left + 1, word]; + } else { + return [null, null]; + } +}; + +export default class AutosuggestInput extends ImmutablePureComponent { + + static propTypes = { + value: PropTypes.string, + suggestions: ImmutablePropTypes.list, + disabled: PropTypes.bool, + placeholder: PropTypes.string, + onSuggestionSelected: PropTypes.func.isRequired, + onSuggestionsClearRequested: PropTypes.func.isRequired, + onSuggestionsFetchRequested: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyUp: PropTypes.func, + onKeyDown: PropTypes.func, + autoFocus: PropTypes.bool, + className: PropTypes.string, + id: PropTypes.string, + searchTokens: PropTypes.arrayOf(PropTypes.string), + maxLength: PropTypes.number, + }; + + static defaultProps = { + autoFocus: true, + searchTokens: ImmutableList(['@', ':', '#']), + }; + + state = { + suggestionsHidden: true, + focused: false, + selectedSuggestion: 0, + lastToken: null, + tokenStart: 0, + }; + + onChange = (e) => { + const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens); + + if (token !== null && this.state.lastToken !== token) { + this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); + this.props.onSuggestionsFetchRequested(token); + } else if (token === null) { + this.setState({ lastToken: null }); + this.props.onSuggestionsClearRequested(); + } + + this.props.onChange(e); + } + + onKeyDown = (e) => { + const { suggestions, disabled } = this.props; + const { selectedSuggestion, suggestionsHidden } = this.state; + + if (disabled) { + e.preventDefault(); + return; + } + + if (e.which === 229 || e.isComposing) { + // Ignore key events during text composition + // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac) + return; + } + + switch(e.key) { + case 'Escape': + if (suggestions.size === 0 || suggestionsHidden) { + document.querySelector('.ui').parentElement.focus(); + } else { + e.preventDefault(); + this.setState({ suggestionsHidden: true }); + } + + break; + case 'ArrowDown': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); + } + + break; + case 'ArrowUp': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); + } + + break; + case 'Enter': + case 'Tab': + // Select suggestion + if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + e.stopPropagation(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + } + + break; + } + + if (e.defaultPrevented || !this.props.onKeyDown) { + return; + } + + this.props.onKeyDown(e); + } + + onBlur = () => { + this.setState({ suggestionsHidden: true, focused: false }); + } + + onFocus = () => { + this.setState({ focused: true }); + } + + onSuggestionClick = (e) => { + const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); + e.preventDefault(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); + this.input.focus(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { + this.setState({ suggestionsHidden: false }); + } + } + + setInput = (c) => { + this.input = c; + } + + renderSuggestion = (suggestion, i) => { + const { selectedSuggestion } = this.state; + let inner, key; + + if (typeof suggestion === 'object') { + inner = ; + key = suggestion.id; + } else if (suggestion[0] === '#') { + inner = suggestion; + key = suggestion; + } else { + inner = ; + key = suggestion; + } + + return ( +
+ {inner} +
+ ); + } + + render () { + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; + const { suggestionsHidden } = this.state; + const style = { direction: 'ltr' }; + + if (isRtl(value)) { + style.direction = 'rtl'; + } + + return ( +
+ + +
+ {suggestions.map(this.renderSuggestion)} +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index a4f5cf50c..b070fe3e5 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -55,7 +55,8 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { }; state = { - suggestionsHidden: false, + suggestionsHidden: true, + focused: false, selectedSuggestion: 0, lastToken: null, tokenStart: 0, @@ -134,7 +135,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } onBlur = () => { - this.setState({ suggestionsHidden: true }); + this.setState({ suggestionsHidden: true, focused: false }); + } + + onFocus = (e) => { + this.setState({ focused: true }); + if (this.props.onFocus) { + this.props.onFocus(e); + } } onSuggestionClick = (e) => { @@ -145,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } } @@ -184,7 +192,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; const { suggestionsHidden } = this.state; const style = { direction: 'ltr' }; @@ -192,33 +200,39 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { style.direction = 'rtl'; } - return ( -
-