diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..8256390f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/docker-existing-docker-compose +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "Retrospring", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yml" + ], + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "web", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/app", + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "apt-get update && apt-get install -y curl", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "app" +} diff --git a/.docker/ruby/Dockerfile b/.docker/ruby/Dockerfile index f9480dc2..205ec80a 100644 --- a/.docker/ruby/Dockerfile +++ b/.docker/ruby/Dockerfile @@ -8,7 +8,7 @@ ARG GID=1000 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - +RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - RUN apt-get update -qq \ && apt-get install -y --no-install-recommends build-essential \ @@ -47,4 +47,4 @@ COPY . /app ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 -CMD ["rails", "server", "-b", "0.0.0.0"] \ No newline at end of file +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/.github/workflows/retrospring.yml b/.github/workflows/retrospring.yml index d0147b68..1dd575e5 100644 --- a/.github/workflows/retrospring.yml +++ b/.github/workflows/retrospring.yml @@ -48,10 +48,10 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - - name: Set up Node 12 + - name: Set up Node 14 uses: actions/setup-node@v3 with: - node-version: '12' + node-version: '14' - name: Copy default configuration run: | cp config/database.yml.postgres config/database.yml diff --git a/Gemfile b/Gemfile index 1cceaaea..114a5228 100644 --- a/Gemfile +++ b/Gemfile @@ -80,13 +80,11 @@ gem "puma" group :development, :test do gem "better_errors" gem "bullet" - gem "capybara" gem "database_cleaner" gem "factory_bot_rails", require: false gem "faker" gem "haml_lint", require: false gem "letter_opener" # Use this just in local test environments - gem "poltergeist" gem "rails-controller-testing" gem "rake" gem "rspec-its", "~> 1.3" diff --git a/Gemfile.lock b/Gemfile.lock index f7d5f60d..070b237d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,15 +95,6 @@ GEM bullet (7.0.3) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - capybara (3.37.1) - addressable - matrix - mini_mime (>= 0.1.3) - nokogiri (~> 1.8) - rack (>= 1.6.0) - rack-test (>= 0.6.3) - regexp_parser (>= 1.5, < 3.0) - xpath (~> 3.2) carrierwave (2.1.0) activemodel (>= 5.0.0) activesupport (>= 5.0.0) @@ -112,7 +103,6 @@ GEM mimemagic (>= 0.3.0) mini_mime (>= 0.1.3) chunky_png (1.4.0) - cliver (0.3.2) coderay (1.1.3) colorize (0.8.1) concurrent-ruby (1.1.10) @@ -261,7 +251,6 @@ GEM mail (2.7.1) mini_mime (>= 0.1.1) marcel (1.0.2) - matrix (0.4.2) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) method_source (1.0.0) @@ -315,10 +304,6 @@ GEM pg (1.4.4) pghero (3.0.1) activerecord (>= 6) - poltergeist (1.18.1) - capybara (>= 2.1, < 4) - cliver (~> 0.3.1) - websocket-driver (>= 0.2.0) public_suffix (4.0.7) puma (6.0.0) nio4r (~> 2.0) @@ -521,8 +506,6 @@ GEM websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - xpath (3.2.0) - nokogiri (~> 1.8) zeitwerk (2.6.5) PLATFORMS @@ -536,7 +519,6 @@ DEPENDENCIES bootsnap bootstrap_form (~> 4.5) bullet - capybara carrierwave (~> 2.0) carrierwave_backgrounder! colorize @@ -571,7 +553,6 @@ DEPENDENCIES omniauth-twitter pg pghero - poltergeist puma questiongenerator (~> 1.0) rails (~> 6.1) diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index 038cba98..55d553e8 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -6,7 +6,8 @@ class Settings::PrivacyController < ApplicationController def edit; end def update - user_attributes = params.require(:user).permit(:privacy_allow_anonymous_questions, + user_attributes = params.require(:user).permit(:privacy_lock_inbox, + :privacy_allow_anonymous_questions, :privacy_allow_public_timeline, :privacy_allow_stranger_answers, :privacy_show_in_search, diff --git a/app/models/user.rb b/app/models/user.rb index a8f57dd5..2098f825 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -169,6 +169,10 @@ class User < ApplicationRecord !self.export_processing end + def inbox_locked? + privacy_lock_inbox + end + # %w[admin moderator].each do |m| # define_method(m) { raise "not allowed: #{m}" } # define_method(m+??) { raise "not allowed: #{m}?"} diff --git a/app/views/application/_questionbox.html.haml b/app/views/application/_questionbox.html.haml index 09183122..ec5d3560 100644 --- a/app/views/application/_questionbox.html.haml +++ b/app/views/application/_questionbox.html.haml @@ -14,6 +14,9 @@ - elsif user_signed_in? && user.blocking?(current_user) .text-center %strong= t(".status.blocked") + - elsif user.inbox_locked? + .text-center + %strong= t(".status.locked") - elsif !user_signed_in? && user.privacy_require_user? .text-center %strong= t(".status.require_user_html", sign_in: link_to(t("voc.login"), new_user_session_path), sign_up: link_to(t("voc.register"), new_user_registration_path)) diff --git a/app/views/settings/privacy/edit.html.haml b/app/views/settings/privacy/edit.html.haml index 3a3f6168..a0f25626 100644 --- a/app/views/settings/privacy/edit.html.haml +++ b/app/views/settings/privacy/edit.html.haml @@ -1,6 +1,7 @@ .card .card-body = bootstrap_form_for(current_user, url: settings_privacy_path, method: :patch, data: { turbo: false }) do |f| + = f.check_box :privacy_lock_inbox = f.check_box :privacy_allow_anonymous_questions = f.check_box :privacy_require_user = f.check_box :privacy_allow_public_timeline diff --git a/app/workers/question_worker.rb b/app/workers/question_worker.rb index 8f7823a6..9a361553 100644 --- a/app/workers/question_worker.rb +++ b/app/workers/question_worker.rb @@ -12,6 +12,7 @@ class QuestionWorker question = Question.find(question_id) user.followers.each do |f| + next if f.inbox_locked? next if f.banned? next if MuteRule.where(user: f).any? { |rule| rule.applies_to? question } diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 8be68c2c..956a3d5e 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -67,6 +67,7 @@ en: remember_created_at: "Remember me set at" password: "Password" password_confirmation: "Confirm your password" + privacy_lock_inbox: "Lock inbox and don't allow new questions" privacy_allow_anonymous_questions: "Allow anonymous questions" privacy_require_user: "Require users to be logged in to ask you questions" privacy_allow_public_timeline: "Show your answers in the public timeline" diff --git a/config/locales/errors.en.yml b/config/locales/errors.en.yml index 6624e7e7..82516fff 100644 --- a/config/locales/errors.en.yml +++ b/config/locales/errors.en.yml @@ -6,6 +6,7 @@ en: param_is_missing: "param is missing" forbidden: "This is illegal, you know" + inbox_locked: "This user currently does not accept questions" blocked: "You have been blocked from performing this request" other_blocked_self: "You have been blocked by this user" asking_other_blocked_self: "You have been blocked from asking this user questions" diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index d5cfe7cd..c626effb 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -122,6 +122,7 @@ en: banned: "This user got hit with ye olde banhammer." blocking: "You are blocking this user." blocked: "This user has blocked you." + locked: "This user currently does not accept questions." require_user_html: | This user requires others to be logged in to ask questions.
(%{sign_in} or %{sign_up}) diff --git a/db/migrate/20221106130744_add_inbox_locked_to_users.rb b/db/migrate/20221106130744_add_inbox_locked_to_users.rb new file mode 100644 index 00000000..295ebca7 --- /dev/null +++ b/db/migrate/20221106130744_add_inbox_locked_to_users.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddInboxLockedToUsers < ActiveRecord::Migration[6.1] + def up + add_column :users, :privacy_lock_inbox, :boolean, default: false + end + + def down + remove_column :users, :privacy_lock_inbox + end +end diff --git a/db/schema.rb b/db/schema.rb index 9363de00..ebffd650 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -31,8 +31,8 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.string "identifier" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false - t.bigint "question_id" t.bigint "user_id" + t.bigint "question_id" t.index ["identifier"], name: "index_anonymous_blocks_on_identifier" t.index ["question_id"], name: "index_anonymous_blocks_on_question_id" t.index ["user_id"], name: "index_anonymous_blocks_on_user_id" @@ -46,8 +46,6 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.datetime "created_at" t.datetime "updated_at" t.integer "smile_count", default: 0, null: false - t.datetime "deleted_at" - t.index ["deleted_at"], name: "index_answers_on_deleted_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" end @@ -60,8 +58,6 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.text "content" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false - t.datetime "deleted_at" - t.index ["deleted_at"], name: "index_appendables_on_deleted_at" t.index ["parent_id", "parent_type"], name: "index_appendables_on_parent_id_and_parent_type" t.index ["user_id", "created_at"], name: "index_appendables_on_user_id_and_created_at" end @@ -73,9 +69,7 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.datetime "created_at" t.datetime "updated_at" t.integer "smile_count", default: 0, null: false - t.datetime "deleted_at" t.index ["answer_id"], name: "index_comments_on_answer_id" - t.index ["deleted_at"], name: "index_comments_on_deleted_at" t.index ["user_id", "created_at"], name: "index_comments_on_user_id_and_created_at" end @@ -108,10 +102,10 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do end create_table "mute_rules", id: :bigint, default: -> { "gen_timestamp_id('mute_rules'::text)" }, force: :cascade do |t| + t.bigint "user_id" t.string "muted_phrase" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "user_id" t.index ["user_id"], name: "index_mute_rules_on_user_id" end @@ -150,8 +144,6 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.datetime "updated_at" t.integer "answer_count", default: 0, null: false t.boolean "direct", default: false, null: false - t.datetime "deleted_at" - t.index ["deleted_at"], name: "index_questions_on_deleted_at" t.index ["user_id", "created_at"], name: "index_questions_on_user_id_and_created_at" end @@ -302,8 +294,8 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.integer "otp_module", default: 0, null: false t.datetime "deleted_at" t.boolean "privacy_require_user", default: false + t.boolean "privacy_lock_inbox", default: false t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true - t.index ["deleted_at"], name: "index_users_on_deleted_at" t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["screen_name"], name: "index_users_on_screen_name", unique: true @@ -317,5 +309,7 @@ ActiveRecord::Schema.define(version: 2022_11_13_110942) do t.index ["user_id"], name: "index_users_roles_on_user_id" end + add_foreign_key "anonymous_blocks", "questions" + add_foreign_key "anonymous_blocks", "users" add_foreign_key "profiles", "users" end diff --git a/lib/errors.rb b/lib/errors.rb index a1b1cdba..9d613e95 100644 --- a/lib/errors.rb +++ b/lib/errors.rb @@ -35,6 +35,9 @@ module Errors class SelfAction < Forbidden end + class InboxLocked < Forbidden + end + class FollowingSelf < SelfAction end diff --git a/lib/use_case/question/create.rb b/lib/use_case/question/create.rb index 4cf426ee..eb085350 100644 --- a/lib/use_case/question/create.rb +++ b/lib/use_case/question/create.rb @@ -15,6 +15,7 @@ module UseCase def call check_user + check_lock check_anonymous_rules check_blocks @@ -43,6 +44,10 @@ module UseCase private + def check_lock + raise Errors::InboxLocked if target_user.inbox_locked? + end + def check_anonymous_rules if !source_user_id && !anonymous # We can not create a non-anonymous question without a source user diff --git a/spec/controllers/ajax/question_controller_spec.rb b/spec/controllers/ajax/question_controller_spec.rb index cd3166a4..cbb1a0f1 100644 --- a/spec/controllers/ajax/question_controller_spec.rb +++ b/spec/controllers/ajax/question_controller_spec.rb @@ -328,6 +328,23 @@ describe Ajax::QuestionController, :ajax_controller, type: :controller do include_examples "does not create the question" end end + + context "when users inbox is locked" do + let(:user_allows_anonymous_questions) { true } + let(:expected_response) do + { + "success" => false, + "status" => "inbox_locked", + "message" => anything + } + end + + before do + target_user.update(privacy_lock_inbox: true) + end + + include_examples "does not create the question" + end end context "when rcpt is followers" do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e0552d11..c6b03bd1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -10,8 +10,6 @@ require "rspec/rails" # Add additional requires below this line. Rails is not loaded until this point! require "rspec/its" require "devise" -require "capybara/rails" -require "capybara/rspec" require "rspec-sidekiq" # Requires supporting ruby files with custom matchers and macros, etc, in diff --git a/spec/workers/question_worker_spec.rb b/spec/workers/question_worker_spec.rb index cafbc455..ab1e2914 100644 --- a/spec/workers/question_worker_spec.rb +++ b/spec/workers/question_worker_spec.rb @@ -6,7 +6,7 @@ describe QuestionWorker do describe "#perform" do let(:user) { FactoryBot.create(:user) } let(:user_id) { user.id } - let(:question) { FactoryBot.create(:question, user: user) } + let(:question) { FactoryBot.create(:question, user:) } let(:question_id) { question.id } before do @@ -41,6 +41,18 @@ describe QuestionWorker do ) end + it "respects inbox locks" do + user.followers.first.update(privacy_lock_inbox: true) + + + expect { subject } + .to( + change { Inbox.where(user_id: user.followers.ids, question_id:, new: true).count } + .from(0) + .to(4) + ) + end + it "does not send questions to banned users" do user.followers.first.ban