diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 2c5c2ec82..369709f05 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -87,6 +87,7 @@ jobs: bundler-cache: true - name: Prepare for testing run: | + echo "${{ secrets.MASTER_KEY }}" > config/master.key cp config/database.sample.yml config/database.yml cp config/storage.sample.yml config/storage.yml bundle exec rails db:create diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 9c5500d84..7f1bb2ba6 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -5,7 +5,7 @@ class ApplicationJob < ActiveJob::Base # Most jobs are safe to ignore if the underlying records are no longer available discard_on ActiveJob::DeserializationError - def initialize + def initialize(*args, **opts) @job_id = SecureRandom.uuid super end diff --git a/app/jobs/mail_uncaptured_donations_job.rb b/app/jobs/mail_uncaptured_donations_job.rb new file mode 100644 index 000000000..94fbe5b18 --- /dev/null +++ b/app/jobs/mail_uncaptured_donations_job.rb @@ -0,0 +1,29 @@ +class MailUncapturedDonationsJob < ApplicationJob + queue_as :default + + def perform(*_args) + intents = Stripe::PaymentIntent.list + mailed = 0 + errors = 0 + intents.auto_paging_each do |pi| + break unless Time.at(pi.created) >= 24.hours.ago + next unless pi.status == 'requires_payment_method' + next unless pi.metadata['user_id'].present? + next if pi.metadata['emailed'].present? + + user = User.find(pi.metadata['user_id']) + symbol = { 'GBP' => '£', 'USD' => '$', 'EUR' => '€' }[pi.currency.upcase] + amount = pi.amount / 100 + DonationMailer.with(currency: symbol, amount: amount, email: user.email, name: user.username, intent: pi) + .donation_uncaptured.deliver_now + Stripe::PaymentIntent.update(pi.id, { metadata: { emailed: true } }) + logger.debug "Mailed ##{user.id} for PaymentIntent #{pi.id}" + mailed += 1 + rescue => e + Stripe::PaymentIntent.update(pi.id, { metadata: { email_error: e.message.to_s } }) + logger.error "Error sending email for user #{user.id}, PaymentIntent #{pi.id}: #{e.message}" + errors += 1 + end + logger.info "Mailed #{mailed} donations, #{errors} errors" + end +end diff --git a/app/jobs/recalc_abilities_job.rb b/app/jobs/recalc_abilities_job.rb new file mode 100644 index 000000000..af45cbe10 --- /dev/null +++ b/app/jobs/recalc_abilities_job.rb @@ -0,0 +1,55 @@ +class RecalcAbilitiesJob < ApplicationJob + queue_as :default + + ## + # Perform ability recalculations. + # @param options [Hash] additional options for the job. Currently supports +verbose+ and +quiet+. + def perform(**options) + resolved = [] + destroy = [] + all = AbilityQueue.pending.to_a + + all.each do |q| + cu = q.community_user + u = cu&.user + + if cu.nil? || u.nil? + destroy << q.id + next + end + + RequestContext.community = cu.community + + if options[:verbose] && !options[:quiet] + logger.debug "Scope: Community : #{cu.community.name} (#{cu.community.host})" + logger.debug " User : #{u.username} (#{cu.user_id})" + logger.debug " CommunityUser : #{cu.id}" + elsif !options[:verbose] && !options[:quiet] + logger.debug "Scope: CommunityUser : #{cu.id}" + end + + cu.recalc_abilities! + + # Grant mod ability if mod status is given + if cu.at_least_moderator? && !cu.ability?('mod') + cu.grant_ability!('mod') + end + + resolved << q.id + rescue => e + logger.error " Failed: #{e.class.name}: #{e.message}" + logger.error e.backtrace + + if Rails.env.test? + raise e + end + end + + AbilityQueue.where(id: resolved).update(completed: true) + AbilityQueue.where(id: destroy).delete_all + + unless options[:quiet] + logger.info "Completed, #{resolved.size}/#{all.size} tasks successful, #{destroy.size} tasks invalid" + end + end +end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index fc62796fc..1765efcd9 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -SA7h7Xm7xF3AEvje/xZiPMzem0wh2CysuvvB+iM6R0Epkg+z+vZG2oxI/9tVzDGELWtwoNDPeqNa0bUbGjo6gt7xyEpmQbivEVKnwcbVK+1bxPHKke+hveVEUhmY/axFIIpty+VTnZVnrJkZ35/rW9egcRr5O2HikuAuBz8KrYLvB2w3h8EdmcIbbfjMvKWlUVvc8D9t3v+k56B1CbTghTmGeG0NLijWAA+U+pDmErPOR9JUAVY/nbF2/T8aQ5Szameuoj88Pk5P6alFDHwlWUSHhuEY5Z7A5LKM1NtzlTGc+CAcTVzKvx6DCg8WUJugy3uS7Bpp6uNj9TvJoZzrb0i5G1nDmIl7tK2GCNclp3JFEklrUJIrF4asRuDQUzdvuPeTEXNyi2cc5d6hSBG+MwEvG90P6n0td5+gv+rr1FCKuzP5ftyxqPsnvZuBKRxNSw0+VTTXhTO73jcwttHYrD9Jwc9tVbCVqegwsEO0dB0sNL6M89rudDMbpPnz7rJXr6lXD4Az3qJ/0N3ubYDwiuGOk7UHDntm4Id4BFDm5qeLeOUg1n2PDhsQUWh2Lfnk7ADvjQMinfXhdAQkYVim9sDtNO/Grn9xSXfCn3vs1xOdTtKLb23+EIxO2STtkHTsJG5XvxC2jmzL5/zQx/vO/zepHom7POdso4Ygoqw9oEttm/KPG85lcX0edXv/l1i5JBMMbzaQgd1PL4uWwrv+3LoSq+vQORijR50OiLdlMZ5IFn+QagJEdeVtUiCbYA3AhUK/FcH3L5QtSGNObEUrUJ4fVuWP6X8E34vd+DoY++D6PtG89SFIGBRmQ23cT111uIw3p2A9HP4N9B/uNKwxAnr4S1he6EkoA5CtDE0NZufy25s0IVf//FDWXS8FDg3ZhtOUeMWB0dew9VeCCWwr8RrLu4jP46IvVqfFeiVWf7gJcenEJ0QuqPlBeukSpgNxckDjwijRz5obzeS9ZAWYgCZg2kg/6CmuIM2VjkWMJ13uKYkFgpheoNsk1jZwH4+0fdm+Zc+fE4KQFcSkkazea/qEanN2/tDo6pRowSlRpi/nWJqHxcu87QMm7cBq5/E9DNjwyKOPmgr7c2NzDrlPYMRgzdcauPz6Y4qfcg9C6lKwJIo18VQhrqwh7wW+nNTov3Shw/yclkVrxnHTQbyH3mOstJsVPpYeYewxOQj/gikvvkx/lUeUnyo2/Ebff2nJCbnLmJ8/7Vwz9c/yvg==--d1HPd+AnhFU1woqD--z2ikkRr6d5WTzC7X5xOVPA== \ No newline at end of file +ukMy8KvWRehfYquyl3GRlaSkFk/8OAjIw3HK2LIH4u1HhtiY2XrkXbwg3rKeCofCW1K5lxNf7r+w+v0IMfVnOkmOPI0eB/TNotrq0ieomzMnhYOXzBBQb/31vFFMFCGOZIRbk9vyduQGg/wifWMjoEk9ooL++YP2q+fXm/80d2gDF/G4hMNgdAq0SFPjn3cVM2Qty1ru1BuOJwgqo4yKDBjvuL7ZOxudDuPnKujY2IpgykCkKCk7TE80v9B4ZKdIi27P1ciWmP5Kc50jk7SWlhHVbo3FdJy1a/n5RadGq/vdlwOXFdyNUvlRF7xRvjlgiqoyqDHcyOuOyIP86v2qgeDl5Y7yYwIhe8FwqPy/oW/jMDP8OvDZuWtNwpxIFJYT7sKdvIvEvjrPR+0pyTVsVMZomTiUIcFC/IA9KV+DVjtj+Tfy2UZH4W8AU6i8hObiSs4CbT081L2AHc0ZcLKMI/bh8+vBay4J2UB6vcBbpNbAaNPyj7bXsrM0+YxpPJidgrEMyoVt+fIiA5H/kXhp8WV3Jkr2eEriq5z9+m6IZFSFs58i06hN0ZH5WHUdhJcVIgMBiLOex4SVd9zL6NW2y+ZK9Fs27z8u22zn+51oQHcpz2wbmVV3sGBYaFLFinqm3/HoW60eAcvM4SfXErz0fwIzq7yN7XIY9q5/6E0hhAdHZQqVlEJWOhUAIH9ik+Ga90gWLfkkoym/G1PIUYnmeJtLUKYyj94b+YdxQNkgzGtJA7FCGVmNUuy8wCw0OIG1hg3z9TSg5mbN7ZNhBRVDvKQENJqg9tFXM0MgXn+lQ0v+io4ds6Ist2BGr4gJIz5R7bRFOIG6G60/+ZEfPBjNcj5rrGYDBATmqKo9myaSLQ68WyJB37Fs3CchM4lAdoVbXnEyJwP8HGRaips+ZhjBVvSs/8w7yQteBswFrc00+gts5TpqCz3AjNNMsHTP5cr5IhqoNWdpxvFgHHLBAPtn0HWJ5C3OP3X/4GhxG3G9Z99pxbH1ZjWn1vnKadKUfY4vzPYCZsMaokTr2/XuL8wROLohCD53SVATsGsbiL947u+X6WYEZVuOH2IY5xMhTO9JW8DYb0Rm31Te8oOpzoIrz9Ck70YDQcADiNg4SEeLTQlpve40C5+YI5Xr+60GbOHRHSGv31ENnubVTCTv3q6otE0iqCOAQ62RdqhzT23aaz0NuH5DtLIlWn/3ev9Js6mm1rnF7AzVKey01IsS--p3woHV6iIC4KuPxP--fRXQKncEML1whOWmVm2Z8w== diff --git a/scripts/change_to_wilson_scores.rb b/scripts/change_to_wilson_scores.rb deleted file mode 100644 index ae0389346..000000000 --- a/scripts/change_to_wilson_scores.rb +++ /dev/null @@ -1,33 +0,0 @@ -# First we grab votes so we can populate upvote_count and downvote_count -puts "01: Vote grab" -votes = Vote.unscoped.all.group(:post_id, :vote_type).count - -# Format translation -# {[123, 1] => 34, [123, -1] => 43, [124, 1] => 45, [124, -1] => 56} into -# {123 => {1 => 34, -1 => 43}, 124 => {1 => 45, -1 => 56}} -puts "02: Vote translation" -all_ids = Post.unscoped.all.pluck(:id).map { |i| [i, {}] }.to_h -votes = all_ids.merge(votes.to_a.group_by { |v| v[0][0] }.map { |i, v| [i, v.map { |g| [g[0][1], g[1]] }.to_h] }.to_h) - -# Generate and execute sanitized update SQL for each post. -progress = ProgressBar.create(title: "03: UPDATEs", total: votes.size, progress_mark: '█') -votes.each do |post_id, vote_counts| - params = [] - vote_counts = { 1 => 0, -1 => 0 }.merge(vote_counts) - updates = vote_counts.map do |vt, count| - attrib = { 1 => 'upvote_count', -1 => 'downvote_count' }[vt] - params << count - "#{attrib} = ?" - end - params << post_id - sql = "UPDATE posts SET #{updates.join(', ')} WHERE id = ?" - sanitized = ActiveRecord::Base.sanitize_sql_array([sql, *params]) - ActiveRecord::Base.connection.execute sanitized - progress.increment -end - -puts "04: update scores" -score_update = "UPDATE posts p INNER JOIN (SELECT * FROM " \ - "(SELECT id, (upvote_count + 2)/(upvote_count + downvote_count + 4) AS score FROM posts) i) q " \ - "ON p.id = q.id SET p.score = q.score" -ActiveRecord::Base.connection.execute score_update \ No newline at end of file diff --git a/scripts/cleanup_votes.rb b/scripts/cleanup_votes.rb index 67707824e..2420dafed 100644 --- a/scripts/cleanup_votes.rb +++ b/scripts/cleanup_votes.rb @@ -1 +1 @@ -CleanupVotesJob.perform_later \ No newline at end of file +CleanupVotesJob.perform_now diff --git a/scripts/create_backup_2fa_codes.rb b/scripts/create_backup_2fa_codes.rb deleted file mode 100644 index ddeac6793..000000000 --- a/scripts/create_backup_2fa_codes.rb +++ /dev/null @@ -1,4 +0,0 @@ -User.where(enabled_2fa: true).each do |user| - user.update(backup_2fa_code: SecureRandom.alphanumeric(24)) - TwoFactorMailer.with(user: user, host: 'meta.codidact.com').backup_code.deliver_now -end diff --git a/scripts/create_general_thread_for_old_comments.rb b/scripts/create_general_thread_for_old_comments.rb deleted file mode 100644 index 0a05726f0..000000000 --- a/scripts/create_general_thread_for_old_comments.rb +++ /dev/null @@ -1,15 +0,0 @@ -Post.unscoped.where(id: Comment.unscoped.where(comment_thread: nil) - .select(Arel.sql('distinct post_id'))).each do |post| - comments = Comment.unscoped.where(post: post, comment_thread: nil) - next unless comments.any? - - comments = comments.all - - new_thread = CommentThread.new(title: 'General comments', post: post, reply_count: comments.size, locked: false, - archived: false, deleted: false, community: post.community) - new_thread.save - - comments.update_all(comment_thread_id: new_thread.id) - - puts "#{comments.size} comments updated for post Id=#{post.id}" -end \ No newline at end of file diff --git a/scripts/mail_uncaptured_donations.rb b/scripts/mail_uncaptured_donations.rb index e82cfabd0..2cc89b570 100644 --- a/scripts/mail_uncaptured_donations.rb +++ b/scripts/mail_uncaptured_donations.rb @@ -1,18 +1 @@ -intents = Stripe::PaymentIntent.list -intents.auto_paging_each do |pi| - begin - break unless Time.at(pi.created) >= 24.hours.ago - next unless pi.status == 'requires_payment_method' - next unless pi.metadata['user_id'].present? - next if pi.metadata['emailed'].present? - user = User.find(pi.metadata['user_id']) - symbol = { 'GBP' => '£', 'USD' => '$', 'EUR' => '€' }[pi.currency.upcase] - amount = pi.amount / 100 - DonationMailer.with(currency: symbol, amount: amount, email: user.email, name: user.username, intent: pi) - .donation_uncaptured.deliver_now - Stripe::PaymentIntent.update(pi.id, { metadata: { emailed: true } }) - puts "Mailed ##{user.id} for PaymentIntent #{pi.id}" - rescue => ex - Stripe::PaymentIntent.update(pi.id, { metadata: { email_error: "#{ex.message}" } }) - end -end +MailUncapturedDonations.perform_now diff --git a/scripts/recalc_abilities.rb b/scripts/recalc_abilities.rb index cb2566483..09733238c 100644 --- a/scripts/recalc_abilities.rb +++ b/scripts/recalc_abilities.rb @@ -1,58 +1,15 @@ -options = OpenStruct.new +options = {} OptionParser.new do |opts| opts.banner = "Usage: rails r scripts/recalc_abilities.rb [options]" opts.on('-v', '--verbose', 'Run with additional logging') do - options.verbose = true + options[:verbose] = true end opts.on('-q', '--quiet', 'Run with minimal logging') do - options.quiet = true + options[:quiet] = true end end.parse! -resolved = [] -destroy = [] -all = AbilityQueue.pending.to_a - -all.each do |q| - begin - cu = q.community_user - u = cu&.user - - if cu.nil? || u.nil? - destroy << q.id - next - end - - RequestContext.community = cu.community - - if options.verbose && !options.quiet - puts "Scope: Community : #{cu.community.name} (#{cu.community.host})" - puts " User : #{u.username} (#{cu.user_id})" - puts " CommunityUser : #{cu.id}" - elsif !options.verbose && !options.quiet - puts "Scope: CommunityUser : #{cu.id}" - end - - cu.recalc_abilities - - # Grant mod ability if mod status is given - if cu.at_least_moderator? && !cu.ability?('mod') - cu.grant_ability!('mod') - end - - resolved << q.id - rescue => e - $stderr.puts " Failed: #{e.class.name}: #{e.message}" - $stderr.puts e.backtrace - end -end - -AbilityQueue.where(id: resolved).update(completed: true) -AbilityQueue.where(id: destroy).delete_all - -unless options.quiet - puts "Completed, #{resolved.size}/#{all.size} tasks successful, #{destroy.size} tasks invalid" -end +RecalcAbilitiesJob.perform_now(options) diff --git a/scripts/recalc_abilities_upon_first_migration.rb b/scripts/recalc_abilities_upon_first_migration.rb deleted file mode 100644 index b7394be55..000000000 --- a/scripts/recalc_abilities_upon_first_migration.rb +++ /dev/null @@ -1,26 +0,0 @@ -puts "Recalculating all the Abilities" -puts - -User.unscoped.all.map do |u| - puts "Scope: User.Id=#{u.id}" - - u.community_users.each do |cu| - puts " Attempt CommunityUser.Id=#{cu.id}" - RequestContext.community = cu.community - cu.recalc_privileges! - - if cu.at_least_moderator? && !cu.privilege?('mod') - puts " Granting mod privilege to CommunityUser.Id=#{cu.id}" - cu.grant_privilege!('mod') - end - - puts " Recalc success for CommunityUser.Id=#{cu.id}" - rescue - puts " !!! Error recalcing for CommunityUser.Id=#{cu.id}" - end - puts "End" - puts -end - -puts "---" -puts "Recalculating completed" diff --git a/scripts/run_complaints_closure.rb b/scripts/run_complaints_closure.rb index b3fc37fc0..487cbb12c 100644 --- a/scripts/run_complaints_closure.rb +++ b/scripts/run_complaints_closure.rb @@ -1 +1 @@ -AutoCloseComplaintsJob.perform_later \ No newline at end of file +AutoCloseComplaintsJob.perform_now diff --git a/scripts/run_new_thread_followers_cleanup.rb b/scripts/run_new_thread_followers_cleanup.rb index 73305dbde..8170d081b 100644 --- a/scripts/run_new_thread_followers_cleanup.rb +++ b/scripts/run_new_thread_followers_cleanup.rb @@ -1 +1 @@ -CleanUpNewThreadFollowersJob.perform_later +CleanUpNewThreadFollowersJob.perform_now diff --git a/scripts/run_spam_cleanup.rb b/scripts/run_spam_cleanup.rb index 82122f9c5..a6d0afd4b 100644 --- a/scripts/run_spam_cleanup.rb +++ b/scripts/run_spam_cleanup.rb @@ -1,2 +1,2 @@ -CleanUpSpammyUsersJob.perform_later -PotentialSpamProfilesJob.perform_later +CleanUpSpammyUsersJob.perform_now +PotentialSpamProfilesJob.perform_now diff --git a/scripts/run_summary_mailer.rb b/scripts/run_summary_mailer.rb index ce82f1544..99dbc5e16 100644 --- a/scripts/run_summary_mailer.rb +++ b/scripts/run_summary_mailer.rb @@ -1 +1 @@ -SendSummaryEmailsJob.perform_later +SendSummaryEmailsJob.perform_now diff --git a/scripts/run_thread_followers_cleanup.rb b/scripts/run_thread_followers_cleanup.rb index 06bbee308..e304c345d 100644 --- a/scripts/run_thread_followers_cleanup.rb +++ b/scripts/run_thread_followers_cleanup.rb @@ -1 +1 @@ -CleanUpThreadFollowersJob.perform_later +CleanUpThreadFollowersJob.perform_now diff --git a/test/controllers/donations_controller_test.rb b/test/controllers/donations_controller_test.rb index 21fa129a2..e675e2f26 100644 --- a/test/controllers/donations_controller_test.rb +++ b/test/controllers/donations_controller_test.rb @@ -3,6 +3,14 @@ class DonationsControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers + setup do + WebMock.allow_net_connect! + end + + teardown do + WebMock.disable_net_connect! + end + test 'should get index' do get :index assert_response(:success) diff --git a/test/jobs/mail_uncaptured_donations_job_test.rb b/test/jobs/mail_uncaptured_donations_job_test.rb new file mode 100644 index 000000000..ac62482c5 --- /dev/null +++ b/test/jobs/mail_uncaptured_donations_job_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' + +class MailUncapturedDonationsJobTest < ActiveJob::TestCase + setup do + WebMock.allow_net_connect! + end + + teardown do + WebMock.disable_net_connect! + end + + test 'should run job successfully' do + skip unless Stripe.api_key + WebMock.allow_net_connect! + perform_enqueued_jobs do + MailUncapturedDonationsJob.perform_later + end + assert_performed_jobs 1 + end +end diff --git a/test/jobs/recalc_abilities_job_test.rb b/test/jobs/recalc_abilities_job_test.rb new file mode 100644 index 000000000..6e56bc0c9 --- /dev/null +++ b/test/jobs/recalc_abilities_job_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' + +class RecalcAbilitiesJobTest < ActiveJob::TestCase + test 'should run job successfully' do + perform_enqueued_jobs do + RecalcAbilitiesJob.perform_later(verbose: false, quiet: true) + end + assert_performed_jobs 1 + end +end