Skip to content

Commit

Permalink
feat: add proper errors (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vexcited committed Aug 28, 2024
1 parent 41415f4 commit af01305
Show file tree
Hide file tree
Showing 23 changed files with 162 additions and 100 deletions.
10 changes: 4 additions & 6 deletions src/api/assignment-upload-file.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { UploadSizeError, TabLocation, type SessionHandle, DocumentKind, EntityState } from "~/models";
import type { FormDataFile } from "@literate.ink/utilities";
import { RequestFN } from "~/core/request-function";
import { RequestUpload } from "~/core/request-upload";
import { TabLocation, type SessionHandle } from "~/models";
import { DocumentKind } from "~/models/document-kind";
import { EntityState } from "~/models/entity-state";
import { createEntityID } from "~/api/helpers/entity-id";
import { RequestUpload } from "~/core/request-upload";
import { RequestFN } from "~/core/request-function";

export const assignmentUploadFile = async (session: SessionHandle, assignmentID: string, file: FormDataFile, fileName: string): Promise<void> => {
// Check if the file can be uploaded.
Expand All @@ -13,7 +11,7 @@ export const assignmentUploadFile = async (session: SessionHandle, assignmentID:
const fileSize: number | undefined = file.size || file.byteLength;
const maxFileSize = session.user.authorizations.maxAssignmentFileUploadSize;
if (typeof fileSize === "number" && fileSize > maxFileSize) {
throw new Error(`File size is too big, maximum allowed is ${maxFileSize} bytes.`);
throw new UploadSizeError(maxFileSize);
}

// Ask to the server to store the file for us.
Expand Down
4 changes: 2 additions & 2 deletions src/api/discussion-send-draft.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Discussion, DiscussionDraftMessage, DiscussionMessages, SessionHandle } from "~/models";
import { type Discussion, DiscussionActionError, type DiscussionDraftMessage, type SessionHandle } from "~/models";
import { discussionPostCommand } from "./private/discussion-post-command";
import { encodeDiscussionSendAction } from "~/encoders/discussion-send-action";
import { discussions } from "./discussions";
import { discussionMessages } from "./discussion-messages";

export const discussionSendDraft = async (session: SessionHandle, discussion: Discussion, draft: DiscussionDraftMessage, includeParentsAndStudents = false): Promise<void> => {
if (typeof discussion.messages?.sendAction === "undefined")
throw new Error("You can't create drafts in this discussion.");
throw new DiscussionActionError();

await discussionPostCommand(session, "", {
button: encodeDiscussionSendAction(discussion.messages.sendAction, includeParentsAndStudents),
Expand Down
8 changes: 4 additions & 4 deletions src/api/discussion-send-message.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Discussion, DiscussionMessages, EntityState, SessionHandle, TabLocation } from "~/models";
import { type Discussion, DiscussionActionError, DiscussionMessagesMissingError, EntityState, type SessionHandle, TabLocation } from "~/models";
import { encodeDiscussionSendAction } from "~/encoders/discussion-send-action";
import { RequestFN } from "~/core/request-function";
import { discussionMessages } from "./discussion-messages";
import { createEntityID } from "./helpers/entity-id";
import { discussions } from "./discussions";
import { discussionMessages } from "./discussion-messages";

export const discussionSendMessage = async (
session: SessionHandle,
Expand All @@ -13,10 +13,10 @@ export const discussionSendMessage = async (
replyTo = discussion.messages?.defaultReplyMessageID
): Promise<void> => {
if (!discussion.messages)
throw new Error("You should request messages before sending.");
throw new DiscussionMessagesMissingError();

if (typeof discussion.messages.sendAction === "undefined")
throw new Error("You can't create messages in this discussion.");
throw new DiscussionActionError();

const action = encodeDiscussionSendAction(discussion.messages.sendAction, includeParentsAndStudents);
const request = new RequestFN(session, "SaisieMessage", {
Expand Down
3 changes: 0 additions & 3 deletions src/api/helpers/frequency.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { SessionHandle, WeekFrequency } from "~/models";

export const frequency = (session: SessionHandle, weekNumber: number): WeekFrequency | undefined => {
if (weekNumber < 1) throw new Error("Week number must be at least 1.");
else if (weekNumber > 62) throw new Error("Week number can't be more than maximum value which is 62.");

return session.instance.weekFrequencies.get(weekNumber);
};
4 changes: 2 additions & 2 deletions src/api/helpers/login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AccountKind, RefreshInformation, SessionHandle } from "~/models";
import { type AccountKind, type RefreshInformation, type SessionHandle, BadCredentialsError } from "~/models";
import { sessionInformation } from "../session-information";
import { instanceParameters } from "../private/instance-parameters";
import { cleanURL } from "./clean-url";
Expand Down Expand Up @@ -215,7 +215,7 @@ const solveChallenge = (session: SessionHandle, identity: any, key: forge.util.B
return AES.encrypt(solution, key, iv);
}
catch {
throw new Error("Unable to resolve the challenge, probably bad credentials.");
throw new BadCredentialsError();
}
};

Expand Down
4 changes: 0 additions & 4 deletions src/api/helpers/timings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ export const translatePositionToTimings = (

const formatted = endings[position];

if (!formatted) {
throw new Error(`Could not find starting time for position ${position}`);
}

const [hours, minutes] = formatted.split("h").map(Number);
return { hours, minutes };
};
41 changes: 12 additions & 29 deletions src/api/private/authenticate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RequestFN } from "~/core/request-function";
import type { SessionHandle } from "~/models";
import { AccessDeniedError, AccountDisabledError, AuthenticateError, BadCredentialsError, type SessionHandle } from "~/models";

export const authenticate = async (session: SessionHandle, challenge: string) => {
const request = new RequestFN(session, "Authentification", {
Expand All @@ -15,51 +15,34 @@ export const authenticate = async (session: SessionHandle, challenge: string) =>

// Handle potential errors.
if (typeof data.Acces === "number" && data.Acces !== 0) {
let error = "Unknown error during authentication.";

switch (data.Acces) {
case 1: // EGenreErreurAcces.Identification
error = "Your username or password is incorrect.\nPlease note that passwords are case-sensitive.";
break;
throw new BadCredentialsError();

case 2: // EGenreErreurAcces.Autorisation
error = "Access denied: You do not have access to this area.\nContact the school to update your authorization profile.";
break;
case 3: // EGenreErreurAcces.ConnexionClasse
error = "You do not have the necessary authorizations to access displays linked to the 'In the classroom' connection mode.";
break;
case 4: // EGenreErreurAcces.AucuneRessource
error = "Access denied: You do not have access to this area.\nContact the school to update your information.";
break;
case 5: // EGenreErreurAcces.Connexion
error = "You do not have the necessary authorizations to access displays.";
break;
case 6: // EGenreErreurAcces.BloqueeEleve
error = "Following your departure from the school, your connection to the Students' Area has been blocked.";
break;
case 7: // EGenreErreurAcces.FonctionAccompagnant
error = "You do not have access to this area.\nContact the school to update your function.";
break;
case 8: // EGenreErreurAcces.AccompagnantAucunEleve
error = "You do not have access to this area.\nContact the school to be assigned the students you are accompanying.";
break;
throw new AccessDeniedError();

case 6: // EGenreErreurAcces.BloqueeEleve
case 10: // EGenreErreurAcces.CompteDesactive
error = "You do not have access to this account type. Your account has been deactivated.";
break;
throw new AccountDisabledError();

case 9: // EGenreErreurAcces.Message
if (typeof data.AccesMessage !== "undefined") {
error = data.AccesMessage.message ?? error;
let error: string = data.AccesMessage.message ?? "(none)";

if (data.AccesMessage.titre) {
error = `${data.AccesMessage.titre}: ${error}`;
error += `${data.AccesMessage.titre} ${error}`;
}

throw new AuthenticateError(error);
}
}

throw new Error(error);
}

if (!data.jetonConnexionAppliMobile)
throw new Error("Next time token wasn't given.");

return data;
};
15 changes: 5 additions & 10 deletions src/api/session-information.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defaultFetcher, type Fetcher, type Request, setCookiesArrayToRequest } from "@literate.ink/utilities";
import { decodeSessionInformation } from "~/decoders/session-information";
import { encodeAccountKindToPath } from "~/encoders/account-kind";
import type { AccountKind, SessionInformation } from "~/models";
import { BusyPageError, PageUnavailableError, SuspendedIPError, type AccountKind, type SessionInformation } from "~/models";

export const sessionInformation = async (options: {
base: string
Expand Down Expand Up @@ -41,20 +41,15 @@ export const sessionInformation = async (options: {
}
catch (error) {
if (html.includes("Votre adresse IP est provisoirement suspendue")) {
throw new Error("Your IP address is temporarily suspended.");
throw new SuspendedIPError();
}
else if (html.includes("Le site n'est pas disponible")) {
throw new Error("The site is not available.");
throw new PageUnavailableError();
}
else if (html.includes("Le site est momentanément indisponible")) {
let error = "The site is temporarily unavailable";
if (html.includes("Toutes les sessions utilisateurs sont occupées")) {
error += ", all user sessions are busy";
}

throw new Error(error + ".");
throw new BusyPageError();
}

throw new Error("Failed to extract session from HTML, for an unknown reason.");
throw new PageUnavailableError();
}
};
9 changes: 3 additions & 6 deletions src/core/request-upload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FormDataFile } from "@literate.ink/utilities";
import type { SessionHandle } from "~/models";
import { UploadFailedError, type SessionHandle } from "~/models";
import { aesKeys } from "~/api/private/keys";
import { AES } from "~/api/private/aes";

Expand Down Expand Up @@ -61,11 +61,8 @@ export class RequestUpload {
// Even if there's an error, it bumped.
this.session.information.order++;

if (state === 0) { // UNKNOWN
throw new Error("The upload state is unknown.");
}
else if (state === 2) { // ERROR
throw new Error("The upload failed.");
if (state === 0 || state === 2) { // UNKNOWN or ERROR
throw new UploadFailedError();
}
}
}
29 changes: 9 additions & 20 deletions src/core/response-function.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SessionHandle } from "~/models";
import { PageUnavailableError, RateLimitedError, ServerSideError, SessionExpiredError, SuspendedIPError, type SessionHandle } from "~/models";
import forge from "node-forge";
import { AES } from "../api/private/aes";
import pako from "pako";
Expand Down Expand Up @@ -27,27 +27,25 @@ export class ResponseFN {
this.data = JSON.parse(this.data);
}

this.check();
if ("_Signature_" in this.data && this.data._Signature_.Erreur) {
throw new ServerSideError(this.data._Signature_.MessageErreur);
}
}
catch (error) {
if (content.includes("La page a expir")) {
throw new Error("The page has expired.");
throw new SessionExpiredError();
}

else if (content.includes("Votre adresse IP ")) {
throw new Error("Your IP address is temporarily suspended.");
}

else if (content.includes("La page dem")) {
throw new Error("The requested page does not exist.");
throw new SuspendedIPError();
}

else if (content.includes("Impossible d'a")) {
throw new Error("Page unaccessible.");
else if (content.includes("La page dem") || content.includes("Impossible d'a")) {
throw new PageUnavailableError();
}

else if (content.includes("Vous avez d")) {
throw new Error("You've been rate-limited.");
throw new RateLimitedError();
}

throw error;
Expand All @@ -69,13 +67,4 @@ export class ResponseFN {
const compressed = new Uint8Array(Array.from(bytes).map((char) => char.charCodeAt(0)));
this.data = pako.inflateRaw(compressed, { to: "string" });
}

/**
* Handle potential errors in the response.
*/
private check (): void {
if ("_Signature_" in this.data && this.data._Signature_.Erreur) {
throw new Error(this.data._Signature_.MessageErreur ?? "An error occurred, server-side.");
}
}
}
5 changes: 2 additions & 3 deletions src/decoders/account-kind.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AccountKind } from "~/models";
import { AccountKind, UnreachableError } from "~/models";

/**
* @param path mobile.eleve.html or eleve.html, both works.
Expand All @@ -11,7 +11,6 @@ export const decodeAccountKindFromPath = (path: string): AccountKind => {
case "eleve": return AccountKind.STUDENT;
case "parent": return AccountKind.PARENT;
case "professeur": return AccountKind.TEACHER;

default: throw new Error(`Unknown account kind: ${path}`); // TODO: have the error in `models`
default: throw new UnreachableError("decodeAccountKindFromPath");
}
};
6 changes: 2 additions & 4 deletions src/decoders/grade-value.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type GradeValue, GradeKind } from "~/models";
import { type GradeValue, GradeKind, UnreachableError } from "~/models";

export const decodeGradeValue = (grade: string | number): GradeValue => {
let kind: GradeKind = GradeKind.Grade;
Expand All @@ -24,9 +24,7 @@ export const decodeGradeValue = (grade: string | number): GradeValue => {
else if (typeof grade === "number") {
value = grade;
}
else {
throw new Error("decodeGradeValue: Unknown grade type.");
}
else throw new UnreachableError("decodeGradeValue");

return {
kind,
Expand Down
4 changes: 2 additions & 2 deletions src/decoders/meal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Meal, DishKind } from "~/models";
import { type Meal, DishKind, UnreachableError } from "~/models";
import { decodeFood } from "./food";

export const decodeMeal = (meal: any): Meal => {
Expand All @@ -25,7 +25,7 @@ export const decodeMeal = (meal: any): Meal => {
key = "dessert";
break;
default:
throw new Error("unreachable");
throw new UnreachableError("decodeMeal");
}

acc[key] = dish.ListeAliments.V.map(decodeFood);
Expand Down
4 changes: 2 additions & 2 deletions src/decoders/menu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Menu, MealKind } from "~/models";
import { type Menu, MealKind, UnreachableError } from "~/models";
import { decodePronoteDate } from "./pronote-date";
import { decodeMeal } from "./meal";

Expand All @@ -14,7 +14,7 @@ export const decodeMenu = (menu: any): Menu => {
key = "dinner";
break;
default:
throw new Error("unreachable");
throw new UnreachableError("decodeMenu");
}

acc[key] = decodeMeal(meal);
Expand Down
4 changes: 2 additions & 2 deletions src/decoders/news.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { News, NewsCategory, SessionHandle } from "~/models";
import { UnreachableError, type News, type NewsCategory, type SessionHandle } from "~/models";
import { decodeNewsInformation } from "./news-information";
import { decodeNewsCategory } from "./news-category";
import { decodeNewsSurvey } from "./news-survey";
Expand All @@ -13,7 +13,7 @@ export const decodeNews = (news: any, session: SessionHandle): News => {

if (item.estInformation) decoder = decodeNewsInformation;
else if (item.estSondage) decoder = decodeNewsSurvey;
else throw new Error("Unknown news type");
else throw new UnreachableError("decodeNews");

return decoder(item, session, categories);
})
Expand Down
4 changes: 3 additions & 1 deletion src/decoders/pronote-date.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UnreachableError } from "~/models";

const SHORT_DATE_RE = /^\d{2}\/\d{2}\/\d{4}$/;
const LONG_DATE_LONG_HOURS_RE = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}$/;
const LONG_DATE_SHORT_HOURS_RE = /^\d{2}\/\d{2}\/\d{2} \d{2}h\d{2}$/;
Expand Down Expand Up @@ -29,5 +31,5 @@ export const decodePronoteDate = (formatted: string): Date => {
return output;
}

throw new Error("Could not parse date given by the API.");
throw new UnreachableError("decodePronoteDate");
};
27 changes: 27 additions & 0 deletions src/models/errors/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export class BadCredentialsError extends Error {
constructor() {
super("Unable to resolve the challenge, make sure the credentials or token are corrects");
this.name = "BadCredentialsError";
}
}

export class AuthenticateError extends Error {
constructor(message: string) {
super(message);
this.name = "AuthenticateError";
}
}

export class AccessDeniedError extends Error {
constructor() {
super("You do not have access to this area or your authorizations are insufficient");
this.name = "AccessDeniedError";
}
}

export class AccountDisabledError extends Error {
constructor() {
super("Your account has been deactivated");
this.name = "AccountDisabledError";
}
}
Loading

0 comments on commit af01305

Please sign in to comment.