Add flag to connect over socket and retrieve action response, simple systemd service, and more actions
This commit is contained in:
parent
32803c4416
commit
312af9bc10
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
A_NUMBER=A12345671
|
||||
PASSWORD=
|
11
aggietimed.service
Normal file
11
aggietimed.service
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=aggietimed
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/home/lizzy/work/simple_scripts/aggietime_cli/.env
|
||||
ExecStart=/usr/bin/aggietimed -d -s /tmp/aggietimed.sock
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
4
cli.js
Executable file
4
cli.js
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import main from './src/main.js';
|
||||
main();
|
@ -1,4 +1,7 @@
|
||||
{
|
||||
"name": "aggietimed",
|
||||
"version": "0.0.1",
|
||||
"author": "Lizzy Hunt (@simponic)",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"axios": "^1.3.3",
|
||||
@ -11,5 +14,8 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/main.js"
|
||||
},
|
||||
"bin": {
|
||||
"aggietimed": "cli.js"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ import * as aggietime from "./aggietime.js";
|
||||
|
||||
const ACTIONS = {
|
||||
"clock-in": aggietime.clock_in,
|
||||
"clock-out": aggietime.clock_out,
|
||||
"current-shift": aggietime.current_shift,
|
||||
"current-user": aggietime.get_user_info,
|
||||
"status-line": aggietime.get_status_line,
|
||||
};
|
||||
|
||||
export const do_action = async (body) => {
|
||||
|
116
src/aggietime.js
116
src/aggietime.js
@ -4,12 +4,19 @@ import {
|
||||
USER_PATH,
|
||||
USER_CACHE_EXP_SEC,
|
||||
CLOCKIN_PATH,
|
||||
CLOCKOUT_PATH,
|
||||
OPEN_SHIFT_PATH,
|
||||
OPEN_SHIFT_EXP_SEC,
|
||||
} from "./constants.js";
|
||||
|
||||
import { client } from "./axios_client.js";
|
||||
|
||||
import expireCache from "expire-cache";
|
||||
|
||||
const aggietime = client.create({
|
||||
baseURL: AGGIETIME_URI,
|
||||
});
|
||||
|
||||
const replace_path_args = (path, map) =>
|
||||
path.replaceAll(/:([a-zA-Z0-9_]+)/g, (_, key) => map[key]);
|
||||
|
||||
@ -27,18 +34,16 @@ const get_user_position_or_specified = async (position) => {
|
||||
|
||||
export const get_user_info = async () => {
|
||||
if (!expireCache.get("user")) {
|
||||
const user = await client
|
||||
.get(`${AGGIETIME_URI}/${USER_PATH}`)
|
||||
.then(({ data, config }) => {
|
||||
const csrf_token = config.jar
|
||||
.toJSON()
|
||||
.cookies.find(
|
||||
({ domain, key }) =>
|
||||
domain === AGGIETIME_DOMAIN && key === "XSRF-TOKEN"
|
||||
).value;
|
||||
expireCache.set("aggietime-csrf", csrf_token);
|
||||
return data;
|
||||
});
|
||||
const user = await aggietime.get(USER_PATH).then(({ data, config }) => {
|
||||
const csrf_token = config.jar
|
||||
.toJSON()
|
||||
.cookies.find(
|
||||
({ domain, key }) =>
|
||||
domain === AGGIETIME_DOMAIN && key === "XSRF-TOKEN"
|
||||
).value;
|
||||
expireCache.set("aggietime-csrf", csrf_token);
|
||||
return data;
|
||||
});
|
||||
|
||||
expireCache.set("user", user, USER_CACHE_EXP_SEC);
|
||||
}
|
||||
@ -48,15 +53,82 @@ export const get_user_info = async () => {
|
||||
export const clock_in = async ({ position } = {}) => {
|
||||
position = await get_user_position_or_specified(position);
|
||||
|
||||
return await client.post(
|
||||
`${AGGIETIME_URI}/${replace_path_args(CLOCKIN_PATH, { position })}`,
|
||||
{
|
||||
comment: "",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"X-XSRF-TOKEN": expireCache.get("aggietime-csrf"),
|
||||
return await aggietime
|
||||
.post(
|
||||
replace_path_args(CLOCKIN_PATH, { position }),
|
||||
{
|
||||
comment: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
{
|
||||
headers: {
|
||||
"X-XSRF-TOKEN": expireCache.get("aggietime-csrf"),
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(({ data }) => {
|
||||
expireCache.remove("status_line");
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
export const clock_out = async ({ position } = {}) => {
|
||||
position = await get_user_position_or_specified(position);
|
||||
|
||||
return await aggietime
|
||||
.post(
|
||||
replace_path_args(CLOCKOUT_PATH, { position }),
|
||||
{
|
||||
comment: "",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"X-XSRF-TOKEN": expireCache.get("aggietime-csrf"),
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(({ data }) => {
|
||||
expireCache.remove("status_line");
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
export const current_shift = async () => {
|
||||
const req_path = replace_path_args(OPEN_SHIFT_PATH, await get_user_info());
|
||||
const {
|
||||
request: {
|
||||
res: { responseUrl: response_url },
|
||||
},
|
||||
data,
|
||||
} = await aggietime.get(req_path);
|
||||
|
||||
if (`${AGGIETIME_URI}/${req_path}` != response_url) {
|
||||
// IMO a very weird decision - when there is no open shift the api redirects
|
||||
// home instead of sending back a 404 or something actually *reasonable* :3
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const get_status_line = async () => {
|
||||
if (!expireCache.get("status_line")) {
|
||||
const { anumber } = await get_user_info();
|
||||
const shift = await current_shift();
|
||||
let status_line = "No Shift";
|
||||
|
||||
if (shift && shift?.start) {
|
||||
const start = new Date(shift?.start);
|
||||
status_line =
|
||||
((new Date().getTime() - start.getTime()) / (1000 * 60 * 60)).toFixed(
|
||||
2
|
||||
) + " hours";
|
||||
}
|
||||
|
||||
expireCache.set(
|
||||
"status_line",
|
||||
`${anumber} - ${status_line}`,
|
||||
OPEN_SHIFT_EXP_SEC
|
||||
);
|
||||
}
|
||||
return { status: expireCache.get("status_line") };
|
||||
};
|
||||
|
@ -3,11 +3,14 @@ export const KILL_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
||||
|
||||
export const AGGIETIME_DOMAIN = "aggietimeultra.usu.edu";
|
||||
export const AGGIETIME_URI = `https://${AGGIETIME_DOMAIN}`;
|
||||
export const REFRESH_JWT_MS = 5 * 1000 * 60;
|
||||
export const LOGIN_PATH = "api/v1/auth/login";
|
||||
export const LOGOUT_PATH = "api/v1/auth/logout";
|
||||
export const CLOCKIN_PATH = "api/v1/positions/:position/clock_in";
|
||||
export const CLOCKOUT_PATH = "api/v1/positions/:position/clock_out";
|
||||
export const USER_PATH = "api/v1/auth/get_user_info";
|
||||
export const REFRESH_JWT_MS = 5 * 1000 * 60;
|
||||
export const OPEN_SHIFT_PATH = "api/v1/users/:anumber/open_shift";
|
||||
export const OPEN_SHIFT_EXP_SEC = 60;
|
||||
|
||||
export const EXECUTION_SELECTOR = "input[type=hidden][name=execution]";
|
||||
export const DUO_IFRAME_SELECTOR = "#duo_iframe";
|
||||
|
42
src/main.js
42
src/main.js
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
DEFAULT_SOCKET_PATH,
|
||||
KILL_SIGNALS,
|
||||
@ -10,7 +12,7 @@ import * as net from "net";
|
||||
import * as dotenv from "dotenv";
|
||||
import * as fs from "fs";
|
||||
|
||||
const main = async () => {
|
||||
export default async () => {
|
||||
dotenv.config();
|
||||
const args = build_args();
|
||||
|
||||
@ -20,16 +22,35 @@ const main = async () => {
|
||||
} catch {
|
||||
fs.unlinkSync(args.socket_path);
|
||||
}
|
||||
} else if (args.action) {
|
||||
if (fs.existsSync(args.socket_path)) {
|
||||
run_action(args.socket_path, args.action);
|
||||
} else {
|
||||
console.error(`ERR: No such socket '${args.socket_path}'`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const run_action = (socket_path, action) => {
|
||||
const connection = net.connect(socket_path);
|
||||
connection.on("data", (data) => {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
console.log(data.toString().trim());
|
||||
} else {
|
||||
console.log(data.trim());
|
||||
}
|
||||
connection.end();
|
||||
});
|
||||
connection.write(JSON.stringify({ action }));
|
||||
};
|
||||
|
||||
const build_args = () => {
|
||||
const parser = new argparse.ArgumentParser({ description: "AggieTime CLI" });
|
||||
|
||||
parser.add_argument("-d", "--daemon", {
|
||||
help: "Start server as a process blocking daemon",
|
||||
action: argparse.BooleanOptionalAction,
|
||||
default: true,
|
||||
default: false,
|
||||
});
|
||||
|
||||
parser.add_argument("-s", "--socket_path", {
|
||||
@ -37,6 +58,10 @@ const build_args = () => {
|
||||
help: `Set server socket path, defaults to ${DEFAULT_SOCKET_PATH}`,
|
||||
});
|
||||
|
||||
parser.add_argument("-a", "--action", {
|
||||
help: `Ignored when daemon flag is set. Returns the value of action (see actions.js) when sent over the socket.`,
|
||||
});
|
||||
|
||||
return parser.parse_args();
|
||||
};
|
||||
|
||||
@ -76,7 +101,16 @@ specify another socket path with --socket_path`
|
||||
return;
|
||||
}
|
||||
|
||||
actions.do_action(body);
|
||||
actions
|
||||
.do_action(body)
|
||||
.then((resp) => {
|
||||
client.write(JSON.stringify(resp) + "\r\n");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
|
||||
client.write(JSON.stringify({ err: true }) + "\r\n");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -90,5 +124,3 @@ specify another socket path with --socket_path`
|
||||
process.on(signal, () => kill_server(unix_server, socket_path))
|
||||
);
|
||||
};
|
||||
|
||||
main();
|
||||
|
Loading…
Reference in New Issue
Block a user