diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/pull_email_job.rb b/app/jobs/pull_email_job.rb new file mode 100644 index 00000000..e9235774 --- /dev/null +++ b/app/jobs/pull_email_job.rb @@ -0,0 +1,86 @@ +require 'logger' + +class PullEmailJob < ApplicationJob + queue_as :default + + def perform(*args) + STDOUT.sync = true + + logger = Logger.new(STDOUT) + logger.level = Logger::INFO + + Rails.application.credentials.fetch(:email) { raise 'Could not find `email` credentials!' } + Rails.application.credentials.email.fetch(:email) { raise 'Could not find `email` in `email` credentials!' } + Rails.application.credentials.email.fetch(:name) { raise 'Could not find `name` in `email` credentials!' } + Rails.application.credentials.email.fetch(:port) { raise 'Could not find `port` in `email` credentials!' } + Rails.application.credentials.email.fetch(:host) { raise 'Could not find `host` in `email` credentials!' } + Rails.application.credentials.email.fetch(:ssl) { raise 'Could not find `ssl` in `email` credentials!' } + # Cannot used nested hashes in credentials without [] in Rails 6 + # https://blog.saeloun.com/2021/06/02/rails-access-nested-secrects-by-method-call/ + if Rails.application.credentials.email[:oauth].nil? && Rails.application.credentials.email[:password].nil? + raise 'Could not find `oauth` or `password` in `email` credentials!' + elsif !Rails.application.credentials.email[:oauth].nil? && !Rails.application.credentials.email[:password].nil? + raise 'Found both `oauth` and `password` in `email` credentials!' + elsif !Rails.application.credentials.email[:oauth].nil? + Rails.application.credentials.email[:oauth].fetch(:site) { raise 'Could not find `site` in `email.oauth` credentials!' } + Rails.application.credentials.email[:oauth].fetch(:authorize_url) { raise 'Could not find `authorize_url` in `email.oauth` credentials!' } + Rails.application.credentials.email[:oauth].fetch(:token_url) { raise 'Could not find `token_url` in `email.oauth` credentials!' } + Rails.application.credentials.email[:oauth].fetch(:refresh_token) { raise 'Could not find `refresh_token` in `email.oauth` credentials!' } + Rails.application.credentials.email[:oauth].fetch(:client_id) { raise 'Could not find `client_id` in `email.oauth` credentials!' } + Rails.application.credentials.email[:oauth].fetch(:client_secret) { raise 'Could not find `client_secret` in `email.oauth` credentials!' } + end + config = Rails.application.credentials.email + + reconnectSleep = 1 + + logger.info("Logging in to mailbox #{config[:email]}") + + begin + imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) + imap.capable?(:IMAP4rev1) or raise "Not an IMAP4rev1 server" + if imap.auth_capable?("XOAUTH2") && !Rails.application.credentials.email[:oauth].nil? + oauth_client = OAuth2::Client.new(config[:oauth][:client_id], config[:oauth][:client_secret], {site: config[:oauth][:site], authorize_url: config[:oauth][:authorize_url], token_url: config[:oauth][:token_url]}) + access_token = OAuth2::AccessToken.from_hash(oauth_client, refresh_token: config[:oauth][:refresh_token]).refresh! + imap.authenticate('XOAUTH2', config[:email], access_token.token) + elsif imap.auth_capable?("PLAIN") + imap.authenticate("PLAIN", config[:email], config[:password]) + # Should not use deprecated LOGIN method + # elsif !imap.capability?("LOGINDISABLED") + # imap.login(config[:email], config[:password]) + else + raise "No acceptable authentication mechanisms" + end + rescue Net::IMAP::NoResponseError, SocketError, Faraday::ConnectionFailed => error + logger.error("Could not authenticate for #{config[:email]}, error: #{error.message}") + return + end + + begin + imap.select(config[:name]) + + while true + logger.info("Pulling emails for #{config[:email]}") + query = ["BEFORE", Net::IMAP.format_date(Time.now + 1.day)] + latest = Email.order("timestamp DESC").first + query = ["SINCE", Net::IMAP.format_date(latest.timestamp)] if latest + + ids = imap.search(query) + imap.fetch(ids, "BODY.PEEK[]").each do |msg| + mail = Mail.new(msg.attr["BODY[]"]) + + unless Email.where(message_id: mail.message_id).exists? + begin + unless Email.create_from_mail(mail) + logger.error("Could not pull message #{mail.message_id}") + end + rescue Exception => e + logger.error("Exception while loading message #{mail.message_id}: " + e.to_s + "\n" + e.backtrace.join("\n")) + end + end + end + end + rescue Net::IMAP::Error, EOFError, Errno::ECONNRESET => e + logger.error("Disconnected for mailbox #{config[:email]}") + end + end +end diff --git a/app/jobs/send_event_slack_notifications_job.rb b/app/jobs/send_event_slack_notifications_job.rb new file mode 100644 index 00000000..4e49048e --- /dev/null +++ b/app/jobs/send_event_slack_notifications_job.rb @@ -0,0 +1,102 @@ +class SendEventSlackNotificationsJob < ApplicationJob + queue_as :default + + def perform(*args) + Rails.application.routes.default_url_options = Rails.application.config.action_mailer.default_url_options + + STDOUT.sync = true + + logger = Logger.new(STDOUT) + + + Rails.application.credentials.fetch(:slack) { raise 'Could not find `slack` credentials!' } + Rails.application.credentials.slack.fetch(:token) { raise 'Could not find `token` in `slack` credentials!' } + env_config = Rails.application.credentials.slack + + logger.info("Logging into Slack") + Slack.configure do |config| + config.token = env_config[:token] + end + client = Slack::Web::Client.new + + channel = + if Rails.env.development? + "#bot-testing" + elsif Rails.env.staging? + "#bot-testing" + else + "#events" + end + channel_social = + if Rails.env.development? + "#bot-testing" + elsif Rails.env.staging? + "#bot-testing" + else + "#social" + end + + startdate = DateTime.now + enddate = 1.hour.from_now + + calls = Eventdate.where(events: {textable: true, status: Event::Event_Status_Group_Not_Cancelled}).call_between(startdate, enddate).includes(:event).references(:event) + strikes = Eventdate.where(events: {textable: true, status: Event::Event_Status_Group_Not_Cancelled}).strike_between(startdate, enddate).includes(:event).references(:event) + + def message_gen(msg, event_url, eventdate) + [ + msg, + { + type: "section", + text: { + type: "mrkdwn", + text: msg + "\n_" + eventdate.locations.join(", ") + "_" + }, + accessory: { + type: "button", + text: { + type: "plain_text", + text: "View on Tracker", + emoji: true, + }, + url: event_url + } + } + ] + end + + messages = [] + messages_social = [] + calls.each do |eventdate| + event_url = Rails.application.routes.url_helpers.url_for(eventdate.event).to_s + msg = "Call for <" + event_url + "|" + eventdate.event.title + "> - " + eventdate.description + " is at " + eventdate.effective_call.strftime("%H:%M") + messages.push(message_gen(msg, event_url, eventdate)) + messages_social.push(message_gen(msg, event_url, eventdate)) if eventdate.event.textable_social + end + strikes.each do |eventdate| + event_url = Rails.application.routes.url_helpers.url_for(eventdate.event).to_s + msg = "Strike for <" + event_url + "|" + eventdate.event.title + "> - " + eventdate.description + " is at " + eventdate.effective_strike.strftime("%H:%M") + messages.push(message_gen(msg, event_url, eventdate)) + messages_social.push(message_gen(msg, event_url, eventdate)) if eventdate.event.textable_social + end + + messages_text = messages.map { |msg| msg[0] } + messages_blocks = messages.map { |msg| msg[1] } + messages_social_text = messages_social.map { |msg| msg[0] } + messages_social_blocks = messages_social.map { |msg| msg[1] } + + unless messages.empty? + message_text = messages_text.join("\n") + + logger.info("Sending message") + client.chat_postMessage(channel: channel, text: message_text, as_user: true, blocks: messages_blocks) + end + + unless messages_social.empty? + message_text = messages_social_text.join("\n") + + logger.info("Sending social message") + client.chat_postMessage(channel: channel_social, text: message_text, as_user: true, blocks: messages_blocks) + end + + end +end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb deleted file mode 100644 index c4b77bdb..00000000 --- a/app/mailers/admin_mailer.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AdminMailer < ActionMailer::Base - def cleanup_backups - mail to: "abtech@andrew.cmu.edu" - end -end diff --git a/app/views/admin_mailer/cleanup_backups.text.erb b/app/views/admin_mailer/cleanup_backups.text.erb deleted file mode 100644 index fb601dd0..00000000 --- a/app/views/admin_mailer/cleanup_backups.text.erb +++ /dev/null @@ -1,3 +0,0 @@ -This is a quarterly reminder to clean out the tracker database backups folder on AFS. - ---Automated Email from AB Tech Tracker, do not respond diff --git a/deploy/systemd/abtech-tracker-email-idle@.service b/deploy/systemd/abtech-tracker-email-idle@.service deleted file mode 100644 index af2fedd2..00000000 --- a/deploy/systemd/abtech-tracker-email-idle@.service +++ /dev/null @@ -1,44 +0,0 @@ -[Unit] -Description=AB Tech Tracker Email Idle Task -After=network.target - -# Only run if associated Rails is running, otherwise it might be down for -# development -After=abtech-tracker@.service - -[Service] -# * -# * BEGIN TRACKER EMAIL IDLE TASK CONFIG DEFAULTS -# * -# * Override options in this block by using the `systemctl edit` command. -# * Use this command; DO NOT EDIT THIS FILE ON THE DEPLOYED SYSTEM. This file -# * documents the options. -# * - -# Tracker Environment File -# ======================== -# The working directory should be the location of the repo. -EnvironmentFile=/srv/abtech-tracker/%i/tracker.env - -# Service Working Directory -# ========================= -# The working directory should be the location of the repo. -WorkingDirectory=/srv/abtech-tracker/%i/repo - -# Service User -# ============ -# This user will run Tracker. Preferably configure a non-privileged user that -# does not need edit access to the rbenv root. If you override this, then you -# also need to change the associated systemd socket file. -User=deploy-abtech-tracker - -# * -# * END TRACKER EMAIL IDLE TASK CONFIG DEFAULTS -# * - -Type=simple - -ExecStart=/srv/abtech-tracker/%i/rbenv/shims/bundle exec --keep-file-descriptors rails email:idle - -[Install] -WantedBy=multi-user.target diff --git a/deploy/systemd/abtech-tracker-slack-notify@.service b/deploy/systemd/abtech-tracker-slack-notify@.service deleted file mode 100644 index 9d77c67f..00000000 --- a/deploy/systemd/abtech-tracker-slack-notify@.service +++ /dev/null @@ -1,44 +0,0 @@ -[Unit] -Description=AB Tech Tracker Slack Notification Task -After=network.target - -# Only run if associated Rails is running, otherwise it might be down for -# development -After=abtech-tracker@.service - -[Service] -# * -# * BEGIN TRACKER SLACK NOTIFICATION TASK CONFIG DEFAULTS -# * -# * Override options in this block by using the `systemctl edit` command. -# * Use this command; DO NOT EDIT THIS FILE ON THE DEPLOYED SYSTEM. This file -# * documents the options. -# * - -# Tracker Environment File -# ======================== -# The working directory should be the location of the repo. -EnvironmentFile=/srv/abtech-tracker/%i/tracker.env - -# Service Working Directory -# ========================= -# The working directory should be the location of the repo. -WorkingDirectory=/srv/abtech-tracker/%i/repo - -# Service User -# ============ -# This user will run Tracker. Preferably configure a non-privileged user that -# does not need edit access to the rbenv root. If you override this, then you -# also need to change the associated systemd socket file. -User=deploy-abtech-tracker - -# * -# * END TRACKER SLACK NOTIFICATION TASK CONFIG DEFAULTS -# * - -Type=oneshot - -ExecStart=/srv/abtech-tracker/%i/rbenv/shims/bundle exec --keep-file-descriptors rails slack:notify - -[Install] -WantedBy=multi-user.target diff --git a/deploy/systemd/abtech-tracker-slack-notify@.timer b/deploy/systemd/abtech-tracker-slack-notify@.timer deleted file mode 100644 index 9ee3d39d..00000000 --- a/deploy/systemd/abtech-tracker-slack-notify@.timer +++ /dev/null @@ -1,25 +0,0 @@ -[Unit] -Description=AB Tech Tracker Slack Notification Task Timer - -[Timer] -# * -# * BEGIN TRACKER SLACK NOTIFICATION TASK TIMER CONFIG DEFAULTS -# * -# * Override options in this block by using the `systemctl edit` command. -# * Use this command; DO NOT EDIT THIS FILE ON THE DEPLOYED SYSTEM. This file -# * documents the options. -# * - -# Trigger datetime -# ================ -# Hourly -OnCalendar=*-*-* *:00:00 - -# * -# * END TRACKER SLACK NOTIFICATION TASK TIMER CONFIG DEFAULTS -# * - -Unit=abtech-tracker-slack-notify@.service - -[Install] -WantedBy=timers.target diff --git a/lib/tasks/admin.rake b/lib/tasks/admin.rake deleted file mode 100644 index ce26551e..00000000 --- a/lib/tasks/admin.rake +++ /dev/null @@ -1,4 +0,0 @@ -desc "Send a email reminder about cleaning up the database backups folder" -task :email_backup_reminder => :environment do - AdminMailer.cleanup_backups.deliver_now -end \ No newline at end of file diff --git a/lib/tasks/email.rake b/lib/tasks/email.rake deleted file mode 100644 index 59e875ce..00000000 --- a/lib/tasks/email.rake +++ /dev/null @@ -1,107 +0,0 @@ -require 'logger' - -namespace :email do - desc "Check for email continuously in the background" - task :idle => :environment do - STDOUT.sync = true - - logger = Logger.new(STDOUT) - logger.level = Logger::INFO - - Rails.application.credentials.fetch(:email) { raise 'Could not find `email` credentials!' } - Rails.application.credentials.email.fetch(:email) { raise 'Could not find `email` in `email` credentials!' } - Rails.application.credentials.email.fetch(:name) { raise 'Could not find `name` in `email` credentials!' } - Rails.application.credentials.email.fetch(:port) { raise 'Could not find `port` in `email` credentials!' } - Rails.application.credentials.email.fetch(:host) { raise 'Could not find `host` in `email` credentials!' } - Rails.application.credentials.email.fetch(:ssl) { raise 'Could not find `ssl` in `email` credentials!' } - # Cannot used nested hashes in credentials without [] in Rails 6 - # https://blog.saeloun.com/2021/06/02/rails-access-nested-secrects-by-method-call/ - if Rails.application.credentials.email[:oauth].nil? && Rails.application.credentials.email[:password].nil? - raise 'Could not find `oauth` or `password` in `email` credentials!' - elsif !Rails.application.credentials.email[:oauth].nil? && !Rails.application.credentials.email[:password].nil? - raise 'Found both `oauth` and `password` in `email` credentials!' - elsif !Rails.application.credentials.email[:oauth].nil? - Rails.application.credentials.email[:oauth].fetch(:site) { raise 'Could not find `site` in `email.oauth` credentials!' } - Rails.application.credentials.email[:oauth].fetch(:authorize_url) { raise 'Could not find `authorize_url` in `email.oauth` credentials!' } - Rails.application.credentials.email[:oauth].fetch(:token_url) { raise 'Could not find `token_url` in `email.oauth` credentials!' } - Rails.application.credentials.email[:oauth].fetch(:refresh_token) { raise 'Could not find `refresh_token` in `email.oauth` credentials!' } - Rails.application.credentials.email[:oauth].fetch(:client_id) { raise 'Could not find `client_id` in `email.oauth` credentials!' } - Rails.application.credentials.email[:oauth].fetch(:client_secret) { raise 'Could not find `client_secret` in `email.oauth` credentials!' } - end - config = Rails.application.credentials.email - - reconnectSleep = 1 - - logger.info("Logging in to mailbox #{config[:email]}") - while true - begin - imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) - imap.capable?(:IMAP4rev1) or raise "Not an IMAP4rev1 server" - if imap.auth_capable?("XOAUTH2") && !Rails.application.credentials.email[:oauth].nil? - oauth_client = OAuth2::Client.new(config[:oauth][:client_id], config[:oauth][:client_secret], {site: config[:oauth][:site], authorize_url: config[:oauth][:authorize_url], token_url: config[:oauth][:token_url]}) - access_token = OAuth2::AccessToken.from_hash(oauth_client, refresh_token: config[:oauth][:refresh_token]).refresh! - imap.authenticate('XOAUTH2', config[:email], access_token.token) - elsif imap.auth_capable?("PLAIN") - imap.authenticate("PLAIN", config[:email], config[:password]) - # Should not use deprecated LOGIN method - # elsif !imap.capability?("LOGINDISABLED") - # imap.login(config[:email], config[:password]) - else - raise "No acceptable authentication mechanisms" - end - rescue Net::IMAP::NoResponseError, SocketError, Faraday::ConnectionFailed => error - logger.info("Could not authenticate for #{config[:email]}, trying again in #{reconnectSleep} #{"second".pluralize(reconnectSleep)}, error: #{error.message}") - sleep reconnectSleep - reconnectSleep += 1 - - next - end - - reconnectSleep = 1 - - begin - imap.select(config[:name]) - - while true - logger.info("Pulling emails for #{config[:email]}") - query = ["BEFORE", Net::IMAP.format_date(Time.now + 1.day)] - latest = Email.order("timestamp DESC").first - query = ["SINCE", Net::IMAP.format_date(latest.timestamp)] if latest - - ids = imap.search(query) - imap.fetch(ids, "BODY.PEEK[]").each do |msg| - mail = Mail.new(msg.attr["BODY[]"]) - - unless Email.where(message_id: mail.message_id).exists? - begin - unless Email.create_from_mail(mail) - logger.error("Could not pull message #{mail.message_id}") - end - rescue Exception => e - logger.error("Exception while loading message #{mail.message_id}: " + e.to_s + "\n" + e.backtrace.join("\n")) - end - end - end - - logger.info("Idling for #{config[:email]}") - - waiting = Thread.start do - sleep(20.minutes) - - imap.idle_done - end - - imap.idle do |response| - if response.respond_to?(:name) && response.name == 'EXISTS' - waiting.kill - imap.idle_done - end - end - end - rescue Net::IMAP::Error, EOFError, Errno::ECONNRESET => e - logger.info("Disconnected for mailbox #{config[:email]}, reconnecting") - next - end - end - end -end diff --git a/test/jobs/pull_email_job_test.rb b/test/jobs/pull_email_job_test.rb new file mode 100644 index 00000000..d342cec3 --- /dev/null +++ b/test/jobs/pull_email_job_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class PullEmailJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/jobs/send_event_slack_notifications_job_test.rb b/test/jobs/send_event_slack_notifications_job_test.rb new file mode 100644 index 00000000..1c8dd116 --- /dev/null +++ b/test/jobs/send_event_slack_notifications_job_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class SendEventSlackNotificationsJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end