# frozen_string_literal: true require 'webdrivers/chromedriver' module Kobot # The core class that launches browser, logins to KOT, reads today # record, and conducts clock in or clock out action based on config. class Engine def initialize @now = Time.now.getlocal(Config.kot_timezone_offset) @today = @now.strftime(Config.kot_date_format) @top_url = Config.kot_url end # The entrance where the whole flow starts. # # It exits early if today is weekend or marked as to skip by # the #{Config.skip} specified from command line option --skip. # # Unexpected behavior such as record appearing as holiday on # the web or failure of clock in/out action is handled within # the method by logging and/or email notifications if enabled. # # System errors or any unknown exceptions occurred if any are # to be popped up and should be handled by the outside caller. def start return unless should_run_today? launch_browser login read_today_record validate_today_record! if Config.clock == :in clock_in! else clock_out! end logout rescue KotRecordError => e Kobot.logger.warn(e.message) Mailer.send(clock_notify_message(status: e.message)) logout rescue KotClockInError => e Kobot.logger.warn e.message Mailer.send(clock_notify_message(clock: :in, status: e.message)) logout rescue KotClockOutError => e Kobot.logger.warn e.message Mailer.send(clock_notify_message(clock: :out, status: e.message)) logout rescue StandardError => e Kobot.logger.error(e.message) Kobot.logger.error(e.backtrace) Mailer.send(clock_notify_message(status: e.message)) logout ensure close_browser end private def should_run_today? if skip? Kobot.logger.warn("Today=#{@today} is skipped as per: --skip=#{Config.skip}") return false end return true unless weekend? Kobot.logger.info("[Force] should have exited: today=#{@today} is weekend") if Config.force Kobot.logger.warn("Today=#{@today} is weekend") unless Config.force Config.force end def launch_browser prefs = { profile: { default_content_settings: { geolocation: Config.browser_geolocation ? 1 : 2 } } } options = Selenium::WebDriver::Chrome::Options.new(prefs: prefs) options.headless! if Config.browser_headless caps = [ options ] @browser = Selenium::WebDriver.for(:chrome, capabilities: caps) @wait = Selenium::WebDriver::Wait.new(timeout: Config.browser_wait_timeout) Kobot.logger.info('Launch browser successful') end def close_browser return unless @browser Kobot.logger.info('Close browser') @browser.quit end def login Kobot.logger.info("Navigate to: #{@top_url}") @browser.get @top_url @wait.until { @browser.find_element(id: 'modal_window') } modal_title_element = @wait.until { @browser.find_element(css: '.modal-title') } @selector = if modal_title_element.text.downcase.include? 'password' Selector.en else Selector.ja end Kobot.logger.info "Page title: #{@browser.title}" Kobot.logger.debug do "Login with id=#{Credential.kot_id} and password=#{Credential.kot_password}" end @browser.find_element(id: 'id').send_keys Credential.kot_id @browser.find_element(id: 'password').send_keys Credential.kot_password @browser.find_element(css: 'div.btn-control-message').click Kobot.logger.info 'Login successful' @wait.until do @browser.find_element(id: 'notification_content').text.include?(@selector.login_success_notification_text) end if Config.browser_geolocation begin @wait.until do @browser.find_element(id: 'location_area').text.include?(@selector.location_area_notification_text) end rescue StandardError => e Kobot.logger.warn "Get geolocation failed: #{e.message}" end end Kobot.logger.info "Page title: #{@browser.title}" end def logout if @browser.current_url.include? 'admin' Kobot.logger.info('Logout from Time Card (タイムカード) page') @browser.find_element(css: 'div.htBlock-header_logoutButton').click else Kobot.logger.info('Logout from My Recorder (Myレコーダー) page') @wait.until { @browser.find_element(id: 'menu_icon') }.click @wait.until { @browser.find_element(link: @selector.logout_menu_link_text) }.click @browser.switch_to.alert.accept end Kobot.logger.info 'Logout successful' end def read_today_record Kobot.logger.info('Navigate to Time Card (タイムカード) page') @wait.until { @browser.find_element(id: 'menu_icon') }.click @wait.until { @browser.find_element(link: @selector.time_card_menu_link_text) }.click time_table = @wait.until { @browser.find_element(css: 'div.htBlock-adjastableTableF_inner > table') } time_table.find_elements(css: 'tbody > tr').each do |tr| date_cell = tr.find_element(css: 'td.htBlock-scrollTable_day') next unless date_cell.text.include? @today Kobot.logger.info('Reading today record') @kot_data = {} @kot_data[:today] = date_cell.text @kot_data[:today_css_class] = date_cell.attribute('class') @kot_data[:today_type] = tr.find_element(css: 'td.work_day_type').text @kot_data[:today_schedule] = tr.find_element(css: 'td.schedule').text @kot_data[:today_clock_in] = tr.find_element( css: 'td.start_end_timerecord[data-ht-sort-index="START_TIMERECORD"]' ).text @kot_data[:today_clock_out] = tr.find_element( css: 'td.start_end_timerecord[data-ht-sort-index="END_TIMERECORD"]' ).text Kobot.logger.debug do { kot_toay: @kot_data[:today], kot_today_css_class: @kot_data[:today_css_class], kot_today_type: @kot_data[:today_type], kot_today_clock_in: @kot_data[:today_clock_in], kot_today_clock_out: @kot_data[:today_clock_out] } end break end end def validate_today_record! raise KotRecordError, "Today=#{@today} is not found on kot" if @kot_data[:today].strip.empty? if kot_weekend? raise KotRecordError, "Today=#{@today} is marked as weekend: #{@kot_data[:today]}" unless Config.force Kobot.logger.info( "[Force] should have exited: today=#{@today} is marked as weekend: #{@kot_data[:today]}" ) end if kot_public_holiday? raise KotRecordError, "Today=#{@today} is marked as public holiday: #{@kot_data[:today]}" unless Config.force Kobot.logger.info( "[Force] should have exited: today=#{@today} is marked as public holiday: #{@kot_data[:today]}" ) end return unless Config.auto_skip if kot_non_work_schedule? raise KotRecordError, "Today=#{@today} is non-work schedule: #{@kot_data[:today_schedule]}" unless Config.force Kobot.logger.info( "[Force] should have exited: today=#{@today} is non-work schedule: #{@kot_data[:today_schedule]}" ) end end def clock_in! Kobot.logger.warn("Clock in during the afternoon: #{@now}") if @now.hour > 12 if @kot_data[:today_clock_in].strip.empty? click_clock_in_button return if Config.dryrun read_today_record raise KotClockInError, 'Clock in operation seems to have failed' if @kot_data[:today_clock_in].strip.empty? Kobot.logger.info("Clock in successful: #{@kot_data[:today_clock_in]}") Mailer.send(clock_notify_message(clock: :in)) else Kobot.logger.warn("Clock in done already: #{@kot_data[:today_clock_in]}") end end def clock_out! Kobot.logger.warn("Clock out during the morning: #{@now}") if @now.hour <= 12 unless Config.dryrun if @kot_data[:today_clock_in].strip.empty? raise KotClockOutError, "!!!No clock in record for today=#{@kot_data[:today]}!!!" end end if @kot_data[:today_clock_out].strip.empty? click_clock_out_button return if Config.dryrun read_today_record raise KotClockOutError, 'Clock out operation seems to have failed' if @kot_data[:today_clock_out].strip.empty? Kobot.logger.info("Clock out successful: #{@kot_data[:today_clock_out]}") Mailer.send(clock_notify_message(clock: :out)) else Kobot.logger.warn("Clock out done already: #{@kot_data[:today_clock_out]}") end end def click_clock_in_button Kobot.logger.info("Navigate to: #{@top_url}") @browser.get @top_url clock_in_button = @wait.until { @browser.find_element(css: 'div.record-clock-in') } if Config.dryrun Kobot.logger.info('[Dryrun] Clock-in button (出勤) would have been clicked') else Kobot.logger.info('Clicking the Clock-in button (出勤)') clock_in_button.click end end def click_clock_out_button Kobot.logger.info("Navigate to: #{@top_url}") @browser.get @top_url clock_out_button = @wait.until { @browser.find_element(css: 'div.record-clock-out') } if Config.dryrun Kobot.logger.info('[Dryrun] Clock-out button (退勤) would have been clicked') else Kobot.logger.info('Clicking the Clock-out button (退勤)') clock_out_button.click end end def weekend? @now.saturday? || @now.sunday? end def skip? return false unless Config.skip return false unless Config.skip.respond_to? :include? Config.skip.include? @now.strftime('%F') end def kot_weekend? [ @selector.kot_date_saturday, @selector.kot_date_sunday ].any? { |weekend| @kot_data[:today]&.include? weekend } end def kot_non_work_schedule? !@kot_data[:today_schedule]&.include?(Config.auto_skip_without) end def kot_public_holiday? return true if @kot_data[:today_type]&.include? @selector.kot_workday_time_off_text kot_today_highlighted = %w[sunday saturday].any? do |css| @kot_data[:today_css_class]&.include? css end if kot_today_highlighted Kobot.logger.warn( "Today=#{@kot_data[:today]} is highlighted (holiday) but not marked as #{@selector.kot_workday_time_off_text}" ) end kot_today_highlighted end def clock_notify_message(clock: nil, status: :success) color = status == :success ? 'green' : 'red' message = [ "Date: #{@today}", "Status: #{status}" ] message << "Clock_in: #{@kot_data[:today_clock_in]}" if clock message << "Clock_out: #{@kot_data[:today_clock_out]}" if clock == :out message.join('
') end end end