require 'highline/import' require 'yaml' require 'net/http' require 'json' usage 'sync' aliases :s summary 'Sync events with DSA' description 'Sync future events with the DSA panel.' option :k, :key, 'API key for DSA', argument: :required def bold_say(str) say "<%= color %(#{str}), :bold %>" end def bold_ask(str, *args) res = ask "<%= color %(#{str}), :bold %>", *args puts res end DSA_API = "https://localhost:8080/api/activiteiten" # Inspired by https://github.com/nanoc/nanoc/blob/main/nanoc-cli/lib/nanoc/cli/commands/shell.rb class SyncRunner < Nanoc::CLI::Commands::Shell def run @site = load_site Nanoc::Core::Compiler.new_for(@site).run_until_preprocessed items = env[:items] # Add about ~50 hours cut_off = DateTime.now + Rational('2.08333') local_events = filter_items(items, cut_off) api_key = "Zrl0JRxxKJHelIn5IRubA-GZiPw" puts "Found #{local_events.length} local events" # Construct a connection to the server, which will be kept open. uri = URI(DSA_API) Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| # Get remote events from DSA. remote_events = get_server_events(http, api_key, cut_off)["page"]["entries"] .find_all { |e| e["advertise"] } .find_all { |e| e["sync_data"] } puts "Found #{local_events.length} remote events" do_sync(http, local_events, remote_events, api_key) end end private def do_sync(http, local_events, remote_events, api_key) # Contains local events we want to add. to_add = [] # Contains events we want to potentially update. to_update = [] # Calculate the identifier for each local event. # We use the map (academic year) + file name without extension. # This is necessarily unique. local_events.each { |local_event| path = Pathname(local_event[:filename]) academic_year = path.dirname.basename filename = path.basename(".*") identifier = "#{academic_year}-#{filename}" # Check if we have a remote event with the same identifier remote = remote_events.find { |e| e["sync_data"] == identifier } if remote == nil to_add.append([identifier, local_event]) else to_update.append([identifier, remote, local_event]) end remote_events.delete(remote) } puts "#{to_update.length} existing events will be updated" puts "#{to_add.length} new events will be added to remote" puts "#{remote_events.length} remote events will be deleted" # Add new events. to_add.each { |new_event| add_event(http, api_key, *new_event) } to_update.each { |existing_event| update_event(http, api_key, *existing_event) } remote_events.each { |old_event| delete(http, api_key, old_event) } end def local_to_params(local_event, identifier) puts local_event[:time].iso8601 { "activity" => { "title" => local_event[:title], "description" => local_event[:description], "location" => local_event[:location], "address" => local_event[:locationlink], "advertise" => true, "association" => "zeus", "sync_data" => identifier, "start_time" => local_event[:time].iso8601, "end_time" => local_event[:end].iso8601, "infolink" => "https://zeus.ugent.be#{local_event.path}" } } end def update_event(http, api_key, identifier, remote_event, local_event) params = local_to_params(local_event, identifier) uri = URI("#{DSA_API}/#{remote_event["id"]}") req = Net::HTTP::Put.new(uri) req['Authorization'] = api_key req['Content-Type'] = 'application/json' req.body = params.to_json response = http.request(req) case response when Net::HTTPSuccess puts "Updated event #{remote_event["id"]}" when Net::HTTPUnauthorized raise "#{response.message}: username and password set and correct?" when Net::HTTPServerError raise "#{response.message}: try again later?" else raise response.message end end def add_event(http, api_key, identifier, local_event) params = local_to_params(local_event, identifier) uri = URI(DSA_API) req = Net::HTTP::Post.new(uri) req['Authorization'] = api_key req['Content-Type'] = 'application/json' req.body = params.to_json response = http.request(req) case response when Net::HTTPSuccess puts "Added event #{identifier}" when Net::HTTPUnauthorized raise "#{response.message}: username and password set and correct?" when Net::HTTPServerError raise "#{response.message}: try again later?" else print response.body raise response.message end end def delete(http, api_key, remote) uri = URI("#{DSA_API}/#{remote["id"]}") req = Net::HTTP::Delete.new(uri) req['Authorization'] = api_key response = http.request(req) case response when Net::HTTPSuccess puts "Delete event #{remote["id"]}" when Net::HTTPUnauthorized raise "#{response.message}: username and password set and correct?" when Net::HTTPServerError raise "#{response.message}: try again later?" else raise response.message end end def filter_items(items, cut_off) # Must be in the future # Must have sync_id items.find_all('/events/*/*.md') .find_all { |event| event[:time] > cut_off } .find_all { |event| event[:exclude_from_sync] != true } end def get_server_events(http, api_key, cut_off) uri = URI(DSA_API) params = {:start_time => cut_off.iso8601, :association => "zeus"} uri.query = URI.encode_www_form(params) request = Net::HTTP::Get.new(uri) request['Authorization'] = api_key response = http.request(request) case response when Net::HTTPSuccess JSON.parse response.body when Net::HTTPUnauthorized raise "#{response.message}: username and password set and correct?" when Net::HTTPServerError raise "#{response.message}: try again later?" else raise response.message end end end runner SyncRunner