diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml
new file mode 100644
index 00000000..32d8f2d0
--- /dev/null
+++ b/.github/workflows/build-image.yml
@@ -0,0 +1,57 @@
+---
+name: Build container image
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ main ]
+ tags: [ '*' ]
+ pull_request:
+ paths:
+ - .github/workflows/build-image.yml
+ - Containerfile
+
+jobs:
+ build-image:
+ runs-on: ubuntu-latest
+
+ concurrency:
+ group: ${{ github.ref }}
+ cancel-in-progress: true
+
+ steps:
+ - uses: actions/checkout@v3.3.0
+
+ - name: Discover build-time variables
+ run: |
+ echo "RUBY_VERSION=$(cat .ruby-version)" >> $GITHUB_ENV
+ echo "BUNDLER_VERSION=$(egrep -A1 "^BUNDLED WITH" Gemfile.lock | tr -d '\n' | awk '{ print $3; }')" >> $GITHUB_ENV
+ case "${{ github.ref_name }}" in
+ */merge)
+ # use commit id as version for pull requests
+ echo "RETROSPRING_VERSION=${{ github.sha }}" >> $GITHUB_ENV
+ ;;
+ *)
+ # use tags and branches as version otherwise
+ echo "RETROSPRING_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
+ ;;
+ esac
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ vars.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ if: github.event_name != 'pull_request'
+
+ - name: Build and push
+ uses: docker/build-push-action@v4
+ with:
+ build-args: |
+ BUNDLER_VERSION=${{ env.BUNDLER_VERSION }}
+ RETROSPRING_VERSION=${{ env.RETROSPRING_VERSION }}
+ RUBY_VERSION=${{ env.RUBY_VERSION }}
+ context: .
+ file: Containerfile
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: retrospring/retrospring:${{ env.RETROSPRING_VERSION }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 321573da..4916381e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -12,6 +12,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
+ - name: Install dependencies
+ run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
@@ -46,6 +48,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
+ - name: Install dependencies
+ run: sudo apt update && sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick libidn11-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
diff --git a/.rubocop.yml b/.rubocop.yml
index 6091bca9..acd3db74 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -131,3 +131,6 @@ Style/Encoding:
Style/EndlessMethod:
EnforcedStyle: allow_always
+
+Style/TrailingCommaInHashLiteral:
+ EnforcedStyleForMultiline: consistent_comma
diff --git a/.stylelintrc.json b/.stylelintrc.json
index b8f0eb23..2aa477b8 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -9,6 +9,7 @@
"scss/at-import-partial-extension": null,
"scss/at-rule-no-unknown": true,
"at-rule-no-unknown": null,
+ "color-function-notation": null,
"color-hex-length": "long",
"color-hex-case": "upper",
"comment-whitespace-inside": null,
diff --git a/Containerfile b/Containerfile
new file mode 100644
index 00000000..2e740498
--- /dev/null
+++ b/Containerfile
@@ -0,0 +1,79 @@
+# Container image for a production Retrospring setup
+
+FROM registry.opensuse.org/opensuse/leap:15.4
+
+ARG RETROSPRING_VERSION=2023.0131.1
+ARG RUBY_VERSION=3.1.2
+ARG RUBY_INSTALL_VERSION=0.9.0
+ARG BUNDLER_VERSION=2.3.18
+
+ENV RAILS_ENV=production
+
+# update and install dependencies
+RUN zypper up -y \
+ && zypper in -y \
+ # build dependencies (ruby-install)
+ automake \
+ gcc \
+ gdbm-devel \
+ gzip \
+ libffi-devel \
+ libopenssl-devel \
+ libyaml-devel \
+ make \
+ ncurses-devel \
+ readline-devel \
+ tar \
+ xz \
+ zlib-devel \
+ # build dependencies (app)
+ gcc-c++ \
+ git \
+ libidn-devel \
+ nodejs14 \
+ npm14 \
+ postgresql-devel \
+ # runtime dependencies
+ ImageMagick \
+ # cleanup repos
+ && zypper clean -a \
+ # install yarn as another build dependency
+ && npm install -g yarn
+
+# install Ruby via ruby-install
+RUN curl -Lo ruby-install-${RUBY_INSTALL_VERSION}.tar.gz https://github.com/postmodern/ruby-install/archive/v${RUBY_INSTALL_VERSION}.tar.gz \
+ && tar xvf ruby-install-${RUBY_INSTALL_VERSION}.tar.gz \
+ && (cd ruby-install-${RUBY_INSTALL_VERSION} && make install) \
+ && rm -rf ruby-install-${RUBY_INSTALL_VERSION} ruby-install-${RUBY_INSTALL_VERSION}.tar.gz \
+ && ruby-install --no-install-deps --cleanup --system --jobs=$(nproc) ruby ${RUBY_VERSION} -- --disable-install-rdoc \
+ && gem install bundler:${BUNDLER_VERSION}
+
+# create user and dirs to run retrospring in
+RUN useradd -m justask \
+ && install -o justask -g users -m 0755 -d /opt/retrospring/app \
+ && install -o justask -g users -m 0755 -d /opt/retrospring/bundle
+
+WORKDIR /opt/retrospring/app
+USER justask:users
+
+# install the app
+RUN curl -L https://github.com/Retrospring/retrospring/archive/${RETROSPRING_VERSION}.tar.gz | tar xz --strip-components=1
+
+RUN bundle config set without 'development test' \
+ && bundle config set path '/opt/retrospring/bundle' \
+ && bundle install --jobs=$(nproc) \
+ && yarn install --frozen-lockfile
+
+# temporarily set a SECRET_KEY_BASE and copy config files so rake tasks can run
+ARG SECRET_KEY_BASE=secret_for_build
+RUN cp config/justask.yml.example config/justask.yml \
+ && cp config/database.yml.postgres config/database.yml \
+ && bundle exec rails locale:generate \
+ && bundle exec i18n export \
+ && bundle exec rails assets:precompile \
+ && rm config/justask.yml config/database.yml
+
+# set some defaults
+ENV RAILS_LOG_TO_STDOUT=true
+
+EXPOSE 3000
diff --git a/Gemfile b/Gemfile
index b1387ede..48ecdf45 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,7 +22,7 @@ gem "active_model_otp"
gem "bootsnap", require: false
gem "bootstrap_form", "~> 5.0"
gem "carrierwave", "~> 2.0"
-gem "carrierwave_backgrounder", git: "https://github.com/mltnhm/carrierwave_backgrounder.git"
+gem "carrierwave_backgrounder", git: "https://github.com/raccube/carrierwave_backgrounder.git"
gem "colorize"
gem "devise", "~> 4.0"
gem "devise-async"
@@ -58,14 +58,9 @@ gem "httparty"
gem "redcarpet"
gem "sanitize"
-# OmniAuth and providers
-gem "omniauth"
-gem "omniauth-twitter"
-
-# OAuth clients
-gem "twitter"
gem "twitter-text"
+gem "connection_pool"
gem "redis"
gem "fake_email_validator"
@@ -73,7 +68,7 @@ gem "fake_email_validator"
# TLD validation
gem "tldv", "~> 0.1.0"
-gem "jwt", "~> 2.6"
+gem "jwt", "~> 2.7"
group :development do
gem "binding_of_caller"
@@ -97,7 +92,7 @@ group :development, :test do
gem "rspec-mocks"
gem "rspec-rails", "~> 6.0"
gem "rspec-sidekiq", "~> 3.0", require: false
- gem "rubocop", "~> 1.44"
+ gem "rubocop", "~> 1.45"
gem "rubocop-rails", "~> 2.17"
gem "shoulda-matchers", "~> 5.3"
gem "simplecov", require: false
@@ -109,8 +104,6 @@ group :production do
gem "lograge"
end
-gem "omniauth-rails_csrf_protection", "~> 1.0"
-
gem "net-imap"
gem "net-pop"
gem "net-smtp"
diff --git a/Gemfile.lock b/Gemfile.lock
index 21bf8124..94439de4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,6 +1,6 @@
GIT
- remote: https://github.com/mltnhm/carrierwave_backgrounder.git
- revision: 8fe468957f047ad7039f07679e5952a534d07b6d
+ remote: https://github.com/raccube/carrierwave_backgrounder.git
+ revision: 41b756f7514c0e410c561bc8b5ee321cd8cce1ee
specs:
carrierwave_backgrounder (0.4.2)
carrierwave (>= 0.5, <= 2.1)
@@ -96,7 +96,6 @@ GEM
bootstrap_form (5.1.0)
actionpack (>= 5.2)
activemodel (>= 5.2)
- buftok (0.2.0)
builder (3.2.4)
bullet (7.0.7)
activesupport (>= 3.0.0)
@@ -137,8 +136,6 @@ GEM
devise (>= 4.8.0)
diff-lcs (1.5.0)
docile (1.4.0)
- domain_name (0.5.20190701)
- unf (>= 0.0.5, < 1.0.0)
dry-core (1.0.0)
concurrent-ruby (~> 1.0)
zeitwerk (~> 2.6)
@@ -154,9 +151,8 @@ GEM
dry-inflector (~> 1.0, < 2)
dry-logic (>= 1.4, < 2)
zeitwerk (~> 2.6)
- equalizer (0.0.11)
erubi (1.12.0)
- excon (0.98.0)
+ excon (0.99.0)
factory_bot (6.2.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
@@ -165,13 +161,10 @@ GEM
fake_email_validator (1.0.11)
activemodel
mail
- faker (3.1.0)
+ faker (3.1.1)
i18n (>= 1.8.11, < 2)
ffi (1.15.5)
- ffi-compiler (1.0.1)
- ffi (>= 1.0.0)
- rake
- fog-aws (3.16.0)
+ fog-aws (3.17.0)
fog-core (~> 2.1)
fog-json (~> 1.1)
fog-xml (~> 0.1)
@@ -202,22 +195,10 @@ GEM
rainbow
rubocop (>= 0.50.0)
sysexits (~> 1.1)
- hashie (5.0.0)
hcaptcha (7.1.0)
json
hkdf (0.3.0)
http-2 (0.11.0)
- http (4.4.1)
- addressable (~> 2.3)
- http-cookie (~> 1.0)
- http-form_data (~> 2.2)
- http-parser (~> 1.2.0)
- http-cookie (1.0.4)
- domain_name (~> 0.5)
- http-form_data (2.3.0)
- http-parser (1.2.3)
- ffi-compiler (>= 1.0, < 2.0)
- http_parser.rb (0.6.0)
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
@@ -235,7 +216,7 @@ GEM
json (2.6.3)
json-schema (3.0.0)
addressable (>= 2.8)
- jwt (2.6.0)
+ jwt (2.7.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -263,8 +244,6 @@ GEM
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.2)
- memoizable (0.4.2)
- thread_safe (~> 0.3, >= 0.3.1)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
@@ -279,8 +258,6 @@ GEM
msgpack (1.6.0)
multi_json (1.15.0)
multi_xml (0.6.0)
- multipart-post (2.1.1)
- naught (1.1.0)
nested_form (0.3.2)
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
@@ -296,41 +273,25 @@ GEM
net-smtp (0.3.3)
net-protocol
nio4r (2.5.8)
- nokogiri (1.14.0)
+ nokogiri (1.14.1)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
- oauth (0.5.8)
- oj (3.13.23)
- omniauth (2.1.0)
- hashie (>= 3.4.6)
- rack (>= 2.2.3)
- rack-protection
- omniauth-oauth (1.2.0)
- oauth
- omniauth (>= 1.0, < 3)
- omniauth-rails_csrf_protection (1.0.1)
- actionpack (>= 4.2)
- omniauth (~> 2.0)
- omniauth-twitter (1.4.0)
- omniauth-oauth (~> 1.1)
- rack
+ oj (3.14.2)
openssl (3.1.0)
orm_adapter (0.5.0)
parallel (1.22.1)
- parser (3.2.0.0)
+ parser (3.2.1.0)
ast (~> 2.4.1)
pg (1.4.5)
pghero (3.1.0)
activerecord (>= 6)
public_suffix (4.0.7)
- puma (6.0.2)
+ puma (6.1.0)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
racc (1.6.2)
rack (2.2.6.2)
- rack-protection (3.0.5)
- rack
rack-test (2.0.2)
rack (>= 1.3)
rails (6.1.7.2)
@@ -376,14 +337,14 @@ GEM
rake (13.0.6)
redcarpet (3.6.0)
redis (4.8.0)
- regexp_parser (2.6.2)
+ regexp_parser (2.7.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
- rolify (6.0.0)
+ rolify (6.0.1)
rotp (6.2.0)
rpush (7.0.1)
activesupport (>= 5.2)
@@ -422,7 +383,7 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.12.0)
- rubocop (1.44.1)
+ rubocop (1.45.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
@@ -432,8 +393,8 @@ GEM
rubocop-ast (>= 1.24.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.24.1)
- parser (>= 3.1.1.0)
+ rubocop-ast (1.26.0)
+ parser (>= 3.2.1.0)
rubocop-rails (2.17.4)
activesupport (>= 4.2.0)
rack (>= 1.1)
@@ -453,13 +414,13 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
- sentry-rails (5.7.0)
+ sentry-rails (5.8.0)
railties (>= 5.0)
- sentry-ruby (~> 5.7.0)
- sentry-ruby (5.7.0)
+ sentry-ruby (~> 5.8.0)
+ sentry-ruby (5.8.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
- sentry-sidekiq (5.7.0)
- sentry-ruby (~> 5.7.0)
+ sentry-sidekiq (5.8.0)
+ sentry-ruby (~> 5.8.0)
sidekiq (>= 3.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
@@ -467,7 +428,6 @@ GEM
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
- simple_oauth (0.3.1)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -491,27 +451,15 @@ GEM
sysexits (1.2.0)
temple (0.10.0)
thor (1.2.1)
- thread_safe (0.3.6)
tilt (2.0.11)
timeout (0.3.1)
tldv (0.1.0)
tldv-data (~> 1.0)
tldv-data (1.0.2022121701)
- turbo-rails (1.3.2)
+ turbo-rails (1.3.3)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
- twitter (7.0.0)
- addressable (~> 2.3)
- buftok (~> 0.2.0)
- equalizer (~> 0.0.11)
- http (~> 4.0)
- http-form_data (~> 2.0)
- http_parser.rb (~> 0.6.0)
- memoizable (~> 0.4.0)
- multipart-post (~> 2.0)
- naught (~> 1.0)
- simple_oauth (~> 0.3.0)
twitter-text (3.1.0)
idn-ruby
unf (~> 0.1.0)
@@ -546,6 +494,7 @@ DEPENDENCIES
carrierwave (~> 2.0)
carrierwave_backgrounder!
colorize
+ connection_pool
cssbundling-rails (~> 1.1)
database_cleaner
devise (~> 4.0)
@@ -566,7 +515,7 @@ DEPENDENCIES
i18n-js (= 4.0)
jsbundling-rails (~> 1.1)
json-schema
- jwt (~> 2.6)
+ jwt (~> 2.7)
letter_opener
lograge
mail (~> 2.7.1)
@@ -575,9 +524,6 @@ DEPENDENCIES
net-pop
net-smtp
oj
- omniauth
- omniauth-rails_csrf_protection (~> 1.0)
- omniauth-twitter
openssl (~> 3.1)
pg
pghero
@@ -598,7 +544,7 @@ DEPENDENCIES
rspec-mocks
rspec-rails (~> 6.0)
rspec-sidekiq (~> 3.0)
- rubocop (~> 1.44)
+ rubocop (~> 1.45)
rubocop-rails (~> 2.17)
ruby-progressbar
rubyzip (~> 2.3)
@@ -617,7 +563,6 @@ DEPENDENCIES
sprockets-rails
tldv (~> 0.1.0)
turbo-rails
- twitter
twitter-text
BUNDLED WITH
diff --git a/app/assets/stylesheets/components/_answerbox.scss b/app/assets/stylesheets/components/_answerbox.scss
index 2eae714c..adb1b732 100644
--- a/app/assets/stylesheets/components/_answerbox.scss
+++ b/app/assets/stylesheets/components/_answerbox.scss
@@ -82,11 +82,22 @@
}
}
+ &__pinned {
+ display: none;
+ }
+
.card-body {
padding-bottom: .6rem;
}
}
+#pinned-answers {
+
+ .answerbox__pinned {
+ display: inline;
+ }
+}
+
body:not(.cap-web-share) {
[name="ab-share"] {
display: none;
diff --git a/app/assets/stylesheets/components/_inbox-entry.scss b/app/assets/stylesheets/components/_inbox-entry.scss
index 3fd728a1..a56875cd 100644
--- a/app/assets/stylesheets/components/_inbox-entry.scss
+++ b/app/assets/stylesheets/components/_inbox-entry.scss
@@ -1,6 +1,10 @@
+@use "sass:map";
+
.inbox-entry {
$this: &;
+ position: relative;
+
&--new {
box-shadow: 0 0.125rem 0.25rem var(--primary);
@@ -22,6 +26,24 @@
}
}
+ &__close {
+ position: absolute;
+ top: map.get($spacers, 3);
+ right: map.get($spacers, 3);
+ }
+
+ &__sharing {
+ position: absolute;
+ display: flex;
+ background-color: rgba(var(--raised-bg-rgb), 0.9);
+ backdrop-filter: blur(3px);
+ border-radius: var(--card-inner-border-radius);
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
.format-help {
opacity: .3;
}
@@ -30,7 +52,7 @@
&:hover .format-help {
opacity: 1;
}
-
+
.card-header {
position: relative;
}
diff --git a/app/controllers/ajax/answer_controller.rb b/app/controllers/ajax/answer_controller.rb
index de9c611f..859ae794 100644
--- a/app/controllers/ajax/answer_controller.rb
+++ b/app/controllers/ajax/answer_controller.rb
@@ -1,11 +1,17 @@
+# frozen_string_literal: true
+
+require "cgi"
+
class Ajax::AnswerController < AjaxController
+ include SocialHelper::TwitterMethods
+ include SocialHelper::TumblrMethods
+
def create
params.require :id
params.require :answer
- params.require :share
params.require :inbox
- inbox = (params[:inbox] == 'true')
+ inbox = (params[:inbox] == "true")
if inbox
inbox_entry = Inbox.find(params[:id])
@@ -31,20 +37,23 @@ class Ajax::AnswerController < AjaxController
current_user.answer question, params[:answer]
end
- services = JSON.parse params[:share]
- services.each do |service|
- ShareWorker.perform_async(current_user.id, answer.id, service)
- end
-
-
@response[:status] = :okay
@response[:message] = t(".success")
@response[:success] = true
- unless inbox
- # this assign is needed because shared/_answerbox relies on it, I think
- @question = 1
- @response[:render] = render_to_string(partial: 'answerbox', locals: { a: answer, show_question: false })
+
+ if current_user.sharing_enabled
+ @response[:sharing] = {
+ twitter: twitter_share_url(answer),
+ tumblr: tumblr_share_url(answer),
+ custom: CGI.escape(prepare_tweet(answer))
+ }
end
+
+ return if inbox
+
+ # this assign is needed because shared/_answerbox relies on it, I think
+ @question = 1
+ @response[:render] = render_to_string(partial: "answerbox", locals: { a: answer, show_question: false })
end
def destroy
@@ -52,15 +61,13 @@ class Ajax::AnswerController < AjaxController
answer = Answer.find(params[:answer])
- unless (current_user == answer.user) or (privileged? answer.user)
+ unless (current_user == answer.user) || (privileged? answer.user)
@response[:status] = :nopriv
@response[:message] = t(".nopriv")
return
end
- if answer.user == current_user
- Inbox.create!(user: answer.user, question: answer.question, new: true, returning: true)
- end
+ Inbox.create!(user: answer.user, question: answer.question, new: true, returning: true) if answer.user == current_user
answer.destroy
@response[:status] = :okay
diff --git a/app/controllers/ajax/question_controller.rb b/app/controllers/ajax/question_controller.rb
index e86e1572..f3bef810 100644
--- a/app/controllers/ajax/question_controller.rb
+++ b/app/controllers/ajax/question_controller.rb
@@ -31,7 +31,7 @@ class Ajax::QuestionController < AjaxController
UseCase::Question::CreateFollowers.call(
source_user_id: current_user.id,
content: params[:question],
- author_identifier: AnonymousBlock.get_identifier(request.ip)
+ author_identifier: AnonymousBlock.get_identifier(request.remote_ip)
)
return
end
@@ -41,7 +41,7 @@ class Ajax::QuestionController < AjaxController
target_user_id: params[:rcpt],
content: params[:question],
anonymous: params[:anonymousQuestion],
- author_identifier: AnonymousBlock.get_identifier(request.ip)
+ author_identifier: AnonymousBlock.get_identifier(request.remote_ip)
)
end
end
diff --git a/app/controllers/anonymous_block_controller.rb b/app/controllers/anonymous_block_controller.rb
index 1daf9d8c..a8c9a32f 100644
--- a/app/controllers/anonymous_block_controller.rb
+++ b/app/controllers/anonymous_block_controller.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class AnonymousBlockController < ApplicationController
+ include TurboStreamable
+
before_action :authenticate_user!
+ turbo_stream_actions :create, :destroy
+
def create
params.require :question
@@ -16,12 +20,15 @@ class AnonymousBlockController < ApplicationController
target_user: question.user
)
- inbox_id = question.inboxes.first.id
+ inbox_id = question.inboxes.first&.id
question.inboxes.first&.destroy
respond_to do |format|
format.turbo_stream do
- render turbo_stream: turbo_stream.remove("inbox_#{inbox_id}")
+ render turbo_stream: [
+ inbox_id ? turbo_stream.remove("inbox_#{inbox_id}") : nil,
+ render_toast(t(".success"))
+ ].compact
end
format.html { redirect_back(fallback_location: inbox_path) }
@@ -38,7 +45,10 @@ class AnonymousBlockController < ApplicationController
respond_to do |format|
format.turbo_stream do
- render turbo_stream: turbo_stream.remove("block_#{params[:id]}")
+ render turbo_stream: [
+ turbo_stream.remove("block_#{params[:id]}"),
+ render_toast(t(".success"))
+ ]
end
format.html { redirect_back(fallback_location: settings_blocks_path) }
diff --git a/app/controllers/answer_controller.rb b/app/controllers/answer_controller.rb
index b63be49e..d5e70881 100644
--- a/app/controllers/answer_controller.rb
+++ b/app/controllers/answer_controller.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
class AnswerController < ApplicationController
+ before_action :authenticate_user!, only: %i[pin unpin]
+
+ include TurboStreamable
+
+ turbo_stream_actions :pin, :unpin
+
def show
@answer = Answer.includes(comments: %i[user smiles], question: [:user], smiles: [:user]).find(params[:id])
@display_all = true
@@ -16,4 +22,34 @@ class AnswerController < ApplicationController
notif.update_all(new: false) unless notif.empty?
end
end
+
+ def pin
+ answer = Answer.includes(:user).find(params[:id])
+ UseCase::Answer::Pin.call(user: current_user, answer:)
+
+ respond_to do |format|
+ format.html { redirect_to(user_path(username: current_user.screen_name)) }
+ format.turbo_stream do
+ render turbo_stream: [
+ turbo_stream.update("ab-pin-#{answer.id}", partial: "actions/pin", locals: { answer: }),
+ render_toast(t(".success"))
+ ]
+ end
+ end
+ end
+
+ def unpin
+ answer = Answer.includes(:user).find(params[:id])
+ UseCase::Answer::Unpin.call(user: current_user, answer:)
+
+ respond_to do |format|
+ format.html { redirect_to(user_path(username: current_user.screen_name)) }
+ format.turbo_stream do
+ render turbo_stream: [
+ turbo_stream.update("ab-pin-#{answer.id}", partial: "actions/pin", locals: { answer: }),
+ render_toast(t(".success"))
+ ]
+ end
+ end
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 05b7ad28..64ed88c7 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -60,7 +60,7 @@ class ApplicationController < ActionController::Base
if current_user.present?
Sentry.set_user({ id: current_user.id })
else
- Sentry.set_user({ ip_address: request.ip })
+ Sentry.set_user({ ip_address: request.remote_ip })
end
end
end
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/controllers/concerns/turbo_streamable.rb b/app/controllers/concerns/turbo_streamable.rb
new file mode 100644
index 00000000..5066bc05
--- /dev/null
+++ b/app/controllers/concerns/turbo_streamable.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module TurboStreamable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def turbo_stream_actions(*actions)
+ around_action :handle_error, only: actions
+ end
+ end
+
+ def render_toast(message, success = true)
+ turbo_stream.append("toasts", partial: "shared/toast", locals: { message:, success: })
+ end
+
+ private
+
+ def handle_error
+ yield
+ rescue Errors::Base => e
+ render_error I18n.t(e.locale_tag)
+ rescue KeyError, ActionController::ParameterMissing => e
+ render_error t("errors.parameter_error", parameter: e.instance_of?(KeyError) ? e.key : e.param.capitalize)
+ rescue Dry::Types::CoercionError, Dry::Types::ConstraintError
+ render_error t("errors.invalid_parameter")
+ rescue ActiveRecord::RecordNotFound
+ render_error t("errors.record_not_found")
+ end
+
+ def render_error(message)
+ respond_to do |format|
+ format.turbo_stream do
+ render turbo_stream: render_toast(message, false)
+ end
+ end
+ end
+end
diff --git a/app/controllers/inbox_controller.rb b/app/controllers/inbox_controller.rb
index a31080e8..d849dafc 100644
--- a/app/controllers/inbox_controller.rb
+++ b/app/controllers/inbox_controller.rb
@@ -18,12 +18,11 @@ class InboxController < ApplicationController
@delete_id = find_delete_id
@disabled = true if @inbox.empty?
- services = current_user.services.to_a
respond_to do |format|
- format.html { render "show", locals: { services: } }
+ format.html { render "show" }
format.turbo_stream do
- render "show", locals: { services: }, layout: false, status: :see_other
+ render "show", layout: false, status: :see_other
# rubocop disabled as just flipping a flag doesn't need to have validations to be run
@inbox.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
@@ -38,11 +37,10 @@ class InboxController < ApplicationController
user: current_user)
inbox = Inbox.create!(user: current_user, question_id: question.id, new: true)
- services = current_user.services
respond_to do |format|
format.turbo_stream do
- render turbo_stream: turbo_stream.prepend("entries", partial: "inbox/entry", locals: { i: inbox, services: })
+ render turbo_stream: turbo_stream.prepend("entries", partial: "inbox/entry", locals: { i: inbox })
inbox.update(new: false)
end
diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb
deleted file mode 100644
index deaa39a0..00000000
--- a/app/controllers/services_controller.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-class ServicesController < ApplicationController
- before_action :authenticate_user!
- before_action :mark_notifications_as_read, only: %i[index]
-
- def index
- @services = current_user.services
- end
-
- def create
- service = Service.initialize_from_omniauth(omniauth_hash)
- service.user = current_user
- service_name = service.type.split("::").last.titleize
-
- if service.save
- flash[:success] = t(".success", service: service_name)
- else
- flash[:error] = if service.errors.details[:uid]&.any? { |err| err[:error] == :taken }
- t(".duplicate", service: service_name, app: APP_CONFIG["site_name"])
- else
- t(".error", service: service_name)
- end
- end
-
- redirect_to origin || services_path
- end
-
- def update
- service = current_user.services.find(params[:id])
- service.post_tag = params[:service][:post_tag].tr("@", "")
- if service.save
- flash[:success] = t(".success")
- else
- flash[:error] = t(".error")
- end
- redirect_to services_path
- end
-
- def failure
- Rails.logger.info "oauth error: #{params.inspect}"
- flash[:error] = t(".error")
- redirect_to services_path
- end
-
- def destroy
- @service = current_user.services.find(params[:id])
- service_name = @service.type.split("::").last.titleize
- @service.destroy
- flash[:success] = t(".success", service: service_name)
- redirect_to services_path
- end
-
- private
-
- def origin
- request.env["omniauth.origin"]
- end
-
- def omniauth_hash
- request.env["omniauth.auth"]
- end
-
- def mark_notifications_as_read
- Notification::ServiceTokenExpired
- .where(recipient: current_user, new: true)
- .update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
- end
-end
diff --git a/app/controllers/settings/mutes_controller.rb b/app/controllers/settings/mutes_controller.rb
index 6d312ee6..1000b862 100644
--- a/app/controllers/settings/mutes_controller.rb
+++ b/app/controllers/settings/mutes_controller.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class Settings::MutesController < ApplicationController
+ include TurboStreamable
+
before_action :authenticate_user!
+ turbo_stream_actions :create, :destroy
+
def index
@users = current_user.muted_users
@rules = MuteRule.where(user: current_user)
@@ -15,7 +19,8 @@ class Settings::MutesController < ApplicationController
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace("form", partial: "settings/mutes/form"),
- turbo_stream.append("rules", partial: "settings/mutes/rule", locals: { rule: result[:resource] })
+ turbo_stream.append("rules", partial: "settings/mutes/rule", locals: { rule: result[:resource] }),
+ render_toast(t(".success"))
]
end
@@ -32,7 +37,10 @@ class Settings::MutesController < ApplicationController
respond_to do |format|
format.turbo_stream do
- render turbo_stream: turbo_stream.remove("rule_#{params[:id]}")
+ render turbo_stream: [
+ turbo_stream.remove("rule_#{params[:id]}"),
+ render_toast(t(".success"))
+ ]
end
format.html { redirect_to settings_muted_path }
diff --git a/app/controllers/settings/sharing_controller.rb b/app/controllers/settings/sharing_controller.rb
new file mode 100644
index 00000000..76a7ca76
--- /dev/null
+++ b/app/controllers/settings/sharing_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Settings::SharingController < ApplicationController
+ before_action :authenticate_user!
+
+ def edit; end
+
+ def update
+ user_attributes = params.require(:user).permit(:sharing_enabled,
+ :sharing_autoclose,
+ :sharing_custom_url)
+ if current_user.update(user_attributes)
+ flash.now[:success] = t(".success")
+ else
+ flash.now[:error] = t(".error")
+ end
+
+ render :edit
+ end
+end
diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb
index 17926429..1963b496 100644
--- a/app/controllers/user_controller.rb
+++ b/app/controllers/user_controller.rb
@@ -7,6 +7,7 @@ class UserController < ApplicationController
def show
@answers = @user.cursored_answers(last_id: params[:last_id])
+ @pinned_answers = @user.answers.pinned.order(pinned_at: :desc).limit(10)
@answers_last_id = @answers.map(&:id).min
@more_data_available = !@user.cursored_answers(last_id: @answers_last_id, size: 1).count.zero?
diff --git a/app/controllers/well_known/node_info_controller.rb b/app/controllers/well_known/node_info_controller.rb
index 8d6771bb..8faad0eb 100644
--- a/app/controllers/well_known/node_info_controller.rb
+++ b/app/controllers/well_known/node_info_controller.rb
@@ -8,7 +8,7 @@ class WellKnown::NodeInfoController < ApplicationController
links: [
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
href: node_info_url
- ]
+ ],
}
end
@@ -21,12 +21,12 @@ class WellKnown::NodeInfoController < ApplicationController
protocols: %i[],
services: {
inbound: inbound_services,
- outbound: outbound_services
+ outbound: outbound_services,
},
usage: usage_stats,
# We don't implement this so we can always return true for now
openRegistrations: true,
- metadata: {}
+ metadata: {},
}
end
@@ -43,16 +43,12 @@ class WellKnown::NodeInfoController < ApplicationController
def usage_stats
{
users: {
- total: User.count
- }
+ total: User.count,
+ },
}
end
def inbound_services = []
- def outbound_services
- {
- "twitter" => APP_CONFIG.dig("sharing", "twitter", "enabled")
- }.select { |_service, available| available }.keys
- end
+ def outbound_services = []
end
diff --git a/app/javascript/retrospring/controllers/inbox_sharing_controller.ts b/app/javascript/retrospring/controllers/inbox_sharing_controller.ts
new file mode 100644
index 00000000..48f0a905
--- /dev/null
+++ b/app/javascript/retrospring/controllers/inbox_sharing_controller.ts
@@ -0,0 +1,48 @@
+import { Controller } from '@hotwired/stimulus';
+
+export default class extends Controller {
+ static targets = ['twitter', 'tumblr', 'custom'];
+
+ declare readonly twitterTarget: HTMLAnchorElement;
+ declare readonly tumblrTarget: HTMLAnchorElement;
+ declare readonly customTarget: HTMLAnchorElement;
+ declare readonly hasCustomTarget: boolean;
+
+ static values = {
+ config: Object,
+ autoClose: Boolean
+ };
+
+ declare readonly configValue: Record Want your followers on another platform to see your %{app_name} answers?
- You can configure automatic sharing to your favourite platforms easily.
Not sure if it's a favourite, but at the moment only - Twitter is supported.
+We support Tumblr, Twitter, + and many other services including Mastodon and + Misskey.
customize: header: "Customise your experience" body_html: | @@ -74,6 +75,9 @@ en: return: "Return to Inbox" comment: view_smiles: "View comment smiles" + pin: + pin: "Pin to Profile" + unpin: "Unpin from Profile" share: twitter: "Share on Twitter" tumblr: "Share on Tumblr" @@ -122,6 +126,7 @@ en: read: "Read the entire answer" answered: "%{hide} %{user}" # resolves into "Answered by %{user}" hide: "Answered by" + pinned: "Pinned" questionbox: title: "Ask something!" placeholder: "Type your question here…" @@ -214,10 +219,10 @@ en: options: "Sharing Options" placeholder: "Write your answer here…" sharing: - heading: "Sharing" + heading: "Share answer on..." post_to: "Post to %{service}" - none_html: "You have not connected any services yet. Visit your %{settings} to connect one." - settings: "service settings" + hint_html: "You can customise these options in your %{settings}" + settings: "sharing settings" show: empty: "Nothing to see here." actions: @@ -359,13 +364,6 @@ en: heading: "Push notifications are failing to send to one of your devices." text_html: "Please check the %{settings_push} if you still want to be notified." settings_push: "push notification settings" - expiredtwitterserviceconnection: - heading: "Twitter connection expired" - text_html: "If you would like to continue automatically sharing your answers to Twitter, head to %{settings_sharing} and re-connect your account." - settings_services: "Sharing Settings" - services: - index: - title: "Service Settings" settings: account: email_confirm: "Currently awaiting confirmation for %{resource}" @@ -453,6 +451,14 @@ en: profile_picture: "Adjust your new profile picture" profile_header: "Adjust your new profile header" submit_picture: "Save pictures" + sharing: + edit: + title: "Sharing Settings" + advanced: + title: "Advanced options" + body_html: | +If you use a service other than Twitter or Tumblr, which has support for sharing from external services, you can enter their sharing URL here so it will be listed as an option for sharing after an answer has been sent.
+We will prepend a URL-encoded version of the answer to the end of the given link.
two_factor_authentication: otp_authentication: index: @@ -573,6 +579,9 @@ en: anonymous_block: deleted_question: "Deleted question" blocked: "blocked %{time} ago" + question: + visible_to_you: "Only visible to you as it was asked directly" + visible_mod_mode: "You can see this because you are in moderation view" tabs: admin: announcements: "Announcements" diff --git a/config/routes.rb b/config/routes.rb index 7dca9c1a..744a8d83 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -78,6 +78,9 @@ Rails.application.routes.draw do get :privacy, to: redirect("/settings/privacy/edit") resource :privacy, controller: :privacy, only: %i[edit update] + get :sharing, to: redirect("/settings/sharing/edit") + resource :sharing, controller: :sharing, only: %i[edit update] + get :export, to: "export#index" post :export, to: "export#create" @@ -101,17 +104,6 @@ Rails.application.routes.draw do resolve("Theme") { [:settings_theme] } # to make link_to/form_for work nicely when passing a `Theme` object to it, see also: https://api.rubyonrails.org/v6.1.5.1/classes/ActionDispatch/Routing/Mapper/CustomUrls.html#method-i-resolve resolve("Profile") { [:settings_profile] } - # resources :services, only: [:index, :destroy] - get "/settings/services", to: "services#index", as: :services - patch "/settings/services/:id", to: "services#update", as: :update_service - delete "/settings/services/:id", to: "services#destroy", as: :service - controller :services do - scope "/auth", as: "auth" do - get ":provider/callback" => :create - get :failure - end - end - namespace :ajax do post "/ask", to: "question#create", as: :ask post "/destroy_question", to: "question#destroy", as: :destroy_question @@ -154,6 +146,8 @@ Rails.application.routes.draw do get "/user/:username", to: "user#show" get "/@:username", to: "user#show", as: :user get "/@:username/a/:id", to: "answer#show", as: :answer + post "/@:username/a/:id/pin", to: "answer#pin", as: :pin_answer + delete "/@:username/a/:id/pin", to: "answer#unpin", as: :unpin_answer get "/@:username/q/:id", to: "question#show", as: :question get "/@:username/followers", to: "user#followers", as: :show_user_followers get "/@:username/followings", to: "user#followings", as: :show_user_followings diff --git a/db/migrate/20230128233136_add_pinned_at_to_answers.rb b/db/migrate/20230128233136_add_pinned_at_to_answers.rb new file mode 100644 index 00000000..8501ba1e --- /dev/null +++ b/db/migrate/20230128233136_add_pinned_at_to_answers.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddPinnedAtToAnswers < ActiveRecord::Migration[6.1] + def change + add_column :answers, :pinned_at, :timestamp + add_index :answers, %i[user_id pinned_at] + end +end diff --git a/db/migrate/20230205145011_add_sharing_fields_to_user.rb b/db/migrate/20230205145011_add_sharing_fields_to_user.rb new file mode 100644 index 00000000..cc2edc44 --- /dev/null +++ b/db/migrate/20230205145011_add_sharing_fields_to_user.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddSharingFieldsToUser < ActiveRecord::Migration[6.1] + def up + add_column :users, :sharing_enabled, :boolean, default: false + add_column :users, :sharing_autoclose, :boolean, default: false + add_column :users, :sharing_custom_url, :string + end + + def down + remove_column :users, :sharing_enabled + remove_column :users, :sharing_autoclose + remove_column :users, :sharing_custom_url + end +end diff --git a/db/migrate/20230205162103_enable_sharing_for_service_owners.rb b/db/migrate/20230205162103_enable_sharing_for_service_owners.rb new file mode 100644 index 00000000..f194722c --- /dev/null +++ b/db/migrate/20230205162103_enable_sharing_for_service_owners.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class EnableSharingForServiceOwners < ActiveRecord::Migration[6.1] + def up + execute <<~SQUIRREL + UPDATE users + SET sharing_enabled = true + WHERE id IN (SELECT user_id FROM services); + SQUIRREL + end + + def down; end +end diff --git a/db/migrate/20230205162800_drop_services_table.rb b/db/migrate/20230205162800_drop_services_table.rb new file mode 100644 index 00000000..6bb783df --- /dev/null +++ b/db/migrate/20230205162800_drop_services_table.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class DropServicesTable < ActiveRecord::Migration[6.1] + def up + drop_table :services + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20230212181044_remove_expired_service_connection_notifications.rb b/db/migrate/20230212181044_remove_expired_service_connection_notifications.rb new file mode 100644 index 00000000..3f306f39 --- /dev/null +++ b/db/migrate/20230212181044_remove_expired_service_connection_notifications.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveExpiredServiceConnectionNotifications < ActiveRecord::Migration[6.1] + def up = Notification.where(type: "Notification::ServiceTokenExpired").delete_all + + def down = raise ActiveRecord::IrreversibleMigration +end diff --git a/db/schema.rb b/db/schema.rb index e02cfdcc..e85a2c7e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_02_03_054229) do +ActiveRecord::Schema.define(version: 2023_02_12_181044) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -48,8 +48,10 @@ ActiveRecord::Schema.define(version: 2023_02_03_054229) do t.datetime "created_at" t.datetime "updated_at" t.integer "smile_count", default: 0, null: false + t.datetime "pinned_at" t.index ["question_id"], name: "index_answers_on_question_id" t.index ["user_id", "created_at"], name: "index_answers_on_user_id_and_created_at" + t.index ["user_id", "pinned_at"], name: "index_answers_on_user_id_and_pinned_at" end create_table "appendables", force: :cascade do |t| @@ -253,19 +255,6 @@ ActiveRecord::Schema.define(version: 2023_02_03_054229) do t.index ["delivered", "failed", "processing", "deliver_after", "created_at"], name: "index_rpush_notifications_multi", where: "((NOT delivered) AND (NOT failed))" end - create_table "services", id: :serial, force: :cascade do |t| - t.string "type", null: false - t.bigint "user_id", null: false - t.string "uid" - t.string "access_token" - t.string "access_secret" - t.string "nickname" - t.datetime "created_at" - t.datetime "updated_at" - t.string "post_tag", limit: 20 - t.index ["user_id"], name: "index_services_on_user_id" - end - create_table "subscriptions", id: :serial, force: :cascade do |t| t.bigint "user_id", null: false t.bigint "answer_id", null: false @@ -371,6 +360,9 @@ ActiveRecord::Schema.define(version: 2023_02_03_054229) do t.boolean "privacy_require_user", default: false t.boolean "privacy_hide_social_graph", default: false t.boolean "privacy_noindex", default: false + t.boolean "sharing_enabled", default: false + t.boolean "sharing_autoclose", default: false + t.string "sharing_custom_url" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true diff --git a/docker-compose.yml b/docker-compose.yml index 0bdc353d..ae852632 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - redis environment: - SPROCKETS_CACHE=/cache + - DATABASE_URL=postgres://postgres:justask@postgres/justask_development?pool=25 + - REDIS_URL=redis://redis:6379 volumes: - ./:/app - cache:/cache diff --git a/lib/use_case/answer/pin.rb b/lib/use_case/answer/pin.rb new file mode 100644 index 00000000..c790663c --- /dev/null +++ b/lib/use_case/answer/pin.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module UseCase + module Answer + class Pin < UseCase::Base + option :user, type: Types.Instance(::User) + option :answer, type: Types.Instance(::Answer) + + def call + authorize!(:pin, user, answer) + check_unpinned! + + answer.pinned_at = Time.now.utc + answer.save! + + { + status: 200, + resource: answer, + } + end + + private + + def check_unpinned! + raise ::Errors::BadRequest if answer.pinned_at.present? + end + end + end +end diff --git a/lib/use_case/answer/unpin.rb b/lib/use_case/answer/unpin.rb new file mode 100644 index 00000000..8f12742e --- /dev/null +++ b/lib/use_case/answer/unpin.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module UseCase + module Answer + class Unpin < UseCase::Base + option :user, type: Types.Instance(::User) + option :answer, type: Types.Instance(::Answer) + + def call + authorize!(:unpin, user, answer) + check_pinned! + + answer.pinned_at = nil + answer.save! + + { + status: 200, + resource: nil, + } + end + + private + + def check_pinned! + raise ::Errors::BadRequest if answer.pinned_at.nil? + end + end + end +end diff --git a/lib/use_case/base.rb b/lib/use_case/base.rb index e35585c2..7ff23bbf 100644 --- a/lib/use_case/base.rb +++ b/lib/use_case/base.rb @@ -9,5 +9,11 @@ module UseCase def self.call(...) = new(...).call def call = raise NotImplementedError + + private + + def authorize!(verb, user, record, error_class: Errors::NotAuthorized) + raise error_class unless Pundit.policy!(user, record).public_send("#{verb}?") + end end end diff --git a/package.json b/package.json index 2f4499f2..bca4b7f0 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ }, "dependencies": { "@fontsource/lexend": "^4.5.15", - "@fortawesome/fontawesome-free": "^6.2.1", + "@fortawesome/fontawesome-free": "^6.3.0", "@hotwired/stimulus": "^3.2.1", - "@hotwired/turbo-rails": "^7.2.4", + "@hotwired/turbo-rails": "^7.2.5", "@melloware/coloris": "^0.17.1", "@popperjs/core": "^2.11", "@rails/request.js": "^0.0.8", @@ -20,19 +20,19 @@ "croppr": "^2.3.1", "i18n-js": "^4.0", "js-cookie": "2.2.1", - "sass": "^1.57.1", + "sass": "^1.58.0", "sweetalert": "1.1.3", "toastify-js": "^1.12.0", - "typescript": "^4.9.4" + "typescript": "^4.9.5" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.11.0", "@typescript-eslint/parser": "^4.11.0", - "esbuild": "^0.17.5", + "esbuild": "^0.17.8", "eslint": "^7.16.0", "eslint-plugin-import": "^2.27.5", "stylelint": "^14.16.1", "stylelint-config-standard-scss": "^6.1.0", - "stylelint-scss": "^4.3.0" + "stylelint-scss": "^4.4.0" } } diff --git a/spec/controllers/ajax/answer_controller_spec.rb b/spec/controllers/ajax/answer_controller_spec.rb index 79c911dc..93374bb5 100644 --- a/spec/controllers/ajax/answer_controller_spec.rb +++ b/spec/controllers/ajax/answer_controller_spec.rb @@ -25,13 +25,6 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do expect { subject }.to(change { Answer.count }.by(1)) end - it "enqueues a job for sharing the answer to social networks" do - subject - shared_services.each do |service| - expect(ShareWorker).to have_enqueued_sidekiq_job(user.id, Answer.last.id, service) - end - end - include_examples "returns the expected response" end @@ -40,11 +33,6 @@ describe Ajax::AnswerController, :ajax_controller, type: :controller do expect { subject }.not_to(change { Answer.count }) end - it "does not enqueue a job for sharing the answer to social networks" do - subject - expect(ShareWorker).not_to have_enqueued_sidekiq_job(anything, anything, anything) - end - include_examples "returns the expected response" end diff --git a/spec/controllers/anonymous_block_controller_spec.rb b/spec/controllers/anonymous_block_controller_spec.rb index 15dfef01..a1619a51 100644 --- a/spec/controllers/anonymous_block_controller_spec.rb +++ b/spec/controllers/anonymous_block_controller_spec.rb @@ -4,7 +4,7 @@ require "rails_helper" describe AnonymousBlockController, type: :controller do describe "#create" do - subject { post(:create, params:) } + subject { post(:create, params:, format: :turbo_stream) } context "user signed in" do let(:user) { FactoryBot.create(:user) } @@ -23,6 +23,29 @@ describe AnonymousBlockController, type: :controller do it "creates an anonymous block" do expect { subject }.to(change { AnonymousBlock.count }.by(1)) end + + it "contains the inbox entry removal turbo stream action" do + subject + + expect(response.body).to include "turbo-stream action=\"remove\" target=\"inbox_#{inbox.id}" + end + end + + context "when all required parameters are given, but no inbox entry exists" do + let(:question) { FactoryBot.create(:question, author_identifier: "someidentifier") } + let(:params) do + { question: question.id } + end + + it "creates an anonymous block" do + expect { subject }.to(change { AnonymousBlock.count }.by(1)) + end + + it "doesn't contain the inbox entry removal turbo stream action" do + subject + + expect(response.body).not_to include "turbo-stream action=\"remove\" target=\"inbox_" + end end context "when blocking a user globally" do diff --git a/spec/controllers/answer_controller_spec.rb b/spec/controllers/answer_controller_spec.rb index b2c39fd5..108b3969 100644 --- a/spec/controllers/answer_controller_spec.rb +++ b/spec/controllers/answer_controller_spec.rb @@ -2,7 +2,9 @@ require "rails_helper" -describe AnswerController do +describe AnswerController, type: :controller do + include ActiveSupport::Testing::TimeHelpers + let(:user) do FactoryBot.create :user, otp_module: :disabled, @@ -39,4 +41,60 @@ describe AnswerController do end end end + + describe "#pin" do + subject { post :pin, params: { username: user.screen_name, id: answer.id }, format: :turbo_stream } + + context "user signed in" do + before(:each) { sign_in user } + + it "pins the answer" do + travel_to(Time.at(1603290950).utc) do + expect { subject }.to change { answer.reload.pinned_at }.from(nil).to(Time.at(1603290950).utc) + expect(response.body).to include("turbo-stream action=\"update\" target=\"ab-pin-#{answer.id}\"") + end + end + end + + context "other user signed in" do + let(:other_user) { FactoryBot.create(:user) } + + before(:each) { sign_in other_user } + + it "does not pin the answer" do + travel_to(Time.at(1603290950).utc) do + expect { subject }.not_to(change { answer.reload.pinned_at }) + end + end + end + end + + describe "#unpin" do + subject { delete :unpin, params: { username: user.screen_name, id: answer.id }, format: :turbo_stream } + + context "user signed in" do + before(:each) do + sign_in user + answer.update!(pinned_at: Time.at(1603290950).utc) + end + + it "unpins the answer" do + expect { subject }.to change { answer.reload.pinned_at }.from(Time.at(1603290950).utc).to(nil) + expect(response.body).to include("turbo-stream action=\"update\" target=\"ab-pin-#{answer.id}\"") + end + end + + context "other user signed in" do + let(:other_user) { FactoryBot.create(:user) } + + before(:each) do + sign_in other_user + answer.update!(pinned_at: Time.at(1603290950).utc) + end + + it "does not unpin the answer" do + expect { subject }.not_to(change { answer.reload.pinned_at }) + end + end + end end diff --git a/spec/controllers/concerns/turbo_streamable_spec.rb b/spec/controllers/concerns/turbo_streamable_spec.rb new file mode 100644 index 00000000..3a2c9c60 --- /dev/null +++ b/spec/controllers/concerns/turbo_streamable_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe TurboStreamable, type: :controller do + controller do + include TurboStreamable + + turbo_stream_actions :create, :blocked, :not_found + + def create + params.require :message + + respond_to do |format| + format.turbo_stream do + render turbo_stream: render_toast("success!") + end + end + end + + def blocked + raise Errors::Blocked + end + + def not_found + raise ActiveRecord::RecordNotFound + end + end + + before do + routes.draw do + get "create" => "anonymous#create" + get "blocked" => "anonymous#blocked" + get "not_found" => "anonymous#not_found" + end + end + + render_views + + shared_examples_for "it returns a toast as Turbo Stream response" do |action, message| + subject { get action, format: :turbo_stream } + + it "returns a toast as Turbo Stream response" do + subject + + expect(response.header["Content-Type"]).to include "text/vnd.turbo-stream.html" + expect(response.body).to include message + end + end + + describe "#create" do + context "gets called with the proper parameters" do + subject { get :create, format: :turbo_stream, params: { message: "test" } } + + it "returns a toast as Turbo Stream response" do + subject + + expect(response.header["Content-Type"]).to include "text/vnd.turbo-stream.html" + expect(response.body).to include "success!" + end + end + + context "gets called with the wrong parameters" do + it_behaves_like "it returns a toast as Turbo Stream response", :create, "Message is required" + end + end + + it_behaves_like "it returns a toast as Turbo Stream response", :create, "Message is required" + it_behaves_like "it returns a toast as Turbo Stream response", :blocked, "You have been blocked from performing this request" + it_behaves_like "it returns a toast as Turbo Stream response", :not_found, "Record not found" +end diff --git a/spec/controllers/services_controller_spec.rb b/spec/controllers/services_controller_spec.rb deleted file mode 100644 index 5e9582ab..00000000 --- a/spec/controllers/services_controller_spec.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -describe ServicesController, type: :controller do - describe "#index" do - subject { get :index } - - context "user signed in" do - let(:user) { FactoryBot.create(:user) } - - before { sign_in user } - - it "renders the services settings page with no services" do - subject - expect(response).to render_template("index") - expect(controller.instance_variable_get(:@services)).to be_empty - end - - context "user has a service token expired notification" do - let(:notification) do - Notification::ServiceTokenExpired.create( - target_id: user.id, - target_type: "User::ExpiredTwitterServiceConnection", - recipient_id: user.id, - new: true - ) - end - - it "marks the notification as read" do - expect { subject }.to change { notification.reload.new }.from(true).to(false) - end - end - - context "user has Twitter connected" do - before do - Services::Twitter.create(user:, uid: 12) - end - - it "renders the services settings page" do - subject - expect(response).to render_template("index") - expect(controller.instance_variable_get(:@services)).not_to be_empty - end - end - end - end - - describe "#create" do - subject { get :create, params: { provider: "twitter" } } - - context "successful Twitter sign in" do - let(:user) { FactoryBot.create(:user) } - - before do - sign_in user - OmniAuth.config.test_mode = true - OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({ - "provider" => "twitter", - "uid" => "12", - "info" => { "nickname" => "jack" }, - "credentials" => { "token" => "AAAA", "secret" => "BBBB" } - }) - request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] - end - - after do - OmniAuth.config.mock_auth[:twitter] = nil - end - - context "no services connected" do - it "creates a service integration" do - expect { subject }.to change { Service.count }.by(1) - end - end - - context "a user has a service connected" do - let(:other_user) { FactoryBot.create(:user) } - let!(:service) { Services::Twitter.create(user: other_user, uid: 12) } - - it "shows an error when trying to attach a service account which is already connected" do - subject - expect(flash[:error]).to eq("The Twitter account you are trying to connect is already connected to another #{APP_CONFIG['site_name']} account. If you are unable to disconnect the account yourself, please send us a Direct Message on Twitter: @retrospring.") - end - end - end - end - - describe "#update" do - subject { patch :update, params: } - - context "not signed in" do - let(:params) { { id: 1 } } - - it "redirects to sign in page" do - subject - expect(response).to redirect_to(new_user_session_path) - end - end - - context "user with Twitter connection" do - before { sign_in user } - - let(:user) { FactoryBot.create(:user) } - let(:service) { Services::Twitter.create(user:, uid: 12) } - let(:params) { { id: service.id, service: { post_tag: } } } - - context "tag is valid" do - let(:post_tag) { "#askaraccoon" } - - it "updates a service connection" do - expect { subject }.to change { service.reload.post_tag }.to("#askaraccoon") - expect(response).to redirect_to(services_path) - expect(flash[:success]).to eq("Service updated successfully.") - end - end - - context "tag is too long" do - let(:post_tag) { "a" * 21 } # 1 character over the limit - - it "shows an error" do - subject - expect(response).to redirect_to(services_path) - expect(flash[:error]).to eq("Unable to update service.") - end - end - end - end -end diff --git a/spec/controllers/settings/mutes_controller_spec.rb b/spec/controllers/settings/mutes_controller_spec.rb index 95a25b78..3b7579c5 100644 --- a/spec/controllers/settings/mutes_controller_spec.rb +++ b/spec/controllers/settings/mutes_controller_spec.rb @@ -19,7 +19,7 @@ describe Settings::MutesController, type: :controller do end describe "#create" do - subject { post :create, params: { muted_phrase: "foo" } } + subject { post :create, params: { muted_phrase: "foo" }, format: :turbo_stream } context "user signed in" do let(:user) { FactoryBot.create(:user) } @@ -29,11 +29,17 @@ describe Settings::MutesController, type: :controller do it "creates a mute rule" do expect { subject }.to(change { MuteRule.count }.by(1)) end + + it "contains a turbo stream append tag" do + subject + + expect(response.body).to include "turbo-stream action=\"append\" target=\"rules\"" + end end end describe "#destroy" do - subject { delete :destroy, params: } + subject { delete :destroy, params:, format: :turbo_stream } context "user signed in" do let(:user) { FactoryBot.create(:user) } @@ -47,6 +53,12 @@ describe Settings::MutesController, type: :controller do expect(MuteRule.exists?(rule.id)).to eq(false) end + + it "contains a turbo stream remove tag" do + subject + + expect(response.body).to include "turbo-stream action=\"remove\" target=\"rule_#{rule.id}\"" + end end end end diff --git a/spec/controllers/settings/sharing_controller_spec.rb b/spec/controllers/settings/sharing_controller_spec.rb new file mode 100644 index 00000000..6755845a --- /dev/null +++ b/spec/controllers/settings/sharing_controller_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Settings::SharingController, type: :controller do + describe "#edit" do + subject { get :edit } + + it "redirects to the sign in page when not signed in" do + expect(subject).to redirect_to new_user_session_path + end + + context "user signed in" do + let(:user) { FactoryBot.create(:user) } + + before { sign_in user } + + it "renders the edit template" do + expect(subject).to render_template(:edit) + end + end + end + + describe "#update" do + subject { patch :update, params: { user: user_params } } + let(:user_params) do + { + sharing_enabled: "1", + sharing_autoclose: "1", + sharing_custom_url: "", + } + end + + it "redirects to the sign in page when not signed in" do + expect(subject).to redirect_to new_user_session_path + end + + context "user signed in" do + let(:user) { FactoryBot.create :user } + + before { sign_in user } + + it "renders the edit template" do + expect(subject).to render_template(:edit) + end + + it "updates the user's sharing preferences" do + expect { subject } + .to change { user.reload.slice(:sharing_enabled, :sharing_autoclose, :sharing_custom_url).values } + .from([false, false, nil]) + .to([true, true, ""]) + end + + it "sets the success flash" do + subject + expect(flash[:success]).to eq(I18n.t("settings.sharing.update.success")) + end + + context "when passed a valid url" do + let(:user_params) do + super().merge( + sharing_custom_url: "https://example.com/share?text=" + ) + end + + it "renders the edit template" do + expect(subject).to render_template(:edit) + end + + it "updates the user's sharing preferences" do + expect { subject } + .to change { user.reload.slice(:sharing_enabled, :sharing_autoclose, :sharing_custom_url).values } + .from([false, false, nil]) + .to([true, true, "https://example.com/share?text="]) + end + + it "sets the success flash" do + subject + expect(flash[:success]).to eq(I18n.t("settings.sharing.update.success")) + end + end + + context "when passed an invalid url" do + let(:user_params) do + super().merge( + sharing_custom_url: "nfs://example.com/data" + ) + end + + it "renders the edit template" do + expect(subject).to render_template(:edit) + end + + it "updates the user's sharing preferences" do + expect { subject } + .not_to(change { user.reload.slice(:sharing_enabled, :sharing_autoclose, :sharing_custom_url).values }) + end + + it "sets the error flash" do + subject + expect(flash[:error]).to eq(I18n.t("settings.sharing.update.error")) + end + end + + context "when unticking boolean settings" do + let(:user_params) do + super().merge( + sharing_enabled: "0", + sharing_autoclose: "0" + ) + end + + let(:user) { FactoryBot.create :user, sharing_enabled: true, sharing_autoclose: true, sharing_custom_url: "https://example.com/share?text=" } + + it "renders the edit template" do + expect(subject).to render_template(:edit) + end + + it "updates the user's sharing preferences" do + expect { subject } + .to change { user.reload.slice(:sharing_enabled, :sharing_autoclose, :sharing_custom_url).values } + .from([true, true, "https://example.com/share?text="]) + .to([false, false, ""]) + end + + it "sets the success flash" do + subject + expect(flash[:success]).to eq(I18n.t("settings.sharing.update.success")) + end + end + end + end +end diff --git a/spec/controllers/well_known/node_info_controller_spec.rb b/spec/controllers/well_known/node_info_controller_spec.rb index 19de310c..c4728d0a 100644 --- a/spec/controllers/well_known/node_info_controller_spec.rb +++ b/spec/controllers/well_known/node_info_controller_spec.rb @@ -13,9 +13,9 @@ describe WellKnown::NodeInfoController do "links" => [ { "rel" => "http://nodeinfo.diaspora.software/ns/schema/2.1", - "href" => "http://test.host/nodeinfo/2.1" + "href" => "http://test.host/nodeinfo/2.1", } - ] + ], }) end end @@ -49,42 +49,6 @@ describe WellKnown::NodeInfoController do end end - context "Twitter integration enabled" do - before do - stub_const("APP_CONFIG", { - "sharing" => { - "twitter" => { - "enabled" => true - } - } - }) - end - - it "includes Twitter in outbound services" do - subject - parsed = JSON.parse(response.body) - expect(parsed.dig("services", "outbound")).to include("twitter") - end - end - - context "Twitter integration disabled" do - before do - stub_const("APP_CONFIG", { - "sharing" => { - "twitter" => { - "enabled" => false - } - } - }) - end - - it "includes Twitter in outbound services" do - subject - parsed = JSON.parse(response.body) - expect(parsed.dig("services", "outbound")).to_not include("twitter") - end - end - context "site has users" do let(:num_users) { rand(10..50) } diff --git a/spec/helpers/markdown_helper_spec.rb b/spec/helpers/markdown_helper_spec.rb index b5ecf606..177b42c7 100644 --- a/spec/helpers/markdown_helper_spec.rb +++ b/spec/helpers/markdown_helper_spec.rb @@ -40,24 +40,8 @@ describe MarkdownHelper, type: :helper do end describe "#twitter_markdown" do - context "mentioned user has Twitter connected" do - let(:user) { FactoryBot.create(:user) } - let(:service) { Services::Twitter.create(user: user) } - - before do - service.nickname = "test" - service.save! - end - - it "should transform a internal mention to a Twitter mention" do - expect(twitter_markdown("@#{user.screen_name}")).to eq("@test") - end - end - - context "mentioned user doesnt have Twitter connected" do - it "should not transform the mention" do - expect(twitter_markdown("@test")).to eq("test") - end + it "should not transform the mention" do + expect(twitter_markdown("@test")).to eq("test") end it "should not strip weird hearts" do @@ -118,17 +102,9 @@ describe MarkdownHelper, type: :helper do describe "#twitter_markdown_io" do let(:user) { FactoryBot.create(:user) } - let(:service) { Services::Twitter.create(user: user) } - - before do - user.screen_name = "test" - user.save! - service.nickname = "ObamaGaming" - service.save! - end it "should return the expected text" do - expect(twitter_markdown_io("spec/fixtures/markdown/twitter.md")).to eq("@ObamaGaming") + expect(twitter_markdown_io("spec/fixtures/markdown/twitter.md")).to eq("test") end end diff --git a/spec/lib/use_case/answer/pin_spec.rb b/spec/lib/use_case/answer/pin_spec.rb new file mode 100644 index 00000000..26fa29d4 --- /dev/null +++ b/spec/lib/use_case/answer/pin_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe UseCase::Answer::Pin do + include ActiveSupport::Testing::TimeHelpers + + subject { UseCase::Answer::Pin.call(user:, answer:) } + + context "answer exists" do + let(:answer) { FactoryBot.create(:answer, user: FactoryBot.create(:user)) } + + context "as answer owner" do + let(:user) { answer.user } + + it "pins the answer" do + travel_to(Time.at(1603290950).utc) do + expect { subject }.to change { answer.pinned_at }.from(nil).to(Time.at(1603290950).utc) + end + end + + context "answer is already pinned" do + before do + answer.update!(pinned_at: Time.at(1603290950).utc) + end + + it "raises an error" do + expect { subject }.to raise_error(Errors::BadRequest) + expect(answer.reload.pinned_at).to eq(Time.at(1603290950).utc) + end + end + end + + context "as other user" do + let(:user) { FactoryBot.create(:user) } + + it "does not pin the answer" do + expect { subject }.to raise_error(Errors::NotAuthorized) + expect(answer.reload.pinned_at).to eq(nil) + end + end + end +end diff --git a/spec/lib/use_case/answer/unpin_spec.rb b/spec/lib/use_case/answer/unpin_spec.rb new file mode 100644 index 00000000..f4d7a3cb --- /dev/null +++ b/spec/lib/use_case/answer/unpin_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe UseCase::Answer::Unpin do + include ActiveSupport::Testing::TimeHelpers + + subject { UseCase::Answer::Unpin.call(user:, answer:) } + + context "answer exists" do + let(:pinned_at) { Time.at(1603290950).utc } + let(:answer) { FactoryBot.create(:answer, user: FactoryBot.create(:user), pinned_at:) } + + context "as answer owner" do + let(:user) { answer.user } + + it "unpins the answer" do + expect { subject }.to change { answer.pinned_at }.from(pinned_at).to(nil) + end + + context "answer is already unpinned" do + let(:pinned_at) { nil } + + it "raises an error" do + expect { subject }.to raise_error(Errors::BadRequest) + expect(answer.reload.pinned_at).to eq(nil) + end + end + end + + context "as other user" do + let(:user) { FactoryBot.create(:user) } + + it "does not unpin the answer" do + expect { subject }.to raise_error(Errors::NotAuthorized) + expect(answer.reload.pinned_at).to eq(pinned_at) + end + end + end +end diff --git a/spec/lib/use_case/data_export/answers_spec.rb b/spec/lib/use_case/data_export/answers_spec.rb index 3fa24f8e..99335a91 100644 --- a/spec/lib/use_case/data_export/answers_spec.rb +++ b/spec/lib/use_case/data_export/answers_spec.rb @@ -32,7 +32,8 @@ describe UseCase::DataExport::Answers, :data_export do user_id: user.id, created_at: "2022-12-10T13:37:42.000Z", updated_at: "2022-12-10T13:37:42.000Z", - smile_count: 0 + smile_count: 0, + pinned_at: nil, } ] } diff --git a/spec/lib/use_case/data_export/user_spec.rb b/spec/lib/use_case/data_export/user_spec.rb index f9c70ff9..9f01e1ef 100644 --- a/spec/lib/use_case/data_export/user_spec.rb +++ b/spec/lib/use_case/data_export/user_spec.rb @@ -22,6 +22,9 @@ describe UseCase::DataExport::User, :data_export do privacy_allow_public_timeline: false, privacy_allow_stranger_answers: false, privacy_show_in_search: true, + sharing_enabled: false, + sharing_autoclose: false, + sharing_custom_url: nil, screen_name: "fizzyraccoon", show_foreign_themes: true, sign_in_count: 10, @@ -85,7 +88,10 @@ describe UseCase::DataExport::User, :data_export do privacy_lock_inbox: false, privacy_require_user: false, privacy_hide_social_graph: false, - privacy_noindex: false + privacy_noindex: false, + sharing_enabled: false, + sharing_autoclose: false, + sharing_custom_url: nil }, profile: { display_name: "Fizzy Raccoon", diff --git a/spec/models/services/twitter_spec.rb b/spec/models/services/twitter_spec.rb deleted file mode 100644 index c12870bb..00000000 --- a/spec/models/services/twitter_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Services::Twitter do - describe "#post" do - let(:user) { FactoryBot.create(:user) } - let(:service) { Services::Twitter.create(user: user) } - let(:answer) { FactoryBot.create(:answer, user: user, - content: 'a' * 255, - question_content: 'q' * 255) } - let(:twitter_client) { instance_double(Twitter::REST::Client) } - - before do - allow(Twitter::REST::Client).to receive(:new).and_return(twitter_client) - allow(twitter_client).to receive(:update!) - stub_const("APP_CONFIG", { - 'hostname' => 'example.com', - 'https' => true, - 'items_per_page' => 5, - 'sharing' => { - 'twitter' => { - 'consumer_key' => 'AAA', - } - } - }) - end - - it "posts a shortened tweet" do - service.post(answer) - - expect(twitter_client).to have_received(:update!).with("#{'q' * 123}… — #{'a' * 124}… https://example.com/@#{user.screen_name}/a/#{answer.id}") - end - - it "posts an un-shortened tweet" do - answer.question.content = 'Why are raccoons so good?' - answer.question.save! - answer.content = 'Because they are good cunes.' - answer.save! - - service.post(answer) - - expect(twitter_client).to have_received(:update!).with("#{answer.question.content} — #{answer.content} https://example.com/@#{user.screen_name}/a/#{answer.id}") - end - end -end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f1f2abdc..01d3fc47 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -33,6 +33,40 @@ RSpec.describe User, type: :model do end end + describe "custom sharing url validation" do + subject do + FactoryBot.build(:user, sharing_custom_url: url).tap(&:validate).errors[:sharing_custom_url] + end + + shared_examples_for "valid url" do |example_url| + context "when url is #{example_url}" do + let(:url) { example_url } + + it "does not have validation errors" do + expect(subject).to be_empty + end + end + end + + shared_examples_for "invalid url" do |example_url| + context "when url is #{example_url}" do + let(:url) { example_url } + + it "has validation errors" do + expect(subject).not_to be_empty + end + end + end + + include_examples "valid url", "https://myfunnywebsite.com/" + include_examples "valid url", "https://desu.social/share?text=" + include_examples "valid url", "http://insecurebutvalid.business/" + include_examples "invalid url", "ftp://fileprotocols.cool/" + include_examples "invalid url", "notevenanurl" + include_examples "invalid url", %(https://richtig oarger shice) # passes the regexp, but trips up URI.parse + include_examples "invalid url", %(https://österreich.gv.at) # needs to be ASCII + end + describe "email validation" do subject do FactoryBot.build(:user, email: email).tap(&:validate).errors[:email] diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 90ade2fe..43662e83 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -60,6 +60,10 @@ RSpec.configure do |config| # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + # anonymous controllers will infer ApplicationController instead of the + # described class + config.infer_base_class_for_anonymous_controllers = false + config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :helper end diff --git a/spec/workers/share_worker_spec.rb b/spec/workers/share_worker_spec.rb deleted file mode 100644 index c8003258..00000000 --- a/spec/workers/share_worker_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -describe ShareWorker do - let(:user) { FactoryBot.create(:user) } - let(:answer) { FactoryBot.create(:answer, user: user) } - let!(:service) do - Services::Twitter.create!(type: "Services::Twitter", - user: user) - end - - before do - stub_const("APP_CONFIG", { - "hostname" => "example.com", - "anonymous_name" => "Anonymous", - "https" => true, - "items_per_page" => 5, - "sharing" => { - "twitter" => { - "consumer_key" => "" - } - } - }) - end - - describe "#perform" do - before do - allow(Sidekiq.logger).to receive(:info) - end - - subject do - Sidekiq::Testing.fake! do - ShareWorker.perform_async(user.id, answer.id, "twitter") - end - end - - context "when answer doesn't exist" do - it "prevents the job from retrying and logs a message" do - answer.destroy! - expect { subject }.to change(ShareWorker.jobs, :size).by(1) - expect { ShareWorker.drain }.to change(ShareWorker.jobs, :size).by(-1) - expect(Sidekiq.logger).to have_received(:info).with("Tried to post answer ##{answer.id} for user ##{user.id} to Twitter but the user/answer/service did not exist (likely deleted), will not retry.") - end - end - - context "when answer exists" do - it "handles Twitter::Error::DuplicateStatus" do - allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::DuplicateStatus) - subject - ShareWorker.drain - expect(Sidekiq.logger).to have_received(:info).with("Tried to post answer ##{answer.id} from user ##{user.id} to Twitter but the status was already posted.") - end - - it "handles Twitter::Error::Unauthorized" do - allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized) - subject - ShareWorker.drain - expect(Sidekiq.logger).to have_received(:info).with("Tried to post answer ##{answer.id} from user ##{user.id} to Twitter but the token has expired or been revoked.") - end - - it "revokes the service connection when Twitter::Error::Unauthorized is raised" do - allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized) - subject - expect { ShareWorker.drain }.to change { Services::Twitter.count }.by(-1) - end - - it "sends the user a notification when Twitter::Error::Unauthorized is raised" do - allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized) - subject - expect { ShareWorker.drain }.to change { Notification::ServiceTokenExpired.count }.by(1) - end - - it "handles Twitter::Error::Forbidden" do - allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Forbidden) - subject - ShareWorker.drain - expect(Sidekiq.logger).to have_received(:info).with("Tried to post answer ##{answer.id} from user ##{user.id} to Twitter but the account is suspended.") - end - - it "retries on unhandled exceptions" do - expect { subject }.to change(ShareWorker.jobs, :size).by(1) - expect { ShareWorker.drain }.to raise_error(Twitter::Error::BadRequest) - expect(Sidekiq.logger).to have_received(:info) - end - end - end -end diff --git a/yarn.lock b/yarn.lock index f38e6b8f..6324ee68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,115 +49,115 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== -"@esbuild/android-arm64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.5.tgz#a145f43018e639bed94ed637369e2dcdd6bf9ea2" - integrity sha512-KHWkDqYAMmKZjY4RAN1PR96q6UOtfkWlTS8uEwWxdLtkRt/0F/csUhXIrVfaSIFxnscIBMPynGfhsMwQDRIBQw== +"@esbuild/android-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz#b3d5b65a3b2e073a6c7ee36b1f3c30c8f000315b" + integrity sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ== -"@esbuild/android-arm@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.5.tgz#9fa2deff7fc5d180bb4ecff70beea3a95ac44251" - integrity sha512-crmPUzgCmF+qZXfl1YkiFoUta2XAfixR1tEnr/gXIixE+WL8Z0BGqfydP5oox0EUOgQMMRgtATtakyAcClQVqQ== +"@esbuild/android-arm@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.8.tgz#c41e496af541e175369d48164d0cf01a5f656cf6" + integrity sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w== -"@esbuild/android-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.5.tgz#145fc61f810400e65a56b275280d1422a102c2ef" - integrity sha512-8fI/AnIdmWz/+1iza2WrCw8kwXK9wZp/yZY/iS8ioC+U37yJCeppi9EHY05ewJKN64ASoBIseufZROtcFnX5GA== +"@esbuild/android-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.8.tgz#080fa67c29be77f5a3ca5ee4cc78d5bf927e3a3b" + integrity sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg== -"@esbuild/darwin-arm64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.5.tgz#61fb0546aa4bae0850817d6e0d008b1cb3f64b49" - integrity sha512-EAvaoyIySV6Iif3NQCglUNpnMfHSUgC5ugt2efl3+QDntucJe5spn0udNZjTgNi6tKVqSceOw9tQ32liNZc1Xw== +"@esbuild/darwin-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz#053622bf9a82f43d5c075b7818e02618f7b4a397" + integrity sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg== -"@esbuild/darwin-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.5.tgz#54b770f0c49f524ae9ba24c85d6dea8b521f610d" - integrity sha512-ha7QCJh1fuSwwCgoegfdaljowwWozwTDjBgjD3++WAy/qwee5uUi1gvOg2WENJC6EUyHBOkcd3YmLDYSZ2TPPA== +"@esbuild/darwin-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz#8a1aadb358d537d8efad817bb1a5bff91b84734b" + integrity sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw== -"@esbuild/freebsd-arm64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.5.tgz#be1dd18b7b9411f10bdc362ba8bff16386175367" - integrity sha512-VbdXJkn2aI2pQ/wxNEjEcnEDwPpxt3CWWMFYmO7CcdFBoOsABRy2W8F3kjbF9F/pecEUDcI3b5i2w+By4VQFPg== +"@esbuild/freebsd-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz#e6738d0081ba0721a5c6c674e84c6e7fcea61989" + integrity sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A== -"@esbuild/freebsd-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.5.tgz#c9c1960fa3e1eada4e5d4be2a11a2f04ce14198f" - integrity sha512-olgGYND1/XnnWxwhjtY3/ryjOG/M4WfcA6XH8dBTH1cxMeBemMODXSFhkw71Kf4TeZFFTN25YOomaNh0vq2iXg== +"@esbuild/freebsd-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz#1855e562f2b730f4483f6e94086e9e2597feb4c3" + integrity sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q== -"@esbuild/linux-arm64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.5.tgz#34d96d11c6899017ecae42fb97de8e0c3282902f" - integrity sha512-8a0bqSwu3OlLCfu2FBbDNgQyBYdPJh1B9PvNX7jMaKGC9/KopgHs37t+pQqeMLzcyRqG6z55IGNQAMSlCpBuqg== +"@esbuild/linux-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz#481da38952721a3fdb77c17a36ceaacc4270b5c5" + integrity sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww== -"@esbuild/linux-arm@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.5.tgz#86332e6293fd713a54ab299a5e2ed7c60c9e1c07" - integrity sha512-YBdCyQwA3OQupi6W2/WO4FnI+NWFWe79cZEtlbqSESOHEg7a73htBIRiE6uHPQe7Yp5E4aALv+JxkRLGEUL7tw== +"@esbuild/linux-arm@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz#18127072b270bb6321c6d11be20bfd30e0d6ad17" + integrity sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA== -"@esbuild/linux-ia32@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.5.tgz#7bd9185c844e7dfce6a01dfdec584e115602a8c4" - integrity sha512-uCwm1r/+NdP7vndctgq3PoZrnmhmnecWAr114GWMRwg2QMFFX+kIWnp7IO220/JLgnXK/jP7VKAFBGmeOYBQYQ== +"@esbuild/linux-ia32@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz#ee400af7b3bc69e8ca2e593ca35156ffb9abd54f" + integrity sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA== -"@esbuild/linux-loong64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.5.tgz#2907d4120c7b3642b96be6014f77e7624c378eea" - integrity sha512-3YxhSBl5Sb6TtBjJu+HP93poBruFzgXmf3PVfIe4xOXMj1XpxboYZyw3W8BhoX/uwxzZz4K1I99jTE/5cgDT1g== +"@esbuild/linux-loong64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz#8c509d8a454693d39824b83b3f66c400872fce82" + integrity sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA== -"@esbuild/linux-mips64el@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.5.tgz#fc98be741e8080ecd13b404d5fca5302d3835bf4" - integrity sha512-Hy5Z0YVWyYHdtQ5mfmfp8LdhVwGbwVuq8mHzLqrG16BaMgEmit2xKO+iDakHs+OetEx0EN/2mUzDdfdktI+Nmg== +"@esbuild/linux-mips64el@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz#f2b0d36e63fb26bc3f95b203b6a80638292101ca" + integrity sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA== -"@esbuild/linux-ppc64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.5.tgz#ea12e8f6b290a613ac4903c9e00835c69ced065c" - integrity sha512-5dbQvBLbU/Y3Q4ABc9gi23hww1mQcM7KZ9KBqabB7qhJswYMf8WrDDOSw3gdf3p+ffmijMd28mfVMvFucuECyg== +"@esbuild/linux-ppc64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz#1e628be003e036e90423716028cc884fe5ba25bd" + integrity sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw== -"@esbuild/linux-riscv64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.5.tgz#ce47b15fd4227eeb0590826e41bdc430c5bfd06c" - integrity sha512-fp/KUB/ZPzEWGTEUgz9wIAKCqu7CjH1GqXUO2WJdik1UNBQ7Xzw7myIajpxztE4Csb9504ERiFMxZg5KZ6HlZQ== +"@esbuild/linux-riscv64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz#419a815cb4c3fb9f1b78ef5295f5b48b8bf6427a" + integrity sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw== -"@esbuild/linux-s390x@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.5.tgz#962fa540d7498967270eb1d4b9ac6c4a4f339735" - integrity sha512-kRV3yw19YDqHTp8SfHXfObUFXlaiiw4o2lvT1XjsPZ++22GqZwSsYWJLjMi1Sl7j9qDlDUduWDze/nQx0d6Lzw== +"@esbuild/linux-s390x@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz#291c49ae5c3d11d226352755c0835911fe1a9e5c" + integrity sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw== -"@esbuild/linux-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.5.tgz#9fa52884c3d876593a522aa1d4df43b717907050" - integrity sha512-vnxuhh9e4pbtABNLbT2ANW4uwQ/zvcHRCm1JxaYkzSehugoFd5iXyC4ci1nhXU13mxEwCnrnTIiiSGwa/uAF1g== +"@esbuild/linux-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz#03199d91c76faf80bd54104f5cbf0a489bc39f6a" + integrity sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA== -"@esbuild/netbsd-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.5.tgz#47bb187b86aad9622051cb80c27e439b7d9e3a9a" - integrity sha512-cigBpdiSx/vPy7doUyImsQQBnBjV5f1M99ZUlaJckDAJjgXWl6y9W17FIfJTy8TxosEF6MXq+fpLsitMGts2nA== +"@esbuild/netbsd-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz#b436d767e1b21852f9ed212e2bb57f77203b0ae2" + integrity sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q== -"@esbuild/openbsd-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.5.tgz#abc55c35a1ed2bc3c5ede2ef50a3b2f87395009a" - integrity sha512-VdqRqPVIjjZfkf40LrqOaVuhw9EQiAZ/GNCSM2UplDkaIzYVsSnycxcFfAnHdWI8Gyt6dO15KHikbpxwx+xHbw== +"@esbuild/openbsd-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz#d1481d8539e21d4729cd04a0450a26c2c8789e89" + integrity sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg== -"@esbuild/sunos-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.5.tgz#b83c080a2147662599a5d18b2ff47f07c93e03a0" - integrity sha512-ItxPaJ3MBLtI4nK+mALLEoUs6amxsx+J1ibnfcYMkqaCqHST1AkF4aENpBehty3czqw64r/XqL+W9WqU6kc2Qw== +"@esbuild/sunos-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz#2cfb8126e079b2c00fd1bf095541e9f5c47877e4" + integrity sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw== -"@esbuild/win32-arm64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.5.tgz#2a4c41f427d9cf25b75f9d61493711a482106850" - integrity sha512-4u2Q6qsJTYNFdS9zHoAi80spzf78C16m2wla4eJPh4kSbRv+BpXIfl6TmBSWupD8e47B1NrTfrOlEuco7mYQtg== +"@esbuild/win32-arm64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz#7c6ecfd097ca23b82119753bf7072bbaefe51e3a" + integrity sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg== -"@esbuild/win32-ia32@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.5.tgz#7c14e3250725d0e2c21f89c98eb6abb520cba0e0" - integrity sha512-KYlm+Xu9TXsfTWAcocLuISRtqxKp/Y9ZBVg6CEEj0O5J9mn7YvBKzAszo2j1ndyzUPk+op+Tie2PJeN+BnXGqQ== +"@esbuild/win32-ia32@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz#cffec63c3cb0ef8563a04df4e09fa71056171d00" + integrity sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w== -"@esbuild/win32-x64@0.17.5": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.5.tgz#a8f3d26d8afc5186eccda265ceb1820b8e8830be" - integrity sha512-XgA9qWRqby7xdYXuF6KALsn37QGBMHsdhmnpjfZtYxKxbTOwfnDM6MYi2WuUku5poNaX2n9XGVr20zgT/2QwCw== +"@esbuild/win32-x64@0.17.8": + version "0.17.8" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" + integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -179,28 +179,28 @@ resolved "https://registry.yarnpkg.com/@fontsource/lexend/-/lexend-4.5.15.tgz#ee033b850224d05b665d6661bf8d32c950f166bb" integrity sha512-6edLmDmte8pJWtQ1NIahcJq0iWlf6b0/JLwkd7WbDQ3C5tZWnxjvbP4RSklMv4MPzGjpuYuYnc+QANbjUws1oA== -"@fortawesome/fontawesome-free@^6.2.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz#344baf6ff9eaad7a73cff067d8c56bfc11ae5304" - integrity sha512-viouXhegu/TjkvYQoiRZK3aax69dGXxgEjpvZW81wIJdxm5Fnvp3VVIP4VHKqX4SvFw6qpmkILkD4RJWAdrt7A== +"@fortawesome/fontawesome-free@^6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz#b5877182692a6f7a39d1108837bec24247ba4bd7" + integrity sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA== "@hotwired/stimulus@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.1.tgz#e3de23623b0c52c247aba4cd5d530d257008676b" integrity sha512-HGlzDcf9vv/EQrMJ5ZG6VWNs8Z/xMN+1o2OhV1gKiSG6CqZt5MCBB1gRg5ILiN3U0jEAxuDTNPRfBcnZBDmupQ== -"@hotwired/turbo-rails@^7.2.4": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.2.4.tgz#d155533e79c4ebdac23e8fe12697d821d5c06307" - integrity sha512-givDUQqaccd19BvErz1Cf2j6MXF74m0G6I75oqFJGeXAa7vwkz9nDplefVNrALCR9Xi9j9gy32xmSI6wD0tZyA== +"@hotwired/turbo-rails@^7.2.5": + version "7.2.5" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.2.5.tgz#74fc3395a29a76df2bb8835aa88c86885cffde4c" + integrity sha512-F8ztmARxd/XBdevRa//HoJGZ7u+Unb0J7cQUeUP+pBvt9Ta2TJJ7a2TORAOhjC8Zgxx+LKwm/1UUHqN3ojjiGw== dependencies: - "@hotwired/turbo" "^7.2.4" + "@hotwired/turbo" "^7.2.5" "@rails/actioncable" "^7.0" -"@hotwired/turbo@^7.2.4": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.2.4.tgz#0d35541be32cfae3b4f78c6ab9138f5b21f28a21" - integrity sha512-c3xlOroHp/cCZHDOuLp6uzQYEbvXBUVaal0puXoGJ9M8L/KHwZ3hQozD4dVeSN9msHWLxxtmPT1TlCN7gFhj4w== +"@hotwired/turbo@^7.2.5": + version "7.2.5" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.2.5.tgz#2d9d6bde8a9549c3aea8970445ade16ffd56719a" + integrity sha512-o5PByC/mWkmTe4pWnKrixhPECJUxIT/NHtxKqjq7n9Fj6JlNza1pgxdTCJVIq+PI0j95U+7mA3N4n4A/QYZtZQ== "@humanwhocodes/config-array@^0.5.0": version "0.5.0" @@ -835,33 +835,33 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild@^0.17.5: - version "0.17.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.5.tgz#cd76d75700d49ac050ad9eedfbed777bd6a9d930" - integrity sha512-Bu6WLCc9NMsNoMJUjGl3yBzTjVLXdysMltxQWiLAypP+/vQrf+3L1Xe8fCXzxaECus2cEJ9M7pk4yKatEwQMqQ== +esbuild@^0.17.8: + version "0.17.8" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.8.tgz#f7f799abc7cdce3f0f2e3e0c01f120d4d55193b4" + integrity sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g== optionalDependencies: - "@esbuild/android-arm" "0.17.5" - "@esbuild/android-arm64" "0.17.5" - "@esbuild/android-x64" "0.17.5" - "@esbuild/darwin-arm64" "0.17.5" - "@esbuild/darwin-x64" "0.17.5" - "@esbuild/freebsd-arm64" "0.17.5" - "@esbuild/freebsd-x64" "0.17.5" - "@esbuild/linux-arm" "0.17.5" - "@esbuild/linux-arm64" "0.17.5" - "@esbuild/linux-ia32" "0.17.5" - "@esbuild/linux-loong64" "0.17.5" - "@esbuild/linux-mips64el" "0.17.5" - "@esbuild/linux-ppc64" "0.17.5" - "@esbuild/linux-riscv64" "0.17.5" - "@esbuild/linux-s390x" "0.17.5" - "@esbuild/linux-x64" "0.17.5" - "@esbuild/netbsd-x64" "0.17.5" - "@esbuild/openbsd-x64" "0.17.5" - "@esbuild/sunos-x64" "0.17.5" - "@esbuild/win32-arm64" "0.17.5" - "@esbuild/win32-ia32" "0.17.5" - "@esbuild/win32-x64" "0.17.5" + "@esbuild/android-arm" "0.17.8" + "@esbuild/android-arm64" "0.17.8" + "@esbuild/android-x64" "0.17.8" + "@esbuild/darwin-arm64" "0.17.8" + "@esbuild/darwin-x64" "0.17.8" + "@esbuild/freebsd-arm64" "0.17.8" + "@esbuild/freebsd-x64" "0.17.8" + "@esbuild/linux-arm" "0.17.8" + "@esbuild/linux-arm64" "0.17.8" + "@esbuild/linux-ia32" "0.17.8" + "@esbuild/linux-loong64" "0.17.8" + "@esbuild/linux-mips64el" "0.17.8" + "@esbuild/linux-ppc64" "0.17.8" + "@esbuild/linux-riscv64" "0.17.8" + "@esbuild/linux-s390x" "0.17.8" + "@esbuild/linux-x64" "0.17.8" + "@esbuild/netbsd-x64" "0.17.8" + "@esbuild/openbsd-x64" "0.17.8" + "@esbuild/sunos-x64" "0.17.8" + "@esbuild/win32-arm64" "0.17.8" + "@esbuild/win32-ia32" "0.17.8" + "@esbuild/win32-x64" "0.17.8" escape-string-regexp@^1.0.5: version "1.0.5" @@ -2110,10 +2110,10 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -sass@^1.57.1: - version "1.57.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5" - integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw== +sass@^1.58.0: + version "1.58.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.0.tgz#ee8aea3ad5ea5c485c26b3096e2df6087d0bb1cc" + integrity sha512-PiMJcP33DdKtZ/1jSjjqVIKihoDc6yWmYr9K/4r3fVVIEDAluD0q7XZiRKrNJcPK3qkLRF/79DND1H5q1LBjgg== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -2176,12 +2176,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -"source-map-js@>=0.6.2 <2.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" - integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== - -source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -2318,10 +2313,10 @@ stylelint-config-standard@^29.0.0: dependencies: stylelint-config-recommended "^9.0.0" -stylelint-scss@^4.0.0, stylelint-scss@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.3.0.tgz#638800faf823db11fff60d537c81051fe74c90fa" - integrity sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ== +stylelint-scss@^4.0.0, stylelint-scss@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.4.0.tgz#87ce9d049eff1ce67cce788780fbfda63099017e" + integrity sha512-Qy66a+/30aylFhPmUArHhVsHOun1qrO93LGT15uzLuLjWS7hKDfpFm34mYo1ndR4MCo8W4bEZM1+AlJRJORaaw== dependencies: lodash "^4.17.21" postcss-media-query-parser "^0.2.3" @@ -2512,10 +2507,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@^4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== unbox-primitive@^1.0.1: version "1.0.1"