Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
a7b9c31c48
|
@ -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 }}
|
|
@ -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:
|
||||
|
|
|
@ -131,3 +131,6 @@ Style/Encoding:
|
|||
|
||||
Style/EndlessMethod:
|
||||
EnforcedStyle: allow_always
|
||||
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
EnforcedStyleForMultiline: consistent_comma
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
15
Gemfile
15
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"
|
||||
|
|
103
Gemfile.lock
103
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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string, string>;
|
||||
declare readonly autoCloseValue: boolean;
|
||||
|
||||
connect(): void {
|
||||
if (this.autoCloseValue) {
|
||||
this.twitterTarget.addEventListener('click', () => this.close());
|
||||
this.tumblrTarget.addEventListener('click', () => this.close());
|
||||
|
||||
if (this.hasCustomTarget) {
|
||||
this.customTarget.addEventListener('click', () => this.close());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configValueChanged(value: Record<string, string>): void {
|
||||
if (Object.keys(value).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element.classList.remove('d-none');
|
||||
|
||||
this.twitterTarget.href = this.configValue['twitter'];
|
||||
this.tumblrTarget.href = this.configValue['tumblr'];
|
||||
|
||||
if (this.hasCustomTarget) {
|
||||
this.customTarget.href = `${this.customTarget.href}${this.configValue['custom']}`;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
(this.element.closest(".inbox-entry")).remove();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { showNotification } from "utilities/notifications";
|
||||
|
||||
export default class extends Controller<HTMLElement> {
|
||||
static values = {
|
||||
message: String,
|
||||
success: Boolean
|
||||
};
|
||||
|
||||
declare readonly messageValue: string;
|
||||
declare readonly successValue: boolean;
|
||||
|
||||
connect(): void {
|
||||
showNotification(this.messageValue, this.successValue);
|
||||
this.element.remove();
|
||||
}
|
||||
}
|
|
@ -9,16 +9,9 @@ export function answerEntryHandler(event: Event): void {
|
|||
|
||||
element.disabled = true;
|
||||
|
||||
const shareTo = [];
|
||||
inboxEntry.querySelectorAll('input[type=checkbox][name=ib-share]:checked')
|
||||
.forEach((element: HTMLInputElement) => {
|
||||
shareTo.push(element.getAttribute('data-service'));
|
||||
});
|
||||
|
||||
const data = {
|
||||
id: element.getAttribute('data-ib-id'),
|
||||
answer: inboxEntry.querySelector<HTMLInputElement>('textarea[name=ib-answer]')?.value,
|
||||
share: JSON.stringify(shareTo),
|
||||
inbox: 'true'
|
||||
};
|
||||
|
||||
|
@ -36,7 +29,14 @@ export function answerEntryHandler(event: Event): void {
|
|||
}
|
||||
updateDeleteButton(false);
|
||||
showNotification(data.message);
|
||||
(inboxEntry as HTMLElement).remove();
|
||||
|
||||
const sharing = inboxEntry.querySelector<HTMLElement>('.inbox-entry__sharing');
|
||||
if (sharing != null) {
|
||||
sharing.dataset.inboxSharingConfigValue = JSON.stringify(data.sharing);
|
||||
}
|
||||
else {
|
||||
(inboxEntry as HTMLElement).remove();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
|
@ -51,4 +51,4 @@ export function answerEntryInputHandler(event: KeyboardEvent): void {
|
|||
if (event.keyCode == 13 && (event.ctrlKey || event.metaKey)) {
|
||||
document.querySelector<HTMLButtonElement>(`button[name="ib-answer"][data-ib-id="${inboxId}"]`).click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import registerEvents from 'utilities/registerEvents';
|
||||
import { answerEntryHandler, answerEntryInputHandler } from './answer';
|
||||
import { deleteEntryHandler } from './delete';
|
||||
import optionsEntryHandler from './options';
|
||||
import { reportEventHandler } from './report';
|
||||
|
||||
export default (): void => {
|
||||
|
@ -9,7 +8,6 @@ export default (): void => {
|
|||
{ type: 'click', target: 'button[name="ib-answer"]', handler: answerEntryHandler, global: true },
|
||||
{ type: 'click', target: '[name="ib-destroy"]', handler: deleteEntryHandler, global: true },
|
||||
{ type: 'click', target: '[name=ib-report]', handler: reportEventHandler, global: true },
|
||||
{ type: 'click', target: 'button[name=ib-options]', handler: optionsEntryHandler, global: true },
|
||||
{ type: 'keydown', target: 'textarea[name=ib-answer]', handler: answerEntryInputHandler, global: true }
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
export default function optionsEntryHandler(event: Event): void {
|
||||
const button = event.target as HTMLElement;
|
||||
const inboxId = button.dataset.ibId;
|
||||
|
||||
const options = document.querySelector(`#ib-options-${inboxId}`);
|
||||
options.classList.toggle('d-none');
|
||||
|
||||
const buttonIcon = button.getElementsByTagName('i')[0];
|
||||
if (buttonIcon.classList.contains('fa-chevron-down')) {
|
||||
buttonIcon.classList.remove('fa-chevron-down');
|
||||
buttonIcon.classList.add('fa-chevron-up');
|
||||
} else {
|
||||
buttonIcon.classList.remove('fa-chevron-up');
|
||||
buttonIcon.classList.add('fa-chevron-down');
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ export function userActionHandler(event: Event): void {
|
|||
const button: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
const target = button.dataset.target;
|
||||
const action = button.dataset.action;
|
||||
button.disabled = true;
|
||||
|
||||
let targetURL, relationshipType;
|
||||
|
||||
|
@ -56,6 +57,7 @@ export function userActionHandler(event: Event): void {
|
|||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
})
|
||||
.finally(() => {
|
||||
button.disabled = false;
|
||||
if (!success) return;
|
||||
|
||||
switch (action) {
|
||||
|
|
|
@ -8,6 +8,8 @@ import CollapseController from "retrospring/controllers/collapse_controller";
|
|||
import ThemeController from "retrospring/controllers/theme_controller";
|
||||
import CapabilitiesController from "retrospring/controllers/capabilities_controller";
|
||||
import CropperController from "retrospring/controllers/cropper_controller";
|
||||
import InboxSharingController from "retrospring/controllers/inbox_sharing_controller";
|
||||
import ToastController from "retrospring/controllers/toast_controller";
|
||||
|
||||
/**
|
||||
* This module sets up Stimulus and our controllers
|
||||
|
@ -26,5 +28,7 @@ export default function (): void {
|
|||
window['Stimulus'].register('collapse', CollapseController);
|
||||
window['Stimulus'].register('cropper', CropperController);
|
||||
window['Stimulus'].register('format-popup', FormatPopupController);
|
||||
window['Stimulus'].register('inbox-sharing', InboxSharingController);
|
||||
window['Stimulus'].register('theme', ThemeController);
|
||||
window['Stimulus'].register('toast', ToastController);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ class Answer < ApplicationRecord
|
|||
validates :question_id, uniqueness: { scope: :user_id }
|
||||
# rubocop:enable Rails/UniqueValidationWithoutIndex
|
||||
|
||||
scope :pinned, -> { where.not(pinned_at: nil) }
|
||||
|
||||
SHORT_ANSWER_MAX_LENGTH = 640
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
|
@ -56,4 +58,6 @@ class Answer < ApplicationRecord
|
|||
end
|
||||
|
||||
def long? = content.length > SHORT_ANSWER_MAX_LENGTH
|
||||
|
||||
def pinned? = pinned_at.present?
|
||||
end
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Notification::ServiceTokenExpired < Notification
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Notification::TwitterTokenExpired < Notification::ServiceTokenExpired
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
class Service < ApplicationRecord
|
||||
attr_accessor :provider, :info
|
||||
|
||||
belongs_to :user
|
||||
validates_uniqueness_of :uid, scope: :type
|
||||
validates_length_of :post_tag, maximum: 20
|
||||
|
||||
class << self
|
||||
|
||||
def first_from_omniauth(auth_hash)
|
||||
@@auth = auth_hash
|
||||
where(type: service_type, uid: options[:uid]).first
|
||||
end
|
||||
|
||||
def initialize_from_omniauth(auth_hash)
|
||||
@@auth = auth_hash
|
||||
service_type.constantize.new(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def auth
|
||||
@@auth
|
||||
end
|
||||
|
||||
def service_type
|
||||
"Services::#{options[:provider].camelize}"
|
||||
end
|
||||
|
||||
def options
|
||||
{
|
||||
nickname: auth['info']['nickname'],
|
||||
access_token: auth['credentials']['token'],
|
||||
access_secret: auth['credentials']['secret'],
|
||||
uid: auth['uid'],
|
||||
provider: auth['provider'],
|
||||
info: auth['info']
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
class Services::Twitter < Service
|
||||
include Rails.application.routes.url_helpers
|
||||
include SocialHelper::TwitterMethods
|
||||
|
||||
def provider
|
||||
"twitter"
|
||||
end
|
||||
|
||||
def post(answer)
|
||||
Rails.logger.debug "posting to Twitter {'answer' => #{answer.id}, 'user' => #{self.user_id}}"
|
||||
post_tweet answer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client
|
||||
@client ||= Twitter::REST::Client.new(
|
||||
consumer_key: APP_CONFIG['sharing']['twitter']['consumer_key'],
|
||||
consumer_secret: APP_CONFIG['sharing']['twitter']['consumer_secret'],
|
||||
access_token: self.access_token,
|
||||
access_token_secret: self.access_secret
|
||||
)
|
||||
end
|
||||
|
||||
def post_tweet(answer)
|
||||
client.update! prepare_tweet(answer, self.post_tag)
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
include User::PushNotificationMethods
|
||||
include User::ReactionMethods
|
||||
include User::RelationshipMethods
|
||||
include User::SharingMethods
|
||||
include User::TimelineMethods
|
||||
include ActiveModel::OneTimePassword
|
||||
|
||||
|
@ -33,7 +34,6 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
has_many :comments, dependent: :destroy_async
|
||||
has_many :inboxes, dependent: :destroy_async
|
||||
has_many :smiles, class_name: "Appendable::Reaction", dependent: :destroy_async
|
||||
has_many :services, dependent: :destroy_async
|
||||
has_many :notifications, foreign_key: :recipient_id, dependent: :destroy_async
|
||||
has_many :reports, dependent: :destroy_async
|
||||
has_many :lists, dependent: :destroy_async
|
||||
|
@ -61,6 +61,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
end
|
||||
|
||||
validates :email, fake_email: true, typoed_email: true
|
||||
validates :sharing_custom_url, allow_blank: true, valid_url: true
|
||||
validates :screen_name,
|
||||
presence: true,
|
||||
format: { with: SCREEN_NAME_REGEX, message: I18n.t("activerecord.validation.user.screen_name.format") },
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# stub model for notifying about expired service connections
|
||||
class User::ExpiredServiceConnection < User
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class User::ExpiredTwitterServiceConnection < User::ExpiredServiceConnection
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module User::SharingMethods
|
||||
def display_sharing_custom_url
|
||||
URI(sharing_custom_url).host
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AnswerPolicy
|
||||
attr_reader :user, :answer
|
||||
|
||||
def initialize(user, answer)
|
||||
@user = user
|
||||
@answer = answer
|
||||
end
|
||||
|
||||
def pin? = answer.user == user
|
||||
|
||||
def unpin? = answer.user == user
|
||||
end
|
|
@ -7,17 +7,7 @@ class TwitteredMarkdown < Redcarpet::Render::StripDown
|
|||
|
||||
def wrap_mentions(text)
|
||||
text.gsub(/(^|\s)@([a-zA-Z0-9_]{1,16})/) do
|
||||
local_user = User.find_by(screen_name: $2)
|
||||
if local_user.nil?
|
||||
"#{$1}#{$2}"
|
||||
else
|
||||
service = local_user.services.where(type: "Services::Twitter").first
|
||||
if service.nil?
|
||||
"#{$1}#{$2}"
|
||||
else
|
||||
"#{$1}@#{service.nickname}"
|
||||
end
|
||||
end
|
||||
"#{$1}#{$2}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ValidUrlValidator < ActiveModel::EachValidator
|
||||
URI_REGEXP = URI::DEFAULT_PARSER.make_regexp(%w[http https]).freeze
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
return if valid?(value)
|
||||
|
||||
record.errors.add(attribute, :invalid_url)
|
||||
end
|
||||
|
||||
def valid?(value)
|
||||
return false unless URI_REGEXP.match?(value)
|
||||
|
||||
URI.parse(value) # raises URI::InvalidURIError
|
||||
|
||||
true
|
||||
rescue URI::InvalidURIError
|
||||
false
|
||||
end
|
||||
end
|
|
@ -16,6 +16,8 @@
|
|||
%a.dropdown-item{ href: "#", data: { a_id: answer.id, action: "ab-report" } }
|
||||
%i.fa.fa-fw.fa-exclamation-triangle
|
||||
= t("voc.report")
|
||||
- else
|
||||
= render "actions/pin", answer:
|
||||
- if current_user.admin?
|
||||
%a.dropdown-item{ href: rails_admin_path_for_resource(answer), target: "_blank" }
|
||||
%i.fa.fa-fw.fa-gears
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
- if answer.pinned?
|
||||
= button_to unpin_answer_path(username: current_user.screen_name, id: answer.id),
|
||||
class: "dropdown-item",
|
||||
method: :delete,
|
||||
form: { id: "ab-pin-#{answer.id}", data: { turbo_stream: true } } do
|
||||
%i.fa.fa-fw.fa-thumbtack
|
||||
= t(".unpin")
|
||||
- else
|
||||
= button_to pin_answer_path(username: current_user.screen_name, id: answer.id),
|
||||
class: "dropdown-item",
|
||||
method: :post,
|
||||
form: { id: "ab-pin-#{answer.id}", data: { turbo_stream: true } } do
|
||||
%i.fa.fa-fw.fa-thumbtack
|
||||
= t(".pin")
|
|
@ -0,0 +1,2 @@
|
|||
= turbo_stream.update("ab-pin-#{answer.id}") do
|
||||
= render "actions/pin", answer:
|
|
@ -27,6 +27,11 @@
|
|||
.col-md-6.text-start.text-muted
|
||||
%i.fa.fa-clock-o
|
||||
= link_to(raw(t("time.distance_ago", time: time_tooltip(a))), answer_path(a.user.screen_name, a.id), class: "answerbox__permalink")
|
||||
- if a.pinned_at.present?
|
||||
%span.answerbox__pinned
|
||||
·
|
||||
%i.fa.fa-thumbtack
|
||||
= t(".pinned")
|
||||
.col-md-6.d-md-flex.answerbox__actions
|
||||
= render "answerbox/actions", a: a, display_all: display_all
|
||||
.card-footer{ id: "ab-comments-section-#{a.id}", class: display_all.nil? ? "d-none" : nil }
|
||||
|
|
|
@ -33,19 +33,23 @@
|
|||
= t("voc.answer")
|
||||
%button.btn.btn-danger.me-sm-1{ name: "ib-destroy", data: { ib_id: i.id } }
|
||||
= t("voc.delete")
|
||||
%button.btn.btn-default.px-1{ name: "ib-options", data: { ib_id: i.id, state: :hidden } }
|
||||
%i.fa.fa-chevron-down
|
||||
%span.pe-none= t(".options")
|
||||
%p.format-help.ms-auto.align-self-center.mt-2.mt-sm-0.text-center
|
||||
= render "shared/format_link"
|
||||
.card-footer.d-none{ id: "ib-options-#{i.id}" }
|
||||
%h4= t(".sharing.heading")
|
||||
- if services.count.positive?
|
||||
.row
|
||||
- services.each do |service|
|
||||
.col-md-3.col-sm-4.col-xs-6
|
||||
%label
|
||||
%input{ type: "checkbox", name: "ib-share", checked: :checked, data: { ib_id: i.id, service: service.provider } }
|
||||
= raw t(".sharing.post_to", service: service.provider.capitalize)
|
||||
- else
|
||||
%p= t(".sharing.none_html", settings: link_to(t(".sharing.settings"), services_path))
|
||||
- if current_user.sharing_enabled
|
||||
.inbox-entry__sharing.text-center.p-2.justify-content-center.d-none{
|
||||
data: { controller: "inbox-sharing", inbox_sharing_config_value: "{}", inbox_sharing_auto_close_value: current_user.sharing_autoclose.to_s } }
|
||||
%button.btn-close.inbox-entry__close{ data: { action: "inbox-sharing#close" } }
|
||||
%span.visually-hidden= t("voc.close")
|
||||
.align-self-center
|
||||
%p.fs-3.fw-bold= t(".sharing.heading")
|
||||
%p
|
||||
%a.btn.btn-primary{ href: "https://twitter.com/intent/tweet?text=", data: { inbox_sharing_target: "twitter" }, target: "_blank" }
|
||||
%i.fab.fa-twitter.fa-fw
|
||||
Twitter
|
||||
%a.btn.btn-primary{ href: "#", data: { inbox_sharing_target: "tumblr" }, target: "_blank" }
|
||||
%i.fab.fa-tumblr.fa-fw
|
||||
Tumblr
|
||||
- if current_user.sharing_custom_url.present?
|
||||
%a.btn.btn-primary{ href: current_user.sharing_custom_url, data: { inbox_sharing_target: "custom" }, target: "_blank" }
|
||||
= current_user.display_sharing_custom_url
|
||||
%p.text-muted= t(".sharing.hint_html", settings: link_to(t(".sharing.settings"), settings_sharing_path))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#entries
|
||||
- @inbox.each do |i|
|
||||
= render "inbox/entry", services:, i:
|
||||
= render "inbox/entry", i:
|
||||
|
||||
- if @inbox.empty?
|
||||
%p.empty= t(".empty")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
= turbo_stream.append "entries" do
|
||||
- @inbox.each do |i|
|
||||
= render "inbox/entry", services:, i:
|
||||
= render "inbox/entry", i:
|
||||
|
||||
= turbo_stream.update "paginator" do
|
||||
- if @more_data_available
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
= render 'shared/announcements'
|
||||
= yield
|
||||
= render "shared/formatting"
|
||||
.d-none#toasts
|
||||
- if Rails.env.development?
|
||||
#debug
|
||||
%hr
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
|
||||
.container-lg.container--main
|
||||
- @questions.each do |q|
|
||||
= render 'shared/question', q: q, type: nil
|
||||
= render "shared/question", q:, type: "moderation"
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
.d-flex.notification
|
||||
.flex-shrink-0.notification__icon
|
||||
%i.fa.fa-2x.fa-fw.fa-twitter
|
||||
.flex-grow-1
|
||||
%h6.notification__user
|
||||
= t(".heading")
|
||||
.notification__text
|
||||
= t(".text_html", settings_sharing: link_to(t(".settings_services"), services_path))
|
|
@ -22,7 +22,3 @@
|
|||
%br/
|
||||
%button.btn.btn-success#q-answer-btn{ data: { q_id: @question.id } }
|
||||
= t("voc.answer")
|
||||
- current_user.services.each do |service|
|
||||
%label
|
||||
%input{ type: "checkbox", name: "share", checked: :checked, data: { q_id: @question.id, service: service.provider } }
|
||||
= t("inbox.entry.sharing.post_to", service: service.provider.capitalize)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
= render "settings/services", services: @services
|
||||
|
||||
- provide(:title, generate_title(t(".title")))
|
||||
- parent_layout "user/settings"
|
|
@ -1,27 +0,0 @@
|
|||
.card
|
||||
.card-body
|
||||
= t(".services", count: services.count)
|
||||
|
||||
- APP_CONFIG["sharing"].each do |service, service_options|
|
||||
- if service_options["enabled"] && services.none? { |x| x.provider == service.to_s }
|
||||
%p= button_to t(".connect", service: service.capitalize), "/auth/#{service}", method: :post, class: "btn btn-info",
|
||||
form: { data: { turbo: false } }
|
||||
|
||||
- if services.count.positive?
|
||||
%ul.list-group
|
||||
- services.each do |service|
|
||||
%li.list-group-item
|
||||
%i{ class: "fa fa-#{service.provider}" }
|
||||
%strong= service.provider.capitalize
|
||||
(#{service.nickname})
|
||||
= button_to t(".disconnect"),
|
||||
service_path(service),
|
||||
data: { confirm: t(".confirm", service: service.provider.capitalize) },
|
||||
method: :delete,
|
||||
class: "btn btn-link text-danger",
|
||||
form: { class: "d-inline", data: { turbo: false } }
|
||||
|
||||
.col-md-6.mt-2
|
||||
= bootstrap_form_for(service, as: "service", url: update_service_path(service), data: { turbo: false }) do |f|
|
||||
= f.text_field :post_tag, label_as_placeholder: true,
|
||||
append: f.submit(t("voc.update"), class: "btn btn-primary"), maxlength: 20, pattern: "^[^@]*$"
|
|
@ -0,0 +1,20 @@
|
|||
= bootstrap_form_for(current_user, url: settings_sharing_path, method: :patch, data: { turbo: false }) do |f|
|
||||
.card
|
||||
.card-body
|
||||
= f.form_group :sharing, help: t("activerecord.help.user.sharing_enabled") do
|
||||
= f.check_box :sharing_enabled
|
||||
= f.form_group :sharing, help: t("activerecord.help.user.sharing_autoclose"), class: false do
|
||||
= f.check_box :sharing_autoclose
|
||||
|
||||
.card
|
||||
.card-body
|
||||
%h3= t(".advanced.title")
|
||||
= t(".advanced.body_html")
|
||||
= f.url_field :sharing_custom_url
|
||||
|
||||
.card
|
||||
.card-body
|
||||
= f.primary
|
||||
|
||||
- provide(:title, generate_title(t(".title")))
|
||||
- parent_layout "user/settings"
|
|
@ -2,18 +2,23 @@
|
|||
.card.questionbox{ data: { id: q.id } }
|
||||
.card-body{ data: { controller: q.long? ? "collapse" : nil } }
|
||||
.d-flex
|
||||
- if type == 'discover'
|
||||
- if type == "discover"
|
||||
.flex-shrink-0
|
||||
%a{ href: user_screen_name(q.user, link_only: true) }
|
||||
%img.avatar-md.me-2{ src: q.user&.profile_picture&.url(:small), loading: :lazy }
|
||||
.flex-grow-1
|
||||
%h6.text-muted.answerbox__question-user
|
||||
- if type.nil? && q.direct
|
||||
- if user_signed_in? && q.user == current_user
|
||||
%i.fa.fa-eye-slash{ data: { bs_toggle: "tooltip", bs_title: t(".visible_to_you") } }
|
||||
- elsif moderation_view?
|
||||
%i.fa.fa-eye-slash{ data: { bs_toggle: "tooltip", bs_title: t(".visible_mod_mode") } }
|
||||
= t("answerbox.header.asked_html", user: user_screen_name(q.user), time: time_tooltip(q))
|
||||
- if q.answer_count > 1
|
||||
·
|
||||
%a{ href: question_path(q.user.screen_name, q.id) }
|
||||
= pluralize(q.answer_count, t("voc.answer"))
|
||||
.answerbox__question-text{ class: q.long? ? 'collapsed' : '', data: { collapse_target: "content" } }
|
||||
.answerbox__question-text{ class: q.long? ? "collapsed" : "", data: { collapse_target: "content" } }
|
||||
= question_markdown q.content
|
||||
- if q.long?
|
||||
= render "shared/collapse", type: "question"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
.d-none{ data: { controller: "toast", toast_message_value: message, toast_success_value: success } }
|
|
@ -4,7 +4,7 @@
|
|||
= list_group_item t(".profile"), edit_settings_profile_path
|
||||
= list_group_item t(".privacy"), edit_settings_privacy_path
|
||||
= list_group_item t(".security"), settings_two_factor_authentication_otp_authentication_path
|
||||
= list_group_item t(".sharing"), services_path
|
||||
= list_group_item t(".sharing"), settings_sharing_path
|
||||
= list_group_item t(".mutes"), settings_muted_path
|
||||
= list_group_item t(".blocks"), settings_blocks_path
|
||||
= list_group_item t(".theme"), edit_settings_theme_path
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
- unless @user.banned?
|
||||
#pinned-answers
|
||||
- @pinned_answers.each do |a|
|
||||
= render "answerbox", a:
|
||||
|
||||
#answers
|
||||
- @answers.each do |a|
|
||||
= render 'answerbox', a: a
|
||||
= render "answerbox", a:
|
||||
|
||||
- if @more_data_available
|
||||
.d-flex.justify-content-center.justify-content-sm-start#paginator
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ShareWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :share, retry: 5
|
||||
|
||||
# @param user_id [Integer] the user id
|
||||
# @param answer_id [Integer] the user id
|
||||
# @param service [String] the service to post to
|
||||
def perform(user_id, answer_id, service) # rubocop:disable Metrics/AbcSize
|
||||
@user_service = User.find(user_id).services.find_by!(type: "Services::#{service.camelize}")
|
||||
|
||||
@user_service.post(Answer.find(answer_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
logger.info "Tried to post answer ##{answer_id} for user ##{user_id} to #{service.titleize} but the user/answer/service did not exist (likely deleted), will not retry."
|
||||
# The question to be posted was deleted
|
||||
rescue Twitter::Error::DuplicateStatus
|
||||
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the status was already posted."
|
||||
rescue Twitter::Error::Forbidden
|
||||
# User's Twitter account is suspended
|
||||
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the account is suspended."
|
||||
rescue Twitter::Error::Unauthorized
|
||||
# User's Twitter token has expired or been revoked
|
||||
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the token has expired or been revoked."
|
||||
revoke_and_notify(user_id, service)
|
||||
rescue => e
|
||||
logger.info "failed to post answer #{answer_id} to #{service} for user #{user_id}: #{e.message}"
|
||||
Sentry.capture_exception(e)
|
||||
raise
|
||||
end
|
||||
|
||||
def revoke_and_notify(user_id, service)
|
||||
@user_service.destroy
|
||||
|
||||
Notification::ServiceTokenExpired.create(
|
||||
target_id: user_id,
|
||||
target_type: "User::Expired#{service.camelize}ServiceConnection",
|
||||
recipient_id: user_id,
|
||||
new: true
|
||||
)
|
||||
end
|
||||
end
|
|
@ -51,6 +51,10 @@ Rails.application.configure do
|
|||
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = ENV.fetch("LOG_LEVEL") { "debug" }.to_sym
|
||||
|
||||
# Print deprecation notices to the Rails logger.
|
||||
config.active_support.deprecation = :log
|
||||
|
||||
|
@ -73,6 +77,6 @@ Rails.application.configure do
|
|||
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||
end
|
||||
|
||||
# For better_errors to work inside Docker we need
|
||||
# For better_errors to work inside Docker we need
|
||||
# to allow 0.0.0.0 as an IP in development context
|
||||
BetterErrors::Middleware.allow_ip! "0.0.0.0/0"
|
||||
|
|
|
@ -45,7 +45,7 @@ Rails.application.configure do
|
|||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = :debug
|
||||
config.log_level = ENV.fetch("LOG_LEVEL") { "info" }.to_sym
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
config.log_tags = [ :request_id ]
|
||||
|
@ -54,7 +54,19 @@ Rails.application.configure do
|
|||
config.lograge.enabled = true
|
||||
|
||||
# Use a different cache store in production.
|
||||
# config.cache_store = :mem_cache_store
|
||||
cache_redis_url = ENV.fetch("CACHE_REDIS_URL") { nil }
|
||||
if cache_redis_url.present?
|
||||
config.cache_store = :redis_cache_store, {
|
||||
url: cache_redis_url,
|
||||
pool_size: ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i,
|
||||
pool_timeout: ENV.fetch("CACHE_REDIS_TIMEOUT") { 5 },
|
||||
error_handler: -> (method:, returning:, exception:) {
|
||||
# Report errors to Sentry as warnings
|
||||
Sentry.capture_exception exception, level: 'warning',
|
||||
tags: { method: method, returning: returning }
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
# Use a real queuing backend for Active Job (and separate queues per environment)
|
||||
# config.active_job.queue_adapter = :resque
|
||||
|
|
|
@ -34,6 +34,10 @@ Rails.application.configure do
|
|||
# ActionMailer::Base.deliveries array.
|
||||
config.action_mailer.delivery_method = :test
|
||||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = ENV.fetch("LOG_LEVEL") { "debug" }.to_sym
|
||||
|
||||
# Print deprecation notices to the stderr.
|
||||
config.active_support.deprecation = :stderr
|
||||
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Auxiliary config
|
||||
APP_CONFIG = YAML.load_file(Rails.root.join('config', 'justask.yml')).with_indifferent_access
|
||||
|
||||
APP_CONFIG = {}.with_indifferent_access
|
||||
|
||||
# load yml config if it's present
|
||||
justask_yml_path = Rails.root.join("config/justask.yml")
|
||||
APP_CONFIG.merge!(YAML.load_file(justask_yml_path)) if File.exist?(justask_yml_path)
|
||||
|
||||
# load config from ENV where possible
|
||||
env_config = {
|
||||
# The site name, shown everywhere
|
||||
site_name: ENV.fetch("SITE_NAME", nil),
|
||||
|
||||
hostname: ENV.fetch("HOSTNAME", nil),
|
||||
}.compact
|
||||
APP_CONFIG.merge!(env_config)
|
||||
|
||||
# Update rails config for mail
|
||||
Rails.application.config.action_mailer.default_url_options = { host: APP_CONFIG['hostname'] }
|
||||
Rails.application.config.action_mailer.default_url_options = {
|
||||
host: APP_CONFIG["hostname"],
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
Rails.application.config.middleware.use OmniAuth::Builder do
|
||||
APP_CONFIG.fetch('sharing').each do |service, opts|
|
||||
if opts['enabled']
|
||||
provider service.to_sym, opts['consumer_key'], opts['consumer_secret']
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,7 +11,6 @@ Sentry.init do |config|
|
|||
|
||||
exception_fingerprints = {
|
||||
Excon::Error::ServiceUnavailable => 'external-service',
|
||||
Twitter::Error::InternalServerError => 'external-service',
|
||||
}
|
||||
config.before_send = lambda do |event, hint|
|
||||
# These are used for user-facing errors, not when something goes wrong
|
||||
|
|
|
@ -53,14 +53,6 @@ features:
|
|||
public:
|
||||
enabled: true
|
||||
|
||||
# OAuth tokens
|
||||
sharing:
|
||||
twitter:
|
||||
enabled: false
|
||||
# Get the tokens from https://apps.twitter.com
|
||||
consumer_key: ''
|
||||
consumer_secret: ''
|
||||
|
||||
# Redis
|
||||
redis_url: "redis://localhost:6379"
|
||||
|
||||
|
|
|
@ -30,8 +30,6 @@ en:
|
|||
location: "Location"
|
||||
motivation_header: "Motivation header"
|
||||
website: "Website"
|
||||
service:
|
||||
post_tag: "Tag"
|
||||
theme:
|
||||
background_color: "Background colour"
|
||||
body_text: "Body text colour"
|
||||
|
@ -76,6 +74,9 @@ en:
|
|||
privacy_allow_stranger_answers: "Allow other people to answer your questions"
|
||||
privacy_noindex: "Prevent search engines from indexing your profile"
|
||||
privacy_hide_social_graph: "Hide your social graph from others"
|
||||
sharing_enabled: "Enable sharing"
|
||||
sharing_autoclose: "Automatically hide inbox entry after sharing once"
|
||||
sharing_custom_url: "Custom Share Link"
|
||||
profile_picture: "Profile picture"
|
||||
profile_header: "Profile header"
|
||||
sign_in_count: "Sign in count"
|
||||
|
@ -87,11 +88,12 @@ en:
|
|||
screen_name: "Alphanumerical and underscores allowed, max. 16 characters"
|
||||
email: "Don't forget to check your spam folder in case our email might have landed there!"
|
||||
current_password: "We need your current password to confirm your changes"
|
||||
sharing_enabled: "Shows a sharing dialog after answering a question from your inbox"
|
||||
sharing_autoclose: "Use this if you mainly post to a single service"
|
||||
sharing_custom_url: "Example: https://mastodon.social/share?text="
|
||||
profile:
|
||||
anon_display_name: "This name will be used for questions asked to you by anonymous users."
|
||||
motivation_header: "Shown in the header of the question box on your profile. Motivate users to ask you questions!"
|
||||
services/twitter:
|
||||
post_tag: "Automatically append a tag to your shared answers. A # symbol is not automatically prepended."
|
||||
theme:
|
||||
danger_color: "Colour used for errors or critical actions like deleting something."
|
||||
info_color: "Colour used for informational popups or messages."
|
||||
|
@ -102,6 +104,9 @@ en:
|
|||
user:
|
||||
screen_name:
|
||||
format: "contains invalid characters"
|
||||
errors:
|
||||
messages:
|
||||
invalid_url: "does not look like a valid URL"
|
||||
helpers:
|
||||
submit:
|
||||
user:
|
||||
|
|
|
@ -26,6 +26,8 @@ en:
|
|||
destroy:
|
||||
nopriv: "You cannot delete other people's answers."
|
||||
success: "Successfully deleted answer."
|
||||
pin:
|
||||
success: "Successfully pinned answer."
|
||||
comment:
|
||||
create:
|
||||
invalid: "Your comment is too long."
|
||||
|
@ -144,22 +146,15 @@ en:
|
|||
zero: "You are not currently subscribed to push notifications on any devices."
|
||||
one: "You are currently receiving push notifications on one device."
|
||||
other: "You are currently receiving push notifications on %{count} devices."
|
||||
anonymous_block:
|
||||
create:
|
||||
success: "Successfully blocked user."
|
||||
destroy:
|
||||
success: "Successfully unblocked user."
|
||||
inbox:
|
||||
author:
|
||||
info: "No questions from @%{author} found, showing entries from all users instead!"
|
||||
error: "No user with the name @%{author} found, showing entries from all users instead!"
|
||||
services:
|
||||
create:
|
||||
success: "%{service} connected successfully."
|
||||
duplicate: "The %{service} account you are trying to connect is already connected to another %{app} account. If you are unable to disconnect the account yourself, please send us a Direct Message on Twitter: @retrospring."
|
||||
error: "Unable to connect to %{service}."
|
||||
update:
|
||||
success: "Service updated successfully."
|
||||
error: "Unable to update service."
|
||||
failure:
|
||||
error: :errors.base
|
||||
destroy:
|
||||
success: "%{service} disconnected successfully."
|
||||
settings:
|
||||
export:
|
||||
index:
|
||||
|
@ -167,6 +162,11 @@ en:
|
|||
create:
|
||||
success: "Your account is currently being exported. This will take a little while."
|
||||
error: "Exporting is currently not possible."
|
||||
mutes:
|
||||
create:
|
||||
success: "Sucessfully created mute rule"
|
||||
destroy:
|
||||
success: "Successfully removed mute rule"
|
||||
privacy:
|
||||
update:
|
||||
success: :settings.profile.update.success
|
||||
|
@ -182,6 +182,10 @@ en:
|
|||
notice:
|
||||
profile_picture: " It might take a few minutes until your new profile picture is shown everywhere."
|
||||
profile_header: " It might take a few minutes until your new profile header is shown everywhere."
|
||||
sharing:
|
||||
update:
|
||||
success: "Sharing settings updated successfully."
|
||||
error: "Unable to update sharing settings."
|
||||
theme:
|
||||
update:
|
||||
success: "Theme saved successfully."
|
||||
|
@ -208,3 +212,8 @@ en:
|
|||
timeline:
|
||||
public:
|
||||
title: "Public Timeline"
|
||||
answer:
|
||||
pin:
|
||||
success: "This answer will now appear at the top of your profile."
|
||||
unpin:
|
||||
success: "This answer will no longer be pinned to the top of your profile."
|
||||
|
|
|
@ -27,10 +27,11 @@ en:
|
|||
header: "Share your answers"
|
||||
body_html: |
|
||||
<p>Want your followers on another platform to see your %{app_name} answers?
|
||||
You can configure automatic sharing to your favourite platforms easily.</p>
|
||||
You can easily share them to your favourite platforms.</p>
|
||||
|
||||
<p class="text-muted">Not sure if it's a favourite, but at the moment only
|
||||
<b>Twitter</b> is supported.</p>
|
||||
<p class="text-muted">We support <strong>Tumblr</strong>, <strong>Twitter</strong>,
|
||||
and many other services including <strong>Mastodon</strong> and
|
||||
<strong>Misskey</strong>.</p>
|
||||
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: |
|
||||
<p>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.</p>
|
||||
<p>We will prepend a URL-encoded version of the answer to the end of the given link.</p>
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
20
db/schema.rb
20
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
12
package.json
12
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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 <strong>oarger</strong> 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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
285
yarn.lock
285
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"
|
||||
|
|
Loading…
Reference in New Issue