# frozen_string_literal: true require "google/apis/calendar_v3" require "googleauth" require "googleauth/stores/file_token_store" require "fileutils" require "active_support" require "active_support/core_ext/time/zones" require "active_support/core_ext/numeric/time" require "active_support/duration" require "chronic" require "pony" module Nora class Core extend Memoist PAIRINGS_FILE = "past_pairings.txt" PAIRINGS_FILE_SEPARATOR = " " OOB_URI = "urn:ietf:wg:oauth:2.0:oob" CLIENT_SECRETS_PATH = "nora_client_secret.json" CREDENTIALS_PATH = "calendar-ruby-quickstart.yaml" SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR FREE_BUSY_QUERY_BATCH_SIZE = 5 CONFIGURATION = JSON.parse(File.read("nora_configuration.json")) Pony.options = { via: :smtp, via_options: { address: "smtp.sendgrid.net", port: "587", domain: CONFIGURATION["email"]["domain"], authentication: :plain, enable_starttls_auto: true, # These Sendgrid credentials come from the Heroku addon. user_name: CONFIGURATION["email"]["sendgrid_configuration"]["user_name"], password: CONFIGURATION["email"]["sendgrid_configuration"]["password"] } } def initialize(weeks_ahead:, test:) @weeks_ahead = weeks_ahead @test = test FileUtils.touch(PAIRINGS_FILE) # Make sure pairings file exists. # Set global Chronic parsing time zone. Time.zone = "UTC" Chronic.time_class = Time.zone # Initialize the API @service = Google::Apis::CalendarV3::CalendarService.new @service.client_options.application_name = CONFIGURATION["calendar"]["application_name"] @service.authorization = authorize end def run! load_history! # Populate @history for load_calendars! load_calendars! # Outside the loop to reduce calls to Google API. begin shuffle_emails! create_groups! rescue SystemStackError remove_oldest_pair! retry end send_emails( template_emails_for( schedule_meetings! ) ) puts "Done." end private # Adds all calendars to NORA's calendar, so that # `@service.list_calendar_lists` will have every # calendar in the configuration file without us # having to manually add them in the UI. def load_calendars! puts "Loading calendars..." @emails = CONFIGURATION["people"].map { |p| p["email"] } emails_in_history = [] File.open(PAIRINGS_FILE).each do |line| line.split(PAIRINGS_FILE_SEPARATOR).each do |email| emails_in_history << email end end # Load all calendars that aren't in our history. (@emails - emails_in_history).each do |email| puts "Loading calendar: #{email}" @service.insert_calendar_list( Google::Apis::CalendarV3::CalendarListEntry.new(id: email) ) sleep 1 # Avoid exceeding Google's rate limit. end end def shuffle_emails! puts "Shuffling emails..." @emails = @emails.shuffle end def remove_oldest_pair! puts "Removing oldest pair and retrying..." File.open("past_pairings_tmp.txt", "w") do |file| File.open(PAIRINGS_FILE).each.with_index(1) do |line, line_num| file.puts(line) unless line_num == 1 end end FileUtils.mv("past_pairings_tmp.txt", PAIRINGS_FILE) end def load_history! puts "Loading history..." @history = Set.new File.open(PAIRINGS_FILE).each do |line| line.split(PAIRINGS_FILE_SEPARATOR).permutation.each do |emails| @history << emails end end end def create_groups! puts "Creating groups..." @emails.each_slice(group_size).each do |emails| return create_groups! unless (@history & emails.combination(2)).empty? end end # @return [Array] list of meetings, of the format: # { on: