├── .gitignore ├── src ├── config.py ├── jandi.py ├── get_element.py ├── clock_in.py └── clock_out.py ├── config_example.ini ├── requirements.txt ├── README.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | config.ini 3 | geckodriver.log 4 | __pycache__ -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from configparser import RawConfigParser 3 | 4 | 5 | # 取得設定檔的參數 6 | def get_config() -> RawConfigParser: 7 | current_directory_path = os.path.dirname(__file__) 8 | config_file_path = os.path.join(current_directory_path, '../config.ini') 9 | config = RawConfigParser() 10 | config.read(config_file_path) 11 | 12 | return config 13 | -------------------------------------------------------------------------------- /config_example.ini: -------------------------------------------------------------------------------- 1 | [LOCATION] 2 | lat = 3 | lng = 4 | 5 | [WEBSITE] 6 | login_url = https://cloud.nueip.com/login/AccuHit 7 | target_url = https://cloud.nueip.com/home 8 | username = 9 | password = 10 | 11 | [CLOCK_IN] 12 | id = 13 | xpath = 14 | selector = 15 | hour = 16 | minute = 17 | 18 | [CLOCK_OUT] 19 | id = 20 | xpath = 21 | selector = 22 | hour = 23 | minute = 24 | 25 | [JANDI] 26 | api_url = -------------------------------------------------------------------------------- /src/jandi.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from src.config import get_config 4 | 5 | # 取得設定檔的參數 6 | config = get_config() 7 | 8 | def send_message(message: str): 9 | 10 | headers = { 11 | 'Accept': 'application/vnd.tosslab.jandi-v2+json', 12 | 'Content-Type': 'application/json', 13 | } 14 | 15 | requests.post( 16 | config['JANDI']['api_url'], 17 | json={'body': message}, 18 | headers=headers 19 | ) 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | async-generator==1.10 2 | attrs==21.2.0 3 | autopep8==1.6.0 4 | certifi==2021.10.8 5 | cffi==1.15.0 6 | charset-normalizer==2.0.9 7 | cryptography==36.0.0 8 | h11==0.12.0 9 | idna==3.3 10 | outcome==1.1.0 11 | pycodestyle==2.8.0 12 | pycparser==2.21 13 | pyOpenSSL==21.0.0 14 | requests==2.27.1 15 | selenium==4.1.0 16 | six==1.16.0 17 | sniffio==1.2.0 18 | sortedcontainers==2.4.0 19 | soupsieve==2.3.1 20 | toml==0.10.2 21 | trio==0.19.0 22 | trio-websocket==0.9.2 23 | urllib3==1.26.7 24 | wsproto==1.0.0 25 | -------------------------------------------------------------------------------- /src/get_element.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from selenium.common.exceptions import NoSuchElementException 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.firefox.webdriver import WebDriver 6 | 7 | 8 | # 使用 selector 取得元素 9 | def get_element_by_selector(browser: WebDriver, selector: str): 10 | try: 11 | element = browser.find_element(By.CSS_SELECTOR, selector) 12 | except NoSuchElementException: 13 | browser.close() 14 | sys.exit('No element') 15 | 16 | return element 17 | 18 | 19 | # 使用完整 xpath 取得元素 20 | def get_element_by_xpath(browser: WebDriver, xpath: str): 21 | try: 22 | element = browser.find_element(By.XPATH, xpath) 23 | except NoSuchElementException: 24 | browser.close() 25 | sys.exit('No element') 26 | 27 | return element 28 | 29 | 30 | # 使用 id 取得元素 31 | def get_element_by_id(browser: WebDriver, id: str): 32 | try: 33 | element = browser.find_element(By.ID, id) 34 | except NoSuchElementException: 35 | browser.close() 36 | sys.exit('No element') 37 | 38 | return element 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Auto clock in & clock out 2 | 3 | This is a simple project to avoid forgetful person (like me) to forget about clock in. 4 | 5 | > It's only for [NUEiP](https://cloud.nueip.com/). 6 | 7 | ## Requirements 8 | 9 | - [Python 3](https://www.python.org/) 10 | - [Firefox](https://www.mozilla.org/zh-TW/firefox/new/) 11 | - [Gecko driver](https://github.com/mozilla/geckodriver) 12 | 13 | ## How to use 14 | 15 | Move to the clock in project and install the packages. 16 | 17 | ```shell 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | Create the config file and **fill in the setting**. 22 | you could use JANDI to notice the clock in status, referring to this [post](https://blog.jandi.com/tw/jandi-webhooks-incoming/) to create a JANDI bot. 23 | 24 | ```shell 25 | cp config_example.ini config.ini 26 | ``` 27 | 28 | Execute the clock in. 29 | 30 | ```shell 31 | python setup.py 32 | ``` 33 | 34 | You could use crontab to schedule the clock in task. 35 | Edit the crontab setting. 36 | 37 | ```shell 38 | crontab -e 39 | ``` 40 | 41 | Add this line in the crontab file. 42 | According your working hours, you could adjust the crontab setting. 43 | 44 | ```plaintext 45 | 31-40 9,18 * * 1-5 python auto_clock_in_file_path/setup.py >> /Users/username/clock_in.log 46 | ``` 47 | 48 | > If needed, you probably need to add environment variables to your crontab. 49 | -------------------------------------------------------------------------------- /src/clock_in.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | from selenium.webdriver.common.action_chains import ActionChains 5 | from selenium.webdriver.firefox.webdriver import WebDriver 6 | 7 | from src.config import get_config 8 | from src.get_element import * 9 | from src.jandi import send_message 10 | 11 | # 取得設定檔的參數 12 | config = get_config() 13 | 14 | 15 | # 進行打上班卡作業 16 | def clock_in(browser: WebDriver): 17 | current_date = datetime.today().strftime("%Y-%m-%d") 18 | current_time = datetime.today().strftime('%H:%M:%S') 19 | 20 | click_the_clock_in_button(browser) 21 | 22 | if is_clock_in(browser): 23 | message = f'🗓 {current_date}\n🕘 {current_time}\n💼 clock in\n✅ success' 24 | print(message) 25 | send_message(message) 26 | else: 27 | message = f'🗓 {current_date}\n🕘 {current_time}\n💼 clock in\n❌ failed' 28 | print(message) 29 | send_message(message) 30 | 31 | 32 | # 檢查是否已經打上班卡 33 | def is_clock_in(browser: WebDriver) -> bool: 34 | 35 | retry = 0 36 | # 必須使用雙引號,否則即使 class name 存在也會回傳 False 37 | while "clock_btn2" not in get_element_by_selector(browser, config['CLOCK_IN']['selector']).get_attribute('class'): 38 | 39 | retry += 1 40 | if retry > 3: 41 | return False 42 | 43 | time.sleep(3) 44 | 45 | return True 46 | 47 | 48 | # 點擊打上班卡的按鈕 49 | def click_the_clock_in_button(browser: WebDriver): 50 | element = get_element_by_selector(browser, config['CLOCK_IN']['selector']) 51 | 52 | actions = ActionChains(browser) 53 | actions.move_to_element(element) 54 | actions.click() 55 | actions.perform() 56 | -------------------------------------------------------------------------------- /src/clock_out.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | 4 | from selenium.webdriver.common.action_chains import ActionChains 5 | from selenium.webdriver.firefox.webdriver import WebDriver 6 | 7 | from src.config import get_config 8 | from src.get_element import * 9 | from src.jandi import send_message 10 | 11 | # 取得設定檔的參數 12 | config = get_config() 13 | 14 | 15 | # 進行打下班卡作業 16 | def clock_out(browser: WebDriver): 17 | current_date = datetime.today().strftime('%Y-%m-%d') 18 | current_time = datetime.today().strftime('%H:%M:%S') 19 | 20 | click_the_clock_out_button(browser) 21 | 22 | if is_clock_out(browser): 23 | message = f'🗓 {current_date}\n🕕 {current_time}\n🏠 clock out\n✅ success.' 24 | print(message) 25 | send_message(message) 26 | else: 27 | message = f'🗓 {current_date}\n🕕 {current_time}\n🏠 clock out\n❌ failed.' 28 | print(message) 29 | send_message(message) 30 | 31 | 32 | # 檢查是否已經打下班卡 33 | def is_clock_out(browser: WebDriver) -> bool: 34 | 35 | retry = 0 36 | # 必須使用雙引號,否則即使 class name 存在也會回傳 False 37 | while "clock_btn2" not in get_element_by_selector(browser, config['CLOCK_OUT']['selector']).get_attribute('class'): 38 | 39 | retry += 1 40 | if retry > 3: 41 | return False 42 | 43 | time.sleep(3) 44 | 45 | return True 46 | 47 | 48 | # 點擊打下班卡的按鈕 49 | def click_the_clock_out_button(browser: WebDriver): 50 | element = get_element_by_selector(browser, config['CLOCK_OUT']['selector']) 51 | 52 | actions = ActionChains(browser) 53 | actions.move_to_element(element) 54 | actions.click() 55 | actions.perform() 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | from selenium import webdriver 5 | from selenium.webdriver.common.action_chains import ActionChains 6 | from selenium.webdriver.firefox.options import Options 7 | from selenium.webdriver.firefox.service import Service 8 | from selenium.webdriver.firefox.webdriver import WebDriver 9 | 10 | from src.clock_in import * 11 | from src.clock_out import * 12 | from src.config import get_config 13 | from src.get_element import * 14 | 15 | # 取得設定檔的參數 16 | config = get_config() 17 | 18 | # 取得上班時間 19 | clock_in_time = datetime.now().replace( 20 | hour=int(config['CLOCK_IN']['hour']), 21 | minute=int(config['CLOCK_IN']['minute'])) 22 | 23 | # 取得下班時間 24 | clock_out_time = datetime.now().replace( 25 | hour=int(config['CLOCK_OUT']['hour']), 26 | minute=int(config['CLOCK_OUT']['minute'])) 27 | 28 | 29 | # 登入 30 | def login(browser: WebDriver): 31 | browser.get(config['WEBSITE']['login_url']) 32 | 33 | browser.implicitly_wait(10) 34 | 35 | get_element_by_id(browser, 'username_input').send_keys( 36 | config['WEBSITE']['username']) 37 | 38 | get_element_by_id(browser, 'password-input').send_keys( 39 | config['WEBSITE']['password']) 40 | 41 | get_element_by_id(browser, 'login-button').click() 42 | 43 | 44 | # 根據時間打上下班的卡 45 | def clock_in_or_clock_out(browser: WebDriver): 46 | browser.get(config['WEBSITE']['target_url']) 47 | 48 | browser.implicitly_wait(10) 49 | 50 | if datetime.now() < clock_in_time: 51 | if not is_clock_in(browser): 52 | clock_in(browser) 53 | 54 | return 55 | 56 | if datetime.now() > clock_in_time and datetime.now() < clock_out_time: 57 | if not is_clock_in(browser): 58 | clock_in(browser) 59 | 60 | return 61 | 62 | if datetime.now() > clock_out_time: 63 | if not is_clock_out(browser): 64 | clock_out(browser) 65 | 66 | return 67 | 68 | 69 | def main(): 70 | options = Options() 71 | service = Service(log_path=os.path.devnull) 72 | options.headless = True 73 | 74 | # 偽造公司的地理坐標 75 | options.set_preference( 76 | 'geo.wifi.uri', 77 | f'data:application/json,{{"location": {{"lat": {config["LOCATION"]["lat"]}, "lng":{config["LOCATION"]["lng"]}}}, "accuracy": 20.0}}') 78 | options.set_preference("geo.prompt.testing", True) 79 | 80 | browser = webdriver.Firefox( 81 | options=options, service=service) 82 | 83 | login(browser) 84 | clock_in_or_clock_out(browser) 85 | 86 | browser.close() 87 | 88 | 89 | if __name__ == '__main__': 90 | main() 91 | --------------------------------------------------------------------------------