皆さんの職場では勤怠管理やってますか?
最近はベンチャーでも勤務実態の把握がどうたらと出勤退勤つけさせたがりますよね。
今の職場もTeamSpiritで管理してるんですが、あれ面倒です。とても面倒です。大事なことなんで何度も言います。
ページ開いてポチッと押すだけと言われればそうなんですが、なんか面倒です。
APIとか公開されてれば適当に自動化するんですがそういうのはないっぽいし、「パソコン開かずにTeamSpiritに打刻するのがすごいラク | ATOMS by Loftwork」ではChatter使ってますが、うちではChatterは使ってないのでこれもできない。
というわけで、Seleniumとか使ってなんとかしました。
何番煎じだよって感じですがやった内容をご紹介。
1. 全体構成
IFTTT -> Integromat -> API Gateway -> Lambda -> ECS
IFTTTで「OK,Google 仕事開始/終了」を受け取って、最終的にdocker image化したseleniumでTeamSpiritの出勤・退勤ボタンをポチってます。
2. 詳細
2.1. IFTTT
Google Assistantに対応してて、「OK, Google」から他の何か呼び出すのが簡単だったのでこいつを採用。
ここから直接AWSのAPI Gatewayに繋ぎたかったけどそれはできなかった。
API Gatewayで完全公開なAPIにしちゃうと、リンクが知られれば誰でも私の勤怠をつけれちゃうようになるのでAPI keyで制限かけるようにしたいのですが、API Gatewayでそれをやるにはカスタムヘッダ(x-api-key)でkeyを渡すようにしなくてはいけません。
で、問題はIFTTTのWebhooksのアクションではカスタムヘッダを設定できないことです。そこでそれができるIntegromatを一旦通すことにしました。
(※そのうちIFTTTでカスタムヘッダが設定できるようになったらIFTTTだけにしたい。IntegromatはFreeプランあるけど使いすぎると有料になるし。)
2.2. Integromat
IntegromatはIFTTTと同じようなサービスですが、かなり複雑なフローでサービスをつなぎ合わせることができます。途中で複数サービスにブロードキャストさせたり、条件で分岐させたり、ループ作ったりとか色々できます。(複雑にすると途端にフリープランの範囲内では難しくなりますが......)
設定はかなりめんどくさいしドキュメントもあまり親切じゃないのでとっつきにくいですが、慣れるとかなり面白いです。
今回は複雑なことをしたいわけではなくて、単にカスタムヘッダーつけてAPI Gateway呼びたいだけですのでフリープランで十分やってけます。
まずはwebhookつくります。これを作ると呼び出し用のURLが発行されて、このURLへのアクセスを契機に構築したフローを動かせます。
で、それを受けてAPI Gatewayを呼び出すようにします。
もちろんここでx-api-keyを設定します。webhookで受け取ったパラメータをここでもつかえるので、IFTTTからの呼び出しに「出勤/退勤」のパラメータを設定するようにしておいてAPI Gatewayの呼び出しに使います。
2.3. API Gateway
ここからAWSです。
API Gatewayがやることは単純にLambdaを呼び出すだけです。
API keyの設定とかは適当にググって調べてやります。
2.4. Lambda
LambdaはECSでタスクを実行させるだけです。
import boto3
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
req = json.loads(event["body"])
if "kick_type" not in req:
logger.warn("no kick_type.")
return {
"statusCode": 400,
"body": "",
}
kick_type = req["kick_type"]
if kick_type not in ("start", "end", "test"):
logger.warn("kick_type missmatch.")
return {
"statusCode": 400,
"body": "",
}
if kick_type == "start":
entrypoint = "entry"
elif kick_type == "end":
entrypoint = "exit"
else:
entrypoint = "test"
client = boto3.client('ecs')
client.run_task(
cluster="attendance-management-cluster",
taskDefinition="attendance-management-job-define",
launchType="FARGATE",
count=1,
overrides={
'containerOverrides': [
{
"name": "attendance-management",
"command": [
"python3",
"/opt/entrypoint/{}.py".format(entrypoint),
],
},
],
},
networkConfiguration={
"awsvpcConfiguration": {
"subnets": [
"subnet-xxxxxxxx",
],
"assignPublicIp": "ENABLED",
}
})
logger.info("kick {}.".format(entrypoint))
return {
"statusCode": 200,
"body": "",
}
ここでPhantomJSとかCasperJSとか使ってもいいかなと思ったんですが面倒そうだし、node.jsあまり使ったことないというのと、Fargate使ってみたいという欲求もあったのでECSにしました。
2.5. ECS
ECSではFargateを使うので常時起動させておくサービスとかはないです。
ECRにselenium同包したdocker image置いといて、それを呼び出される度に起動してます。
2.5.1. Dockerfile
seleniumが公式のdocker imageを出してるのでそれつかいます。
selenium/node-chromeがseluserってユーザー使うようになってるので、pythonやら入れる間はrootに切り替えてます。
FROM selenium/node-chrome
USER root
RUN apt update -y
RUN apt install -y python3 python3-pip
RUN pip3 install --upgrade pip
RUN pip3 install selenium
ADD entrypoint /opt/entrypoint/
USER seluser
2.5.2. 勤怠管理スクリプト
loginしてhomeに遷移して、そこにあるiframe内の出勤/退勤ボタンをポチるだけです。出勤と退勤の内容はほぼ変わりません。ボタンのidとdisable時のclass名が違うくらいです。
time.sleepがそこら中にでてくるのはご愛嬌。
出勤用
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.options import Options
import os
import time
width = os.getenv("BROWSER_WIDTH", None)
height = os.getenv("BROWSER_HEIGHT", None)
team_id = os.getenv("TEAMSPIRIT_ID", None)
user_id = os.getenv("TEAMSPIRIT_USER_ID", None)
user_pass = os.getenv("TEAMSPIRIT_USER_PASSWORD", None)
login_url = "https://{}.cloudforce.com/".format(team_id)
top_url = "https://{}.cloudforce.com/home/home.jsp".format(team_id)
options = Options()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
options.add_argument("--window-size={},{}".format(width,height))
driver = webdriver.Chrome(chrome_options=options)
try:
# login
driver.get(login_url)
driver.implicitly_wait(10)
time.sleep(10)
uid = driver.find_element_by_id("username")
password = driver.find_element_by_id("password")
uid.send_keys(user_id)
password.send_keys(user_pass)
driver.find_element_by_id("Login").click()
time.sleep(10)
# attendance
driver.get(top_url)
time.sleep(10)
iframe = driver.find_element_by_xpath("//iframe[@title='AtkWorkComponent']")
driver.switch_to_frame(iframe)
time.sleep(5)
in_button = driver.find_element_by_xpath("//div[@id='btnStInput']")
class_attr = in_button.get_attribute("class")
if "pw_btnnst_dis" in class_attr:
print("出勤不可")
else:
print("出勤")
in_button.click()
time.sleep(10)
except NoSuchElementException:
print("出勤ボタンなし")
driver.quit()
退勤用
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.options import Options
import os
import time
width = os.getenv("BROWSER_WIDTH", None)
height = os.getenv("BROWSER_HEIGHT", None)
team_id = os.getenv("TEAMSPIRIT_ID", None)
user_id = os.getenv("TEAMSPIRIT_USER_ID", None)
user_pass = os.getenv("TEAMSPIRIT_USER_PASSWORD", None)
login_url = "https://{}.cloudforce.com/".format(team_id)
top_url = "https://{}.cloudforce.com/home/home.jsp".format(team_id)
options = Options()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
options.add_argument("--window-size={},{}".format(width,height))
driver = webdriver.Chrome(chrome_options=options)
try:
# login
driver.get(login_url)
driver.implicitly_wait(10)
time.sleep(10)
uid = driver.find_element_by_id("username")
password = driver.find_element_by_id("password")
uid.send_keys(user_id)
password.send_keys(user_pass)
driver.find_element_by_id("Login").click()
time.sleep(10)
# attendance
driver.get(top_url)
time.sleep(10)
iframe = driver.find_element_by_xpath("//iframe[@title='AtkWorkComponent']")
driver.switch_to_frame(iframe)
time.sleep(5)
out_button = driver.find_element_by_xpath("//div[@id='btnEtInput']")
class_attr = out_button.get_attribute("class")
if "pw_btnnet_dis" in class_attr:
print("退勤不可")
else:
print("退勤")
out_button.click()
time.sleep(10)
except NoSuchElementException:
print("退勤ボタンなし")
driver.quit()
2.6. その他
他にAWS CodePipelineとAWS CodeBuildとか使ってます。
コードはここ
https://github.com/bokeneko/attendance-management
3. まとめ
色々使ったけどコストは1ヶ月10-20円くらいでいけそう。
それで毎朝晩teamspiritひらいてポチポチやるストレスから解放されるならよし。