# 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 treated as holiday 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
if weekend?
if Config.force
Kobot.logger.info("[Force] should have exited: today=#{@today} is weekend")
else
Kobot.logger.info("Today=#{@today} is weekend")
return
end
end
if skip?
Kobot.logger.info("Today=#{@today} is skipped as per: --skip=#{Config.skip}")
return
end
unless %i[in out].include? Config.clock
Kobot.logger.warn("Invalid clock operation: #{Config.clock}")
return
end
launch_browser
login
read_today_record
verify_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
Kobot.logger.info('Close browser')
@browser&.quit
end
private
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
@browser = Selenium::WebDriver.for(:chrome, options: options)
@wait = Selenium::WebDriver::Wait.new(timeout: Config.browser_wait_timeout)
Kobot.logger.info('Launch browser successful')
end
def login
Kobot.logger.info("Navigate to: #{@top_url}")
@browser.get @top_url
@wait.until { @browser.find_element(id: 'modal_window') }
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 { @browser.find_element(id: 'notification_content').text.include?('データを取得しました') }
if Config.browser_geolocation
begin
@wait.until { @browser.find_element(id: 'location_area').text.include?('位置情報取得済み') }
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 タイムカード page')
@browser.find_element(css: 'div.htBlock-header_logoutButton').click
else
Kobot.logger.info('Logout from Myレコーダー page')
@wait.until { @browser.find_element(id: 'menu_icon') }.click
@wait.until { @browser.find_element(link: 'ログアウト') }.click
@browser.switch_to.alert.accept
end
Kobot.logger.info 'Logout successful'
end
def read_today_record
Kobot.logger.info('Navigate to タイムカード page')
@wait.until { @browser.find_element(id: 'menu_icon') }.click
@wait.until { @browser.find_element(link: 'タイムカード') }.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_today = date_cell.text
@kot_today_css_class = date_cell.attribute('class')
@kot_today_type = tr.find_element(css: 'td.work_day_type').text
@kot_today_clock_in = tr.find_element(
css: 'td.start_end_timerecord[data-ht-sort-index="START_TIMERECORD"]'
).text
@kot_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_today,
kot_today_css_class: @kot_today_css_class,
kot_today_type: @kot_today_type,
kot_today_clock_in: @kot_today_clock_in,
kot_today_clock_out: @kot_today_clock_out
}
end
break
end
end
def verify_today_record!
raise KotRecordError, "Today=#{@today} is not found on kot" if @kot_today.strip.empty?
if kot_weekend?
raise KotRecordError, "Today=#{@today} is marked as weekend on kot: #{@kot_today}" unless Config.force
Kobot.logger.info(
"[Force] should have exited: today=#{@today} is marked as weekend on kot: #{@kot_today}"
)
end
return unless kot_public_holiday?
raise KotRecordError, "Today=#{@today} is marked as public holiday on kot: #{@kot_today}" unless Config.force
Kobot.logger.info(
"[Force] should have exited: today=#{@today} is marked as public holiday on kot: #{@kot_today}"
)
end
def clock_in!
Kobot.logger.warn("Clock in during the afternoon: #{@now}") if @now.hour > 12
if @kot_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_today_clock_in.strip.empty?
Kobot.logger.info("Clock in successful: #{@kot_today_clock_in}")
Mailer.send(clock_notify_message(clock: :in))
else
Kobot.logger.warn("Clock in done already: #{@kot_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_today_clock_in.strip.empty?
raise KotClockOutError,
"!!!No clock in record for today=#{@kot_today}!!!"
end
end
if @kot_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_today_clock_out.strip.empty?
Kobot.logger.info("Clock out successful: #{@kot_today_clock_out}")
Mailer.send(clock_notify_message(clock: :out))
else
Kobot.logger.warn("Clock out done already: #{@kot_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 in 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?
%w[土 日].any? { |kanji| @kot_today&.include? kanji }
end
def kot_public_holiday?
return true if @kot_today_type&.include? '休日'
kot_today_highlighted = %w[sunday saturday].any? do |css|
@kot_today_css_class&.include? css
end
if kot_today_highlighted
Kobot.logger.warn(
"Today=#{@kot_today} is highlighted (holiday) but not marked as 休日"
)
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_today_clock_in}" if clock
message << "Clock_out: #{@kot_today_clock_out}" if clock == :out
message.join('
')
end
end
end