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": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1",
|
"argparse": "^2.0.1",
|
||||||
"axios": "^1.3.3",
|
"axios": "^1.3.3",
|
||||||
@ -11,5 +14,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/main.js"
|
"start": "node src/main.js"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"aggietimed": "cli.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@ import * as aggietime from "./aggietime.js";
|
|||||||
|
|
||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
"clock-in": aggietime.clock_in,
|
"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) => {
|
export const do_action = async (body) => {
|
||||||
|
116
src/aggietime.js
116
src/aggietime.js
@ -4,12 +4,19 @@ import {
|
|||||||
USER_PATH,
|
USER_PATH,
|
||||||
USER_CACHE_EXP_SEC,
|
USER_CACHE_EXP_SEC,
|
||||||
CLOCKIN_PATH,
|
CLOCKIN_PATH,
|
||||||
|
CLOCKOUT_PATH,
|
||||||
|
OPEN_SHIFT_PATH,
|
||||||
|
OPEN_SHIFT_EXP_SEC,
|
||||||
} from "./constants.js";
|
} from "./constants.js";
|
||||||
|
|
||||||
import { client } from "./axios_client.js";
|
import { client } from "./axios_client.js";
|
||||||
|
|
||||||
import expireCache from "expire-cache";
|
import expireCache from "expire-cache";
|
||||||
|
|
||||||
|
const aggietime = client.create({
|
||||||
|
baseURL: AGGIETIME_URI,
|
||||||
|
});
|
||||||
|
|
||||||
const replace_path_args = (path, map) =>
|
const replace_path_args = (path, map) =>
|
||||||
path.replaceAll(/:([a-zA-Z0-9_]+)/g, (_, key) => map[key]);
|
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 () => {
|
export const get_user_info = async () => {
|
||||||
if (!expireCache.get("user")) {
|
if (!expireCache.get("user")) {
|
||||||
const user = await client
|
const user = await aggietime.get(USER_PATH).then(({ data, config }) => {
|
||||||
.get(`${AGGIETIME_URI}/${USER_PATH}`)
|
const csrf_token = config.jar
|
||||||
.then(({ data, config }) => {
|
.toJSON()
|
||||||
const csrf_token = config.jar
|
.cookies.find(
|
||||||
.toJSON()
|
({ domain, key }) =>
|
||||||
.cookies.find(
|
domain === AGGIETIME_DOMAIN && key === "XSRF-TOKEN"
|
||||||
({ domain, key }) =>
|
).value;
|
||||||
domain === AGGIETIME_DOMAIN && key === "XSRF-TOKEN"
|
expireCache.set("aggietime-csrf", csrf_token);
|
||||||
).value;
|
return data;
|
||||||
expireCache.set("aggietime-csrf", csrf_token);
|
});
|
||||||
return data;
|
|
||||||
});
|
|
||||||
|
|
||||||
expireCache.set("user", user, USER_CACHE_EXP_SEC);
|
expireCache.set("user", user, USER_CACHE_EXP_SEC);
|
||||||
}
|
}
|
||||||
@ -48,15 +53,82 @@ export const get_user_info = async () => {
|
|||||||
export const clock_in = async ({ position } = {}) => {
|
export const clock_in = async ({ position } = {}) => {
|
||||||
position = await get_user_position_or_specified(position);
|
position = await get_user_position_or_specified(position);
|
||||||
|
|
||||||
return await client.post(
|
return await aggietime
|
||||||
`${AGGIETIME_URI}/${replace_path_args(CLOCKIN_PATH, { position })}`,
|
.post(
|
||||||
{
|
replace_path_args(CLOCKIN_PATH, { position }),
|
||||||
comment: "",
|
{
|
||||||
},
|
comment: "",
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"X-XSRF-TOKEN": expireCache.get("aggietime-csrf"),
|
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
);
|
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_DOMAIN = "aggietimeultra.usu.edu";
|
||||||
export const AGGIETIME_URI = `https://${AGGIETIME_DOMAIN}`;
|
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 LOGIN_PATH = "api/v1/auth/login";
|
||||||
export const LOGOUT_PATH = "api/v1/auth/logout";
|
export const LOGOUT_PATH = "api/v1/auth/logout";
|
||||||
export const CLOCKIN_PATH = "api/v1/positions/:position/clock_in";
|
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 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 EXECUTION_SELECTOR = "input[type=hidden][name=execution]";
|
||||||
export const DUO_IFRAME_SELECTOR = "#duo_iframe";
|
export const DUO_IFRAME_SELECTOR = "#duo_iframe";
|
||||||
|
42
src/main.js
42
src/main.js
@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SOCKET_PATH,
|
DEFAULT_SOCKET_PATH,
|
||||||
KILL_SIGNALS,
|
KILL_SIGNALS,
|
||||||
@ -10,7 +12,7 @@ import * as net from "net";
|
|||||||
import * as dotenv from "dotenv";
|
import * as dotenv from "dotenv";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
|
||||||
const main = async () => {
|
export default async () => {
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const args = build_args();
|
const args = build_args();
|
||||||
|
|
||||||
@ -20,16 +22,35 @@ const main = async () => {
|
|||||||
} catch {
|
} catch {
|
||||||
fs.unlinkSync(args.socket_path);
|
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 build_args = () => {
|
||||||
const parser = new argparse.ArgumentParser({ description: "AggieTime CLI" });
|
const parser = new argparse.ArgumentParser({ description: "AggieTime CLI" });
|
||||||
|
|
||||||
parser.add_argument("-d", "--daemon", {
|
parser.add_argument("-d", "--daemon", {
|
||||||
help: "Start server as a process blocking daemon",
|
help: "Start server as a process blocking daemon",
|
||||||
action: argparse.BooleanOptionalAction,
|
action: argparse.BooleanOptionalAction,
|
||||||
default: true,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
parser.add_argument("-s", "--socket_path", {
|
parser.add_argument("-s", "--socket_path", {
|
||||||
@ -37,6 +58,10 @@ const build_args = () => {
|
|||||||
help: `Set server socket path, defaults to ${DEFAULT_SOCKET_PATH}`,
|
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();
|
return parser.parse_args();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,7 +101,16 @@ specify another socket path with --socket_path`
|
|||||||
return;
|
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))
|
process.on(signal, () => kill_server(unix_server, socket_path))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
|
||||||
|
Loading…
Reference in New Issue
Block a user