diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c3d96ba4a..d47c9bc168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ All notable changes to this project will be documented in this file. +## |4.2.11] - 2024-08-16 + +### Added + +- Add support for incoming `` tag ([mediaformat](https://github.com/mastodon/mastodon/pull/31375)) + +### Changed + +- Change logic of block/mute bypass for mentions from moderators to only apply to visible roles with moderation powers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31271)) + +### Fixed + +- Fix incorrect rate limit on PUT requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31356)) +- Fix presence of `ß` in adjacent word preventing mention and hashtag matching ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31122)) +- Fix processing of webfinger responses with multiple `self` links ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31110)) +- Fix duplicate `orderedItems` in user archive's `outbox.json` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31099)) +- Fix click event handling when clicking outside of an open dropdown menu ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31251)) +- Fix status processing failing halfway when a remote post has a malformed `replies` attribute ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31246)) +- Fix `--verbose` option of `tootctl media remove`, which was previously erroneously removed ([mjankowski](https://github.com/mastodon/mastodon/pull/30536)) +- Fix division by zero on some video/GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30600)) +- Fix Web UI trying to save user settings despite being logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30324)) +- Fix hashtag regexp matching some link anchors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30190)) +- Fix local account search on LDAP login being case-sensitive ([raucao](https://github.com/mastodon/mastodon/pull/30113)) +- Fix development environment admin account not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29958)) +- Fix report reason selector in moderation interface not unselecting rules when changing category ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29026)) +- Fix already-invalid reports failing to resolve ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29027)) +- Fix OCR when using S3/CDN for assets ([vmstan](https://github.com/mastodon/mastodon/pull/28551)) +- Fix error when encountering malformed `Tag` objects from Kbin ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28235)) +- Fix not all allowed image formats showing in file picker when uploading custom emoji ([june128](https://github.com/mastodon/mastodon/pull/28076)) +- Fix search popout listing unusable search options when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27918)) +- Fix processing of featured collections lacking an `items` attribute ([tribela](https://github.com/mastodon/mastodon/pull/27581)) +- Fix `mastodon:stats` decoration of stats rake task ([mjankowski](https://github.com/mastodon/mastodon/pull/31104)) + ## [4.2.10] - 2024-07-04 ### Security diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx index fd137115cb..752a3ed060 100644 --- a/app/javascript/mastodon/components/dropdown_menu.jsx +++ b/app/javascript/mastodon/components/dropdown_menu.jsx @@ -39,6 +39,7 @@ class DropdownMenu extends PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); e.stopPropagation(); + e.preventDefault(); } }; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 9bebe0bbe3..1163e4c30e 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -433,13 +433,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def fetch_replies(status) collection = @object['replies'] - return if collection.nil? + return if collection.blank? replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id]) return unless replies.nil? uri = value_or_id(collection) ActivityPub::FetchRepliesWorker.perform_async(status.id, uri, { 'request_id' => @options[:request_id] }) unless uri.nil? + rescue => e + Rails.logger.warn "Error fetching replies: #{e}" end def conversation_from_activity diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 098b6296fb..5b9437eb8d 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -20,6 +20,6 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields] serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options) - { '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash) + { '@context': serialized_context(named_contexts, context_extensions) }.merge(serialized_hash) end end diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb index ae8a3b1eae..01a5dbc21d 100644 --- a/app/lib/webfinger.rb +++ b/app/lib/webfinger.rb @@ -6,6 +6,8 @@ class Webfinger class RedirectError < Error; end class Response + ACTIVITYPUB_READY_TYPE = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze + attr_reader :uri def initialize(uri, body) @@ -20,17 +22,28 @@ class Webfinger end def link(rel, attribute) - links.dig(rel, attribute) + links.dig(rel, 0, attribute) + end + + def self_link_href + self_link.fetch('href') end private def links - @links ||= @json['links'].index_by { |link| link['rel'] } + @links ||= @json.fetch('links', []).group_by { |link| link['rel'] } + end + + def self_link + links.fetch('self', []).find do |link| + ACTIVITYPUB_READY_TYPE.include?(link['type']) + end end def validate_response! raise Webfinger::Error, "Missing subject in response for #{@uri}" if subject.blank? + raise Webfinger::Error, "Missing self link in response for #{@uri}" if self_link.blank? end end diff --git a/app/models/account.rb b/app/models/account.rb index d9713dbb42..215a7b3df6 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -72,7 +72,7 @@ class Account < ApplicationRecord INSTANCE_ACTOR_ID = -99 USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i - MENTION_RE = %r{(? e raise Error, e.message rescue Webfinger::Error => e diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 2cf8302e55..8bc90968b7 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -19,8 +19,8 @@ class BackupService < BaseService def build_outbox_json!(file) skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) - skeleton['@context'] = full_context - skeleton['orderedItems'] = ['!PLACEHOLDER!'] + skeleton[:@context] = full_context + skeleton[:orderedItems] = ['!PLACEHOLDER!'] skeleton = Oj.dump(skeleton) prepend, append = skeleton.split('"!PLACEHOLDER!"') add_comma = false @@ -33,7 +33,7 @@ class BackupService < BaseService file.write(statuses.map do |status| item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status, use_bearcap: false), ActivityPub::ActivitySerializer) - item.delete('@context') + item.delete(:@context) unless item[:type] == 'Announce' || item[:object][:attachment].blank? item[:object][:attachment].each do |attachment| diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 533355a2af..ab1b15b56b 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -52,7 +52,7 @@ class NotifyService < BaseService end def from_staff? - @sender.local? && @sender.user.present? && @sender.user_role&.overrides?(@recipient.user_role) + @sender.local? && @sender.user.present? && @sender.user_role&.overrides?(@recipient.user_role) && @sender.user_role&.highlighted? && @sender.user_role&.can?(*UserRole::Flags::CATEGORIES[:moderation]) end def from_self? diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 078a0423f2..8a5863baba 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -106,8 +106,6 @@ class ResolveAccountService < BaseService end def fetch_account! - return unless activitypub_ready? - with_redis_lock("resolve:#{@username}@#{@domain}") do @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors]) end @@ -122,12 +120,8 @@ class ResolveAccountService < BaseService @options[:skip_cache] || @account.nil? || @account.possibly_stale? end - def activitypub_ready? - ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type')) - end - def actor_url - @actor_url ||= @webfinger.link('self', 'href') + @actor_url ||= @webfinger.self_link_href end def gone_from_origin? diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index a3c2b821cf..c0c55f28f2 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -144,7 +144,7 @@ class Rack::Attack API_CREATE_EMOJI_REACTION_REGEX = %r{\A/api/v1/statuses/\d+/emoji_reactions} throttle('throttle_password_change/account', limit: 10, period: 10.minutes) do |req| - req.warden_user_id if (req.put? && !req.path.match?(API_CREATE_EMOJI_REACTION_REGEX)) || (req.patch? && req.path_matches?('/auth')) + req.warden_user_id if (req.put? || req.patch?) && (req.path_matches?('/auth') || req.path_matches?('/auth/password')) end self.throttled_responder = lambda do |request| diff --git a/docker-compose.yml b/docker-compose.yml index 7a6f9be509..1c4ab536a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.10 + image: ghcr.io/mastodon/mastodon:v4.2.11 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -79,7 +79,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.2.10 + image: ghcr.io/mastodon/mastodon:v4.2.11 restart: always env_file: .env.production command: node ./streaming @@ -97,7 +97,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.10 + image: ghcr.io/mastodon/mastodon:v4.2.11 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/install/13.0/setup-imagemagick-7.sh b/install/13.0/setup-imagemagick-7.sh new file mode 100644 index 0000000000..6a769001dd --- /dev/null +++ b/install/13.0/setup-imagemagick-7.sh @@ -0,0 +1,82 @@ + +cat << EOF + +================ [imagemagick 7 setup script] ==================== +Remove old ImageMagick + +EOF + +apt remove -y imagemagick +apt autoremove -y + +cat << EOF + +================ [imagemagick 7 setup script] ==================== +Download source + +EOF + +git clone https://github.com/ImageMagick/ImageMagick.git ImageMagick +cd ImageMagick +git checkout $(git tag -l | grep -E '^7' | sort -V | tail -n 1) + +cat << EOF + +================ [imagemagick 7 setup script] ==================== +Install dependent packages + +EOF + +apt update +apt install -y \ + libjpeg-dev libpng-dev libpng16-16 libltdl-dev libheif-dev libraw-dev libtiff-dev libopenjp2-tools \ + libopenjp2-7-dev libjpeg-turbo-progs libfreetype6-dev libheif-dev libfreetype6-dev libopenexr-dev \ + libwebp-dev libgif-dev + +cat << EOF + +================ [imagemagick 7 setup script] ==================== +Configure + +EOF + +./configure --with-modules --enable-file-type --with-quantum-depth=32 --with-jpeg=yes --with-png=yes \ + --with-gif=yes --with-webp=yes --with-heic=yes --with-raw=yes --with-tiff=yes --with-openjp2 \ + --with-freetype=yes --with-webp=yes --with-openexr=yes --with-gslib=yes --with-gif=yes --with-perl=yes \ + --with-jxl=yes + +cat << EOF + +================ [imagemagick 7 setup script] ==================== +Make + +EOF + +make + +cat << EOF + +================ [imagemagick 7 setup script] ==================== +Make install + +EOF + +make install +ldconfig /usr/local/lib + +cat << EOF + +=========== [imagemagick 7 setup script completed] =============== +ImageMagick 7 setup is completed! +Please check AVIF format on your Mastodon. + +To check ImageMagick version: + exec bash + convert -version + +Or + sudo su - mastodon + convert -version + +EOF + diff --git a/install/13.0/setup1.sh b/install/13.0/setup1.sh new file mode 100755 index 0000000000..e5e248fcf5 --- /dev/null +++ b/install/13.0/setup1.sh @@ -0,0 +1,209 @@ +VERSION=13.0 + +cat << EOF + +Hello, new kmyblue admin. + +================== [kmyblue setup script 1] ====================== +INPUT kmyblue version for install + + - lts : [RECOMMENDED] The long time support version + - latest: The latest version + + - debug : [deprecated] The version in development + - abort : Abort the setup script + +EOF + +KMYBLUE_VERSION=unset +until [ "$KMYBLUE_VERSION" == "lts" ] || [ "$KMYBLUE_VERSION" == "latest" ] || [ "$KMYBLUE_VERSION" == "debug" ] || [ "$KMYBLUE_VERSION" == "abort" ] +do + echo -n "kmyblue version for install [lts/latest/debug/abort]: " + read KMYBLUE_VERSION +done + +if [ "$KMYBLUE_VERSION" == "abort" ]; then + echo Good bye. + exit +fi + +cat << EOF + +================== [kmyblue setup script 1] ====================== +apt updates and upgrades + +EOF + +apt update && apt upgrade -y + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Install basis softwares + +EOF + +apt install -y curl wget gnupg apt-transport-https lsb-release ca-certificates + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Install Node.js + +EOF + +# Node.js +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +sudo apt-get update && sudo apt-get install nodejs -y + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Install PostgreSQL + +EOF + +# PostgreSQL +wget -O /usr/share/keyrings/postgresql.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc +echo "deb [signed-by=/usr/share/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/postgresql.list + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Install packages + +EOF + +# 必要なパッケージをまとめてインストール +apt update +apt install -y \ + imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev file git-core \ + g++ libprotobuf-dev protobuf-compiler pkg-config nodejs gcc autoconf \ + bison build-essential libssl-dev libyaml-dev libreadline6-dev \ + zlib1g-dev libncurses5-dev libffi-dev libgdbm-dev \ + nginx redis-server redis-tools postgresql postgresql-contrib \ + certbot python3-certbot-nginx libidn11-dev libicu-dev libjemalloc-dev + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Initialize yarn + +EOF + +corepack enable +yarn set version classic + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Install requested package + +EOF + +# Mastodonパッケージにもnode-gypは入ってるけど、npmのほうからグローバルにインストールしないと +# yarn installで一部のOptionalパッケージインストール時にエラーが出てしまう様子 +npm i -g node-gyp + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Add mastodon user + +Input user information (No need to type) + +EOF + +# mastodonユーザーを追加 +adduser --disabled-login mastodon + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Create PostgreSQL mastodon user + +EOF + +# PostgreSQLにmastodonユーザーを追加 +sudo -u postgres psql << EOF + CREATE USER mastodon WITH PASSWORD 'ohagi' CREATEDB; +EOF + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Download kmyblue + +EOF + +# kmyblueソースコードをダウンロード +# 続きのシェルスクリプトをgit管理外にコピーし権限を与える +su - mastodon <> ~/.bashrc +echo 'eval "\$(rbenv init -)"' >> ~/.bashrc +EOF + +cat << EOF + +================== [kmyblue setup script 1] ====================== +Copy setting files and services + +EOF + +# これを設定しておかないと、Web表示時にNginxがPermission Errorを起こす +chmod o+x /home/mastodon + +# 必要なファイルをコピー +cp /home/mastodon/live/dist/nginx.conf /etc/nginx/sites-available/mastodon +ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/mastodon +cp /home/mastodon/live/dist/mastodon-*.service /etc/systemd/system/ +systemctl daemon-reload + +# --------------------------------------------------- + +cat << EOF + +============== [kmyblue setup script 1 completed] ================ + +Input this command to continue setup: + sudo su - mastodon + ./setup2.sh + +EOF diff --git a/install/13.0/setup2.sh b/install/13.0/setup2.sh new file mode 100644 index 0000000000..96d2309de7 --- /dev/null +++ b/install/13.0/setup2.sh @@ -0,0 +1,68 @@ +cat << EOF + +================== [kmyblue setup script 2] ====================== +Install Ruby + +EOF + +git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build +RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install $(cat /home/mastodon/live/.ruby-version) +rbenv global $(cat /home/mastodon/live/.ruby-version) + +cat << EOF + +================== [kmyblue setup script 2] ====================== +Install Ruby bundler + +EOF + +gem install bundler --no-document + +cd ~/live + +cat << EOF + +================== [kmyblue setup script 2] ====================== +Install yarn packages + +EOF + +yarn install + +cat << EOF + +================== [kmyblue setup script 2] ====================== +Install bundle packages + +EOF + +bundle config deployment 'true' +bundle config without 'development test' +bundle install -j$(getconf _NPROCESSORS_ONLN) + +# --------------------------------------------------- + +cat << EOF + +============== [kmyblue setup script 2 completed] ================ + +PostgreSQL and Redis are now available on localhost. + +* PostgreSQL + host : /var/run/postgresql + user : mastodon + database : mastodon_production + password : ohagi + +* Redis + host : localhost + password is empty + +[IMPORTANT] Check PostgreSQL password before setup! + +Input this command to finish setup: + cd live + RAILS_ENV=production bundle exec rake mastodon:setup + +EOF + diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 2325cacaf7..0abb6053a9 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -9,13 +9,13 @@ module Mastodon end def kmyblue_minor - 0 + 1 end def kmyblue_flag # 'LTS' - 'dev' - # nil + # 'dev' + nil end def major diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 4a78d57532..2fed92759a 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -88,7 +88,7 @@ class Sanitize end MASTODON_STRICT = freeze_config( - elements: %w(p br span a del pre blockquote code b strong u i em ul ol li ruby rt rp), + elements: %w(p br span a del s pre blockquote code b strong u i em ul ol li ruby rt rp), attributes: { 'a' => %w(href rel class translate), diff --git a/lib/tasks/statistics.rake b/lib/tasks/statistics.rake index dde7890f6b..82840f4fdc 100644 --- a/lib/tasks/statistics.rake +++ b/lib/tasks/statistics.rake @@ -9,11 +9,13 @@ namespace :mastodon do [ ['App Libraries', 'app/lib'], %w(Presenters app/presenters), + %w(Policies app/policies), + %w(Serializers app/serializers), %w(Services app/services), %w(Validators app/validators), %w(Workers app/workers), ].each do |name, dir| - STATS_DIRECTORIES << [name, Rails.root.join(dir)] + STATS_DIRECTORIES << [name, dir] end end end diff --git a/spec/fixtures/requests/activitypub-webfinger.txt b/spec/fixtures/requests/activitypub-webfinger.txt index 465066d84e..733b1693dc 100644 --- a/spec/fixtures/requests/activitypub-webfinger.txt +++ b/spec/fixtures/requests/activitypub-webfinger.txt @@ -4,4 +4,4 @@ Content-Type: application/jrd+json; charset=utf-8 X-Content-Type-Options: nosniff Date: Sun, 17 Sep 2017 06:22:50 GMT -{"subject":"acct:foo@ap.example.com","aliases":["https://ap.example.com/@foo","https://ap.example.com/users/foo"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://ap.example.com/@foo"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://ap.example.com/users/foo.atom"},{"rel":"self","type":"application/activity+json","href":"https://ap.example.com/users/foo"},{"rel":"salmon","href":"https://ap.example.com/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.u3L4vnpNLzVH31MeWI394F0wKeJFsLDAsNXGeOu0QF2x-h1zLWZw_agqD2R3JPU9_kaDJGPIV2Sn5zLyUA9S6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh8lDET6X4Pyw-ZJU0_OLo_41q9w-OrGtlsTm_PuPIeXnxa6BLqnDaxC-4IcjG_FiPahNCTINl_1F_TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq-t8nhQYkgAkt64euWpva3qL5KD1mTIZQEP-LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3QvuHQ==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://ap.example.com/authorize_follow?acct={uri}"}]} \ No newline at end of file +{"subject":"acct:foo@ap.example.com","aliases":["https://ap.example.com/@foo","https://ap.example.com/users/foo"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://ap.example.com/@foo"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://ap.example.com/users/foo.atom"},{"rel":"self","type":"application/html","href":"https://ap.example.com/users/foo.html"},{"rel":"self","type":"application/activity+json","href":"https://ap.example.com/users/foo"},{"rel":"self","type":"application/json","href":"https://ap.example.com/users/foo.json"},{"rel":"salmon","href":"https://ap.example.com/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.u3L4vnpNLzVH31MeWI394F0wKeJFsLDAsNXGeOu0QF2x-h1zLWZw_agqD2R3JPU9_kaDJGPIV2Sn5zLyUA9S6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh8lDET6X4Pyw-ZJU0_OLo_41q9w-OrGtlsTm_PuPIeXnxa6BLqnDaxC-4IcjG_FiPahNCTINl_1F_TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq-t8nhQYkgAkt64euWpva3qL5KD1mTIZQEP-LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3QvuHQ==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://ap.example.com/authorize_follow?acct={uri}"}]} \ No newline at end of file diff --git a/spec/fixtures/requests/webfinger.txt b/spec/fixtures/requests/webfinger.txt index f337ecae6f..fce821bddb 100644 --- a/spec/fixtures/requests/webfinger.txt +++ b/spec/fixtures/requests/webfinger.txt @@ -8,4 +8,4 @@ Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; -{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} +{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"self","type":"application/activity+json","href":"https://ap.example.com/users/foo"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index d9cd0494f0..f212d44f60 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1109,7 +1109,7 @@ RSpec.describe ActivityPub::Activity::Create do inbox: 'https://foo.test/inbox', }.with_indifferent_access end - let!(:webfinger) { { subject: 'acct:actor@foo.test', links: [{ rel: 'self', href: 'https://foo.test' }] } } + let!(:webfinger) { { subject: 'acct:actor@foo.test', links: [{ rel: 'self', href: 'https://foo.test', type: 'application/activity+json' }] } } let(:object_json) do { diff --git a/spec/lib/activitypub/adapter_spec.rb b/spec/lib/activitypub/adapter_spec.rb index 7d8d703ec2..5ecdfe8120 100644 --- a/spec/lib/activitypub/adapter_spec.rb +++ b/spec/lib/activitypub/adapter_spec.rb @@ -59,7 +59,7 @@ RSpec.describe ActivityPub::Adapter do let(:serializer_class) { TestWithBasicContextSerializer } it 'renders a basic @context' do - expect(subject).to include({ '@context' => 'https://www.w3.org/ns/activitystreams' }) + expect(subject).to include({ '@context': 'https://www.w3.org/ns/activitystreams' }) end end @@ -67,7 +67,7 @@ RSpec.describe ActivityPub::Adapter do let(:serializer_class) { TestWithNamedContextSerializer } it 'renders a @context with both items' do - expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) + expect(subject).to include({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) end end @@ -75,7 +75,7 @@ RSpec.describe ActivityPub::Adapter do let(:serializer_class) { TestWithNestedNamedContextSerializer } it 'renders a @context with both items' do - expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) + expect(subject).to include({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) end end @@ -83,7 +83,7 @@ RSpec.describe ActivityPub::Adapter do let(:serializer_class) { TestWithContextExtensionSerializer } it 'renders a @context with the extension' do - expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'sensitive' => 'as:sensitive' }] }) + expect(subject).to include({ '@context': ['https://www.w3.org/ns/activitystreams', { 'sensitive' => 'as:sensitive' }] }) end end @@ -91,7 +91,7 @@ RSpec.describe ActivityPub::Adapter do let(:serializer_class) { TestWithNestedContextExtensionSerializer } it 'renders a @context with both extensions' do - expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive' }] }) + expect(subject).to include({ '@context': ['https://www.w3.org/ns/activitystreams', { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive' }] }) end end end diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb new file mode 100644 index 0000000000..5015deac7f --- /dev/null +++ b/spec/lib/webfinger_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Webfinger do + describe 'self link' do + context 'when self link is specified with type application/activity+json' do + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } + + it 'correctly parses the response' do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + + response = described_class.new('acct:alice@example.com').perform + + expect(response.self_link_href).to eq 'https://example.com/alice' + end + end + + context 'when self link is specified with type application/ld+json' do + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }] } } + + it 'correctly parses the response' do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + + response = described_class.new('acct:alice@example.com').perform + + expect(response.self_link_href).to eq 'https://example.com/alice' + end + end + + context 'when self link is specified with incorrect type' do + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/json"' }] } } + + it 'raises an error' do + stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + + expect { described_class.new('acct:alice@example.com').perform }.to raise_error(Webfinger::Error) + end + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 2851bbe83c..b91cbe915e 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -934,6 +934,14 @@ RSpec.describe Account do it 'does not match URL query string' do expect(subject.match('https://example.com/?x=@alice')).to be_nil end + + it 'matches usernames immediately following the letter ß' do + expect(subject.match('Hello toß @alice from me')[1]).to eq 'alice' + end + + it 'matches usernames containing uppercase characters' do + expect(subject.match('Hello to @aLice@Example.com from me')[1]).to eq 'aLice@Example.com' + end end describe 'validations' do diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 7799afe44b..a3cae4a339 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -95,6 +95,14 @@ RSpec.describe Tag do it 'does not match purely-numeric hashtags' do expect(subject.match('hello #0123456')).to be_nil end + + it 'matches hashtags immediately following the letter ß' do + expect(subject.match('Hello toß #ruby').to_s).to eq '#ruby' + end + + it 'matches hashtags containing uppercase characters' do + expect(subject.match('Hello #rubyOnRails').to_s).to eq '#rubyOnRails' + end end describe '#to_param' do diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index 60ee879e4e..43d8148748 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end context 'when the account does not have a inbox' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do actor[:inbox] = nil @@ -52,7 +52,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end context 'when URI and WebFinger share the same host' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) @@ -74,7 +74,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end context 'when WebFinger presents different domain than URI' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) @@ -98,7 +98,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end context 'when WebFinger returns a different URI' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) @@ -115,7 +115,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end context 'when WebFinger returns a different URI after a redirection' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } + let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index ce0a8534f1..9872c5cb4d 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do end context 'when the account does not have a inbox' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do actor[:inbox] = nil @@ -52,7 +52,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do end context 'when URI and WebFinger share the same host' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) @@ -74,7 +74,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do end context 'when WebFinger presents different domain than URI' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) @@ -98,7 +98,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do end context 'when WebFinger returns a different URI' do - let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } + let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) @@ -115,7 +115,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do end context 'when WebFinger returns a different URI after a redirection' do - let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } + let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob', type: 'application/activity+json' }] } } before do stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' }) diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index ae12ee43d6..563052c072 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteKeyService do subject { described_class.new } - let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } let(:public_key_pem) do <<~TEXT diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index f801227f57..64c6785657 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -601,7 +601,7 @@ RSpec.describe ActivityPub::ProcessAccountService do }.with_indifferent_access webfinger = { subject: "acct:user#{i}@foo.test", - links: [{ rel: 'self', href: "https://foo.test/users/#{i}" }], + links: [{ rel: 'self', href: "https://foo.test/users/#{i}", type: 'application/activity+json' }], }.with_indifferent_access stub_request(:get, "https://foo.test/users/#{i}").to_return(status: 200, body: actor_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) stub_request(:get, "https://foo.test/users/#{i}/featured").to_return(status: 200, body: featured_json.to_json, headers: { 'Content-Type': 'application/activity+json' }) diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index 74b23f85c2..1434df49db 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -62,6 +62,7 @@ RSpec.describe BackupService do aggregate_failures do expect(body.scan('@context').count).to eq 1 + expect(body.scan('orderedItems').count).to eq 1 expect(json['@context']).to_not be_nil expect(json['type']).to eq 'OrderedCollection' expect(json['totalItems']).to eq 4 diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index 8c810f1c32..c639827a61 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -129,6 +129,73 @@ RSpec.describe NotifyService do end end + context 'when the blocked sender has a role' do + let(:sender) { Fabricate(:user, role: sender_role).account } + let(:activity) { Fabricate(:mention, status: Fabricate(:status, account: sender)) } + let(:type) { :mention } + + before do + recipient.block!(sender) + end + + context 'when the role is a visible moderator' do + let(:sender_role) { Fabricate(:user_role, highlighted: true, permissions: UserRole::FLAGS[:manage_users]) } + + it 'does notify' do + expect { subject }.to change(Notification, :count) + end + end + + context 'when the role is a non-visible moderator' do + let(:sender_role) { Fabricate(:user_role, highlighted: false, permissions: UserRole::FLAGS[:manage_users]) } + + it 'does not notify' do + expect { subject }.to_not change(Notification, :count) + end + end + + context 'when the role is a visible non-moderator' do + let(:sender_role) { Fabricate(:user_role, highlighted: true) } + + it 'does not notify' do + expect { subject }.to_not change(Notification, :count) + end + end + end + + context 'with filtered notifications' do + let(:unknown) { Fabricate(:account, username: 'unknown') } + let(:status) { Fabricate(:status, account: unknown) } + let(:activity) { Fabricate(:mention, account: recipient, status: status) } + let(:type) { :mention } + + before do + Fabricate(:notification_policy, account: recipient, filter_not_following: true) + end + + it 'creates a filtered notification' do + expect { subject }.to change(Notification, :count) + expect(Notification.last).to be_filtered + end + + context 'when no notification request exists' do + it 'creates a notification request' do + expect { subject }.to change(NotificationRequest, :count) + end + end + + context 'when a notification request exists' do + let!(:notification_request) do + Fabricate(:notification_request, account: recipient, from_account: unknown, last_status: Fabricate(:status, account: unknown)) + end + + it 'updates the existing notification request' do + expect { subject }.to_not change(NotificationRequest, :count) + expect(notification_request.reload.last_status).to eq status + end + end + end + describe NotifyService::DismissCondition do subject { described_class.new(notification) }