diff --git a/Gemfile b/Gemfile index 28dc5451..72e9a1f7 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem 'rest-client' gem 'geoip' gem 'io-extra', require: 'io/extra' gem 'rye' +gem 'timecop' platform :mri, :rbx do gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic diff --git a/Gemfile.lock b/Gemfile.lock index f519fdc1..2d23c77d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -227,6 +227,7 @@ GEM thread (0.1.4) thread_safe (0.3.4) tilt (1.4.1) + timecop (0.7.4) timers (1.1.0) treetop (1.4.15) polyglot @@ -307,6 +308,7 @@ DEPENDENCIES stripe-ruby-mock (~> 2.0.1) thread tilt + timecop webmock zipruby diff --git a/app/admin.rb b/app/admin.rb index 287945b0..4d54551b 100644 --- a/app/admin.rb +++ b/app/admin.rb @@ -5,6 +5,48 @@ get '/admin' do erb :'admin' end +get '/admin/email' do + require_admin + erb :'admin/email' +end + +post '/admin/email' do + require_admin + + %i{subject body}.each do |k| + if params[k].nil? || params[k].empty? + flash[:error] = "#{k.capitalize} is missing." + redirect '/admin/email' + end + end + + sites = Site.newsletter_sites + + day = 0 + + until sites.empty? + queued_sites = [] + Site::EMAIL_BLAST_MAXIMUM_PER_DAY.times { + break if sites.empty? + queued_sites << sites.pop + } + + queued_sites.each do |site| + EmailWorker.perform_at(day.days.from_now, { + from: 'noreply@neocities.org', + to: site.email, + subject: params[:subject], + body: params[:body] + }) + end + + day += 1 + end + + flash[:success] = "#{sites.length} emails have been queued, #{Site::EMAIL_BLAST_MAXIMUM_PER_DAY} per day." + redirect '/' +end + post '/admin/banip' do require_admin site = Site[username: params[:username]] diff --git a/models/site.rb b/models/site.rb index fcdd0861..b57b3e80 100644 --- a/models/site.rb +++ b/models/site.rb @@ -118,6 +118,15 @@ class Site < Sequel::Model maximum_site_files: 1000 ) + def self.newsletter_sites + Site.select(:email). + exclude(email: 'nil').exclude(is_banned: true). + where{updated_at > EMAIL_BLAST_MAXIMUM_AGE}. + where{changed_count > 0}. + order(:updated_at.desc). + all + end + def too_many_files?(file_count=0) (site_files_dataset.count + file_count) > plan_feature(:maximum_site_files) end @@ -140,6 +149,14 @@ class Site < Sequel::Model BROWSE_PAGINATION_LENGTH = 100 + EMAIL_BLAST_MAXIMUM_AGE = 6.months.ago + + if ENV['RACK_ENV'] == 'test' + EMAIL_BLAST_MAXIMUM_PER_DAY = 2 + else + EMAIL_BLAST_MAXIMUM_PER_DAY = 2000 + end + many_to_many :tags one_to_many :profile_comments diff --git a/tests/acceptance/admin_tests.rb b/tests/acceptance/admin_tests.rb index 3f6923c7..32698e2d 100644 --- a/tests/acceptance/admin_tests.rb +++ b/tests/acceptance/admin_tests.rb @@ -51,4 +51,4 @@ describe '/admin' do end end -end \ No newline at end of file +end diff --git a/tests/admin_tests.rb b/tests/admin_tests.rb new file mode 100644 index 00000000..f98c11ee --- /dev/null +++ b/tests/admin_tests.rb @@ -0,0 +1,56 @@ +require_relative './environment.rb' +require 'rack/test' + +include Rack::Test::Methods + +def app + Sinatra::Application +end + +describe 'email blasting' do + before do + EmailWorker.jobs.clear + @admin_site = Fabricate :site, is_admin: true + end + + it 'works' do + DB['update sites set changed_count=?', 0].first + relevant_emails = [] + + sites_emailed_count = Site::EMAIL_BLAST_MAXIMUM_PER_DAY*2 + + sites_emailed_count.times { + site = Fabricate :site, updated_at: Time.now, changed_count: 1 + relevant_emails << site.email + } + + EmailWorker.jobs.clear + + time = Time.now + + Timecop.freeze(time) do + post '/admin/email', { + :csrf_token => 'abcd', + :subject => 'Subject Test', + :body => 'Body Test'}, { + 'rack.session' => { 'id' => @admin_site.id, '_csrf_token' => 'abcd' } + } + + relevant_jobs = EmailWorker.jobs.select{|j| relevant_emails.include?(j['args'].first['to']) } + relevant_jobs.length.must_equal sites_emailed_count + + relevant_jobs.each do |job| + args = job['args'].first + args['from'].must_equal 'noreply@neocities.org' + args['subject'].must_equal 'Subject Test' + args['body'].must_equal 'Body Test' + end + + immediate_emails = relevant_jobs.select {|j| j['at'].nil? || j['at'] == Time.now.to_f} + immediate_emails.length.must_equal Site::EMAIL_BLAST_MAXIMUM_PER_DAY + + tomorrows_emails = relevant_jobs.select {|j| j['at'] == (time+1.day.to_i).to_f} + tomorrows_emails.length.must_equal Site::EMAIL_BLAST_MAXIMUM_PER_DAY + end + end +end diff --git a/views/admin/email.erb b/views/admin/email.erb new file mode 100644 index 00000000..baf2ec38 --- /dev/null +++ b/views/admin/email.erb @@ -0,0 +1,49 @@ +
+ This sends to all emails on Neocities that have not opted out. It trickles it out, 4000/day to prevent spam report issues. +
+ + +