Skip to content
This repository has been archived by the owner on Nov 6, 2024. It is now read-only.

Commit

Permalink
Merge pull request #49 from AnshGupta01/ASoC
Browse files Browse the repository at this point in the history
  • Loading branch information
ditsuke authored Oct 11, 2023
2 parents a7a4cc3 + 9e95885 commit b524707
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 31 deletions.
90 changes: 66 additions & 24 deletions src/states/render-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ const renderRelativeDate = (d: number): string => {
const relativeDate = new Date(
dateToIST(new Date()).getTime() + d * DAY_TO_MINUTE * MINUTE_TO_MS
);
return `${relativeDate.getFullYear()}-${relativeDate.getMonth() + 1
}-${relativeDate.getDate()}`;
return `${relativeDate.getFullYear()}-${
relativeDate.getMonth() + 1
}-${relativeDate.getDate()}`;
};

const toFormattedPercent = (total: number, went: number) =>
Expand All @@ -46,13 +47,15 @@ export const renderAttendance = (attendance: V1AttendanceRecords) => {
let text = "";
for (let i = 0; i < attendance.records.length; i += 1) {
const record = attendance.records[i];
text += `*Course*: ${record.course?.name ?? "<Unknown>"} *| Code*: ${record?.course?.code || "<Unknown>"
}
=> ${record?.attendance?.attended}/${record?.attendance?.held
} (${toFormattedPercent(
record?.attendance?.held ?? 0,
record?.attendance?.attended ?? 1
)}%)
text += `*Course*: ${record.course?.name ?? "<Unknown>"} *| Code*: ${
record?.course?.code || "<Unknown>"
}
=> ${record?.attendance?.attended}/${
record?.attendance?.held
} (${toFormattedPercent(
record?.attendance?.held ?? 0,
record?.attendance?.attended ?? 1
)}%)
`;
}
Expand All @@ -70,11 +73,12 @@ export const renderCourses = (courses: V1Courses) => {
const { type } = course;
const code = course.ref?.code;
const name = course.ref?.name;
const attendance = `${course?.attendance?.attended}/${course?.attendance?.held
} (${toFormattedPercent(
course?.attendance?.held ?? 0,
course?.attendance?.attended ?? 1
)}%)`;
const attendance = `${course?.attendance?.attended}/${
course?.attendance?.held
} (${toFormattedPercent(
course?.attendance?.held ?? 0,
course?.attendance?.attended ?? 1
)}%)`;
const internalMarks = `${course?.internalMarks?.have}/${course?.internalMarks?.max}`;
text += `
*Course*: ${name} *| Code*: ${code}
Expand Down Expand Up @@ -176,7 +180,7 @@ export const renderAmizoneMenu = () => ({
{
id: "3",
title: "Courses",
description: "(and internals)"
description: "(and internals)",
},
{
id: "4",
Expand All @@ -198,6 +202,35 @@ export const renderAmizoneMenu = () => ({
},
});

export const renderQuickAttendanceButtons = () => ({
type: "button",
header: {
type: "text",
text: "Attendance",
},
body: {
text: "Quick Attendance Checkout",
},
action: {
buttons: [
{
type: "reply",
reply: {
id: "yesterday_attendance",
title: "Yesterday's", // Check schedule -> 28
},
},
{
type: "reply",
reply: {
id: "today_attendance",
title: "Today's",
},
},
],
},
});

export const renderClassScheduleDateList = () => {
const dates = new Array(5);
for (let i = 0; i < 5; i += 1) {
Expand Down Expand Up @@ -230,19 +263,28 @@ export const renderClassScheduleDateList = () => {
};

export const renderExamSchedule = (schedule: V1ExaminationSchedule) => {
const exams = schedule?.exams?.map((exam) => {
const { mode, time: serialTime, course, location } = exam;
// HACK: In general, we should treat the incoming times as UTC and interpret them timezone-agnostically.
// However at the moment I don't feel like dealing with timezones, so I'm just going to subtract 5:30 hours from the time.
const time = serialTime ? dateToIST(new Date(Date.parse(serialTime) - OFFSET_IST * MINUTE_TO_MS)) : undefined;
return `*👉* ${course?.code} ${course?.name}
*⏲️:* ${time ? time.toLocaleString() : "N/A"} (Mode: ${mode})` + (location ? `\n*📍* ${location}` : "");
}).join("\n\n");
const exams = schedule?.exams
?.map((exam) => {
const { mode, time: serialTime, course, location } = exam;
// HACK: In general, we should treat the incoming times as UTC and interpret them timezone-agnostically.
// However at the moment I don't feel like dealing with timezones, so I'm just going to subtract 5:30 hours from the time.
const time = serialTime
? dateToIST(
new Date(Date.parse(serialTime) - OFFSET_IST * MINUTE_TO_MS)
)
: undefined;
return (
`*👉* ${course?.code} ${course?.name}
*⏲️:* ${time ? time.toLocaleString() : "N/A"} (Mode: ${mode})` +
(location ? `\n*📍* ${location}` : "")
);
})
.join("\n\n");

return `*${schedule.title}*
${exams}`;
}
};

export const renderFacultyFeedbackInstructions =
() => `This method will submit feedback for *all* your faculty in a single step.
Expand Down
123 changes: 116 additions & 7 deletions src/states/state-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BotHandlerContext, User, states } from "./states.js";
import { BotHandlerContext, states, User } from "./states.js";
import {
renderAmizoneMenu,
renderQuickAttendanceButtons,
renderAttendance,
renderCourses,
renderSchedule,
Expand All @@ -14,6 +15,15 @@ import {
} from "./render-messages.js";
import { firstNonEmpty, newAmizoneClient } from "../utils.js";

// === Utilities ===
const OFFSET_IST = 330;
const MINUTE_TO_MS = 60_000;
const DAY_TO_MINUTE = 24 * 60;
const currentTzOffset = new Date().getTimezoneOffset();

const dateToIST = (date: Date): Date =>
new Date(date.getTime() + (OFFSET_IST - currentTzOffset) * MINUTE_TO_MS);

const validateAmizoneCredentials = async (
username: string,
password: string
Expand Down Expand Up @@ -79,6 +89,10 @@ export const handleExpectPassword = async (
if (credentialsAreValid) {
updatedUser.amizoneCredentials.password = password;
updatedUser.state = states.LOGGED_IN;
await ctx.bot.sendInteractiveMessage(
payload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(payload.sender, renderAmizoneMenu());
return updatedUser;
}
Expand Down Expand Up @@ -164,14 +178,17 @@ const amizoneMenuHandlersMap: Map<string, StateHandlerFunction> = new Map([
async (ctx): StateHandlerFunctionOut => {
try {
const amizoneClient = newAmizoneClient(ctx.user.amizoneCredentials);
const examSchedule = await amizoneClient.amizoneServiceGetExamSchedule();
return { success: true, message: renderExamSchedule(examSchedule.data) };
}
catch (err) {
const examSchedule =
await amizoneClient.amizoneServiceGetExamSchedule();
return {
success: true,
message: renderExamSchedule(examSchedule.data),
};
} catch (err) {
return { success: false, message: "" };
}
}
]
},
],
]);

/**
Expand All @@ -191,6 +208,13 @@ export const handleLoggedIn = async (ctx: BotHandlerContext): Promise<User> => {
updatedUser.state = states.NEW_USER;
await ctx.bot.sendMessage(payload.sender, "Logged Out!");
return updatedUser;
} else if (
payload.interactive.title === "Yesterday's" ||
payload.interactive.title === "Today's"
) {
// Handle attendance button click
await handleReplyAttendanceButton(ctx);
return updatedUser;
}

const messageHandler = amizoneMenuHandlersMap.get(inputMessage);
Expand All @@ -201,6 +225,10 @@ export const handleLoggedIn = async (ctx: BotHandlerContext): Promise<User> => {
payload.sender,
"Invalid option selected. Try again?"
);
await ctx.bot.sendInteractiveMessage(
payload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(payload.sender, renderAmizoneMenu());
return updatedUser;
}
Expand All @@ -211,12 +239,20 @@ export const handleLoggedIn = async (ctx: BotHandlerContext): Promise<User> => {
payload.sender,
"Unsuccessful. Either Amizone is down or you need to login again (hint: menu has a _logout_ option)"
);
await ctx.bot.sendInteractiveMessage(
payload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(payload.sender, renderAmizoneMenu());
return updatedUser;
}

if (typeof message === "string") {
await ctx.bot.sendMessage(payload.sender, message);
await ctx.bot.sendInteractiveMessage(
payload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(payload.sender, renderAmizoneMenu());
}

Expand All @@ -228,6 +264,63 @@ export const handleLoggedIn = async (ctx: BotHandlerContext): Promise<User> => {
return updatedUser;
};

export const handleReplyAttendanceButton = async (
ctx: BotHandlerContext
): Promise<User> => {
const { payload } = ctx;
const updatedUser = structuredClone(ctx.user);

// Check if the user clicked either "Today's Attendance" or "Yesterday's Attendance"
if (
payload.interactive.title === "Today's" ||
payload.interactive.title === "Yesterday's"
) {
let selectedDate;

if (payload.interactive.title === "Today's") {
selectedDate = new Date(dateToIST(new Date()).getTime());
} else {
selectedDate = new Date(
dateToIST(new Date()).getTime() - 1 * DAY_TO_MINUTE * MINUTE_TO_MS
);
}

try {
// Fetch attendance data for the selected date using your Amizone API client
const [year, month, day] = [
selectedDate.getFullYear(),
selectedDate.getMonth() + 1,
selectedDate.getDate(),
];
const attendance = await newAmizoneClient(
ctx.user.amizoneCredentials
).amizoneServiceGetClassSchedule(year, month, day);

// Send the attendance data as a message to the user
if (
attendance.data.classes !== undefined &&
attendance.data.classes.length > 0
) {
await ctx.bot.sendMessage(
payload.sender,
renderSchedule(attendance.data)
);
} else {
await ctx.bot.sendMessage(payload.sender, "no schedule available.");
}
await ctx.bot.sendInteractiveMessage(
payload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(payload.sender, renderAmizoneMenu());
} catch (error) {
// Handle errors (e.g., API request error)
console.error("Error fetching attendance for the selected date:", error);
}
}
return updatedUser;
};

export const handleScheduleDateInput = async (ctx: BotHandlerContext) => {
const { payload: whatsappPayload } = ctx;
const dateInput = firstNonEmpty(
Expand Down Expand Up @@ -273,6 +366,10 @@ export const handleScheduleDateInput = async (ctx: BotHandlerContext) => {
"no schedule available."
);
}
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderAmizoneMenu()
Expand All @@ -295,6 +392,10 @@ export const handleFacultyFeedbackRating = async (ctx: BotHandlerContext) => {

if (message.toLowerCase().trim() === "cancel") {
await ctx.bot.sendMessage(whatsappPayload.sender, "Cancelled.");
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderAmizoneMenu()
Expand Down Expand Up @@ -345,6 +446,10 @@ export const handleFacultyFeedbackRating = async (ctx: BotHandlerContext) => {
whatsappPayload.sender,
"No feedback to fill at the moment"
);
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderAmizoneMenu()
Expand All @@ -357,6 +462,10 @@ export const handleFacultyFeedbackRating = async (ctx: BotHandlerContext) => {
// @ts-ignore
renderFacultyFeedbackConfirmation(feedback.data.filledFor)
);
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderQuickAttendanceButtons()
);
await ctx.bot.sendInteractiveMessage(
whatsappPayload.sender,
renderAmizoneMenu()
Expand Down

0 comments on commit b524707

Please sign in to comment.