Skip to content

Commit

Permalink
feat: implement custom fetcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Vexcited committed Jan 16, 2024
1 parent 18806c1 commit a36a500
Show file tree
Hide file tree
Showing 21 changed files with 189 additions and 55 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ It returns a boolean that is `true` when the server is a demo.
- [x] parser: able to get attachment's URL
- [x] Find Pronote instances with longitude and latitude
- [x] `findPronoteInstances({ longitude, latitude })`
- Periods
- [x] Periods
- [x] client: `.periods` (property)
- [x] parser: `Period`
- Grades & Averages
- [x] Grades & Averages
- [x] client: `.getGradesOverviewForPeriod(period)`
- [x] parser: `Period.getGradesOverview()`
- [x] parser: `StudentGrade`
- [x] parser: `StudentAverage`
- [x] Custom `fetcher` to call the API with another API than [`fetch`](https://developer.mozilla.org/docs/Web/API/Fetch_API)

## Installation

Expand Down
67 changes: 67 additions & 0 deletions examples/custom-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { authenticatePronoteCredentials, PronoteApiAccountId, findPronoteInstances, type PawnoteFetcher } from "../src";

const customFetcher: PawnoteFetcher = async (url, options) => {
console.time("request from fetcher");

const response = await fetch(url, {
method: options.method,
headers: options.headers,
body: options.method !== "GET" ? options.body : void 0,
redirect: options.redirect
});

console.timeEnd("request from fetcher");

return {
json: async <T>() => {
const data = await response.json() as T;

// We can add stuff in those methods too !
console.info("-> Parsing a JSON in fetcher !");

return data;
},

text: async () => {
const data = await response.text();

// We can add stuff in those methods too !
console.info("-> Reading the response as text...");

return data;
},

// We can even write a function on the headers getter.
get headers () {
console.info("-> Reading headers from fetcher !");
return response.headers;
}
};
};

(async () => {
console.group("findPronoteInstances");
const geolocationResults = await findPronoteInstances(customFetcher, {
latitude: 45.849998,
longitude: 1.25
});

console.info("\nThere's", geolocationResults.length, "instances in the given location.");
console.groupEnd();

console.group("authenticate");
const pronote = await authenticatePronoteCredentials("https://demo.index-education.net/pronote", {
accountTypeID: PronoteApiAccountId.Eleve,
username: "demonstration",
password: "pronotevs",

// Because this is just an example, don't forget to change this.
deviceUUID: "my-device-uuid",

// We use our custom fetcher here !
fetcher: customFetcher
});

console.info("\nThere's", pronote.periods.length, "periods in the given pronote account.");
console.groupEnd();
})();
6 changes: 4 additions & 2 deletions examples/geolocation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { findPronoteInstances, type PronoteInstance } from "../src";
import { findPronoteInstances, defaultPawnoteFetcher, type PronoteInstance } from "../src";

(async () => {
const instances: Array<PronoteInstance> = await findPronoteInstances({
// We need to provide explicitly the fetcher here, since
// it's a direct API call.
const instances: Array<PronoteInstance> = await findPronoteInstances(defaultPawnoteFetcher, {
latitude: 45.849998,
longitude: 1.25
});
Expand Down
19 changes: 9 additions & 10 deletions src/api/geolocation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,23 @@ import { makeApiHandler } from "~/utils/api";
import { MOBILE_CHROME_USER_AGENT } from "~/constants/user-agent";

/** Gives every Pronote instance in a 20km radius of the given `longitude` and `latitude`. */
export const callApiGeolocation = makeApiHandler<ApiGeolocation>(async (input) => {
export const callApiGeolocation = makeApiHandler<ApiGeolocation>(async (fetcher, input) => {
const request_body: PronoteApiGeolocation["request"] = {
nomFonction: "geoLoc",
lat: input.latitude.toString(),
long: input.longitude.toString()
};

const body = new URLSearchParams();
body.set("data", JSON.stringify(request_body));
const searchParamsBody = new URLSearchParams();
searchParamsBody.set("data", JSON.stringify(request_body));

const headers = new Headers();
headers.set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
headers.set("User-Agent", MOBILE_CHROME_USER_AGENT);

const response = await fetch(PRONOTE_GEOLOCATION_URL, {
const response = await fetcher(PRONOTE_GEOLOCATION_URL, {
method: "POST",
headers,
body
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"User-Agent": MOBILE_CHROME_USER_AGENT
},
body: searchParamsBody.toString()
});

let data = await response.json() as PronoteApiGeolocation["response"];
Expand Down
4 changes: 2 additions & 2 deletions src/api/login/authenticate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PronoteApiFunctions } from "~/constants/functions";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { makeApiHandler } from "~/utils/api";

export const callApiLoginAuthenticate = makeApiHandler<ApiLoginAuthenticate>(async (input) => {
export const callApiLoginAuthenticate = makeApiHandler<ApiLoginAuthenticate>(async (fetcher, input) => {
const request_payload = input.session.writePronoteFunctionPayload<PronoteApiLoginAuthenticate["request"]>({
donnees: {
connexion: 0,
Expand All @@ -13,7 +13,7 @@ export const callApiLoginAuthenticate = makeApiHandler<ApiLoginAuthenticate>(asy
}
});

const response = await createPronoteAPICall(PronoteApiFunctions.Authenticate, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.Authenticate, {
session_instance: input.session.instance,
cookies: input.cookies ?? [],
payload: request_payload
Expand Down
4 changes: 2 additions & 2 deletions src/api/login/identify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PronoteApiFunctions } from "~/constants/functions";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { makeApiHandler } from "~/utils/api";

export const callApiLoginIdentify = makeApiHandler<ApiLoginIdentify>(async (input) => {
export const callApiLoginIdentify = makeApiHandler<ApiLoginIdentify>(async (fetcher, input) => {
const requestPayload = input.session.writePronoteFunctionPayload<PronoteApiLoginIdentify["request"]>({
donnees: {
genreConnexion: 0,
Expand All @@ -21,7 +21,7 @@ export const callApiLoginIdentify = makeApiHandler<ApiLoginIdentify>(async (inpu
}
});

const response = await createPronoteAPICall(PronoteApiFunctions.Identify, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.Identify, {
session_instance: input.session.instance,
cookies: input.cookies ?? [],
payload: requestPayload
Expand Down
4 changes: 2 additions & 2 deletions src/api/login/informations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PronoteApiFunctions } from "~/constants/functions";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import forge from "node-forge";

export const callApiLoginInformations = makeApiHandler<ApiLoginInformations>(async (input) => {
export const callApiLoginInformations = makeApiHandler<ApiLoginInformations>(async (fetcher, input) => {
const accountType = PRONOTE_ACCOUNT_TYPES.find((entry) => entry.id === input.accountTypeID);
if (!accountType) throw new Error(`Invalid account type ID: ${input.accountTypeID}`);

Expand Down Expand Up @@ -66,7 +66,7 @@ export const callApiLoginInformations = makeApiHandler<ApiLoginInformations>(asy
}
});

const response = await createPronoteAPICall(PronoteApiFunctions.Informations, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.Informations, {
cookies,
payload: request_payload,
session_instance: session.instance
Expand Down
4 changes: 2 additions & 2 deletions src/api/user/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { PronoteApiFunctions } from "~/constants/functions";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { makeApiHandler } from "~/utils/api";

export const callApiUserData = makeApiHandler<ApiUserData>(async (input) => {
export const callApiUserData = makeApiHandler<ApiUserData>(async (fetcher, input) => {
const request_payload = input.session.writePronoteFunctionPayload<PronoteApiUserData["request"]>({});
const response = await createPronoteAPICall(PronoteApiFunctions.UserData, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.UserData, {
session_instance: input.session.instance,
payload: request_payload
});
Expand Down
4 changes: 2 additions & 2 deletions src/api/user/grades/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeApiHandler } from "~/utils/api";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { PronoteApiFunctions } from "~/constants/functions";

export const callApiUserGrades = makeApiHandler<ApiUserGrades>(async (input) => {
export const callApiUserGrades = makeApiHandler<ApiUserGrades>(async (fetcher, input) => {
const request_payload = input.session.writePronoteFunctionPayload<PronoteApiUserGrades["request"]>({
donnees: {
Periode: {
Expand All @@ -15,7 +15,7 @@ export const callApiUserGrades = makeApiHandler<ApiUserGrades>(async (input) =>

_Signature_: { onglet: PronoteApiOnglets.Grades }
});
const response = await createPronoteAPICall(PronoteApiFunctions.Grades, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.Grades, {
session_instance: input.session.instance,
payload: request_payload
});
Expand Down
4 changes: 2 additions & 2 deletions src/api/user/homework/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PronoteApiOnglets } from "~/constants/onglets";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { makeApiHandler } from "~/utils/api";

export const callApiUserHomework = makeApiHandler<ApiUserHomework>(async (input) => {
export const callApiUserHomework = makeApiHandler<ApiUserHomework>(async (fetcher, input) => {
if (input.fromWeekNumber <= 0) {
throw new Error(`Invalid input on callApiUserHomework, "fromWeekNumber" should be a strictly positive number, got ${input.fromWeekNumber}`);
}
Expand All @@ -27,7 +27,7 @@ export const callApiUserHomework = makeApiHandler<ApiUserHomework>(async (input)
}
});

const response = await createPronoteAPICall(PronoteApiFunctions.Homework, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.Homework, {
session_instance: input.session.instance,
payload: request_payload
});
Expand Down
4 changes: 2 additions & 2 deletions src/api/user/homeworkStatus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PronoteApiOnglets } from "~/constants/onglets";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { makeApiHandler } from "~/utils/api";

export const callApiUserHomeworkStatus = makeApiHandler<ApiUserHomeworkStatus>(async (input) => {
export const callApiUserHomeworkStatus = makeApiHandler<ApiUserHomeworkStatus>(async (fetcher, input) => {
const request_payload = input.session.writePronoteFunctionPayload<PronoteApiUserHomeworkStatus["request"]>({
donnees: {
listeTAF: [{
Expand All @@ -19,7 +19,7 @@ export const callApiUserHomeworkStatus = makeApiHandler<ApiUserHomeworkStatus>(a
}
});

const response = await createPronoteAPICall(PronoteApiFunctions.HomeworkDone, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.HomeworkDone, {
session_instance: input.session.instance,
payload: request_payload
});
Expand Down
4 changes: 2 additions & 2 deletions src/api/user/timetable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PronoteApiOnglets } from "~/constants/onglets";
import { createPronoteAPICall } from "~/pronote/requestAPI";
import { makeApiHandler } from "~/utils/api";

export const callApiUserTimetable = makeApiHandler<ApiUserTimetable>(async (input) => {
export const callApiUserTimetable = makeApiHandler<ApiUserTimetable>(async (fetcher, input) => {
if (input.weekNumber <= 0) {
throw new Error(`Invalid input on callApiUserTimetable, "weekNumber" should be a strictly positive number, got ${input.weekNumber}`);
}
Expand All @@ -32,7 +32,7 @@ export const callApiUserTimetable = makeApiHandler<ApiUserTimetable>(async (inpu
_Signature_: { onglet: PronoteApiOnglets.Timetable }
});

const response = await createPronoteAPICall(PronoteApiFunctions.Timetable, {
const response = await createPronoteAPICall(fetcher, PronoteApiFunctions.Timetable, {
session_instance: input.session.instance,
payload: request_payload
});
Expand Down
27 changes: 17 additions & 10 deletions src/authenticate/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { AuthenticatePronoteCredentialsOptions, AuthenticateTokenOptions, NextAuthenticationCredentials } from "./types";
import { callApiLoginInformations, callApiLoginIdentify, callApiLoginAuthenticate, callApiUserData } from "~/api";
import { PRONOTE_ACCOUNT_TYPES } from "~/constants/accounts";
import { defaultPawnoteFetcher } from "~/utils/fetcher";
import aes from "~/utils/aes";

import Pronote from "~/client/Pronote";
import forge from "node-forge";

export const authenticatePronoteCredentials = async (pronoteStringURL: string, options: AuthenticatePronoteCredentialsOptions): Promise<Pronote> => {
// Use default fetcher if not provided.
const fetcher = options.fetcher ?? defaultPawnoteFetcher;

const pronoteURL = new URL(pronoteStringURL);

const accountType = PRONOTE_ACCOUNT_TYPES.find((entry) => entry.id === options.accountTypeID);
Expand All @@ -22,13 +26,13 @@ export const authenticatePronoteCredentials = async (pronoteStringURL: string, o

const pronoteCookies = ["ielang=fr"];

const { createdSession: session, data: loginInformations } = await callApiLoginInformations({
const { createdSession: session, data: loginInformations } = await callApiLoginInformations(fetcher, {
accountTypeID: accountType.id,
pronoteURL: pronoteURL.href,
cookies: pronoteCookies
});

const { data: loginIdentifier } = await callApiLoginIdentify({
const { data: loginIdentifier } = await callApiLoginIdentify(fetcher, {
cookies: pronoteCookies,
username: options.username,
session: session,
Expand Down Expand Up @@ -91,7 +95,7 @@ export const authenticatePronoteCredentials = async (pronoteStringURL: string, o
}

// Send the resolved challenge.
const { data: authenticationReply } = await callApiLoginAuthenticate({
const { data: authenticationReply } = await callApiLoginAuthenticate(fetcher, {
solvedChallenge: resolved_challenge,
cookies: pronoteCookies,
session
Expand All @@ -109,18 +113,21 @@ export const authenticatePronoteCredentials = async (pronoteStringURL: string, o
session.encryption.aes.key = authKey;

// Retrieve the user data.
const { data: user } = await callApiUserData({ session });
const { data: user } = await callApiUserData(fetcher, { session });

const credentials: NextAuthenticationCredentials = {
username: loginIdentifier.donnees.login ?? options.username,
token: authenticationReply.donnees.jetonConnexionAppliMobile
};

// Return the new Pronote instance.
return new Pronote(session, credentials, user.donnees, loginInformations);
return new Pronote(fetcher, session, credentials, user.donnees, loginInformations);
};

export const authenticateToken = async (pronoteStringURL: string, options: AuthenticateTokenOptions): Promise<Pronote> => {
// Use default fetcher if not provided.
const fetcher = options.fetcher ?? defaultPawnoteFetcher;

const pronoteURL = new URL(pronoteStringURL);

const accountType = PRONOTE_ACCOUNT_TYPES.find((entry) => entry.id === options.accountTypeID);
Expand All @@ -133,13 +140,13 @@ export const authenticateToken = async (pronoteStringURL: string, options: Authe

const pronoteCookies = ["ielang=fr", "appliMobile=1"];

const { createdSession: session, data: loginInformations } = await callApiLoginInformations({
const { createdSession: session, data: loginInformations } = await callApiLoginInformations(fetcher, {
accountTypeID: accountType.id,
pronoteURL: pronoteURL.href,
cookies: pronoteCookies
});

const { data: loginIdentifier } = await callApiLoginIdentify({
const { data: loginIdentifier } = await callApiLoginIdentify(fetcher, {
cookies: pronoteCookies,
username: options.username,
session: session,
Expand Down Expand Up @@ -202,7 +209,7 @@ export const authenticateToken = async (pronoteStringURL: string, options: Authe
}

// Send the resolved challenge.
const { data: authenticationReply } = await callApiLoginAuthenticate({
const { data: authenticationReply } = await callApiLoginAuthenticate(fetcher, {
solvedChallenge: resolved_challenge,
cookies: pronoteCookies,
session
Expand All @@ -220,13 +227,13 @@ export const authenticateToken = async (pronoteStringURL: string, options: Authe
session.encryption.aes.key = authKey;

// Retrieve the user data.
const { data: user } = await callApiUserData({ session });
const { data: user } = await callApiUserData(fetcher, { session });

const credentials: NextAuthenticationCredentials = {
username: loginIdentifier.donnees.login ?? options.username,
token: authenticationReply.donnees.jetonConnexionAppliMobile
};

// Return the new Pronote instance.
return new Pronote(session, credentials, user.donnees, loginInformations);
return new Pronote(fetcher, session, credentials, user.donnees, loginInformations);
};
8 changes: 8 additions & 0 deletions src/authenticate/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type { PronoteApiAccountId } from "~/constants/accounts";
import type { PawnoteFetcher } from "~/utils/fetcher";

interface AuthenticateBaseOptions {
/**
* An unique identifier for the device
* that will authenticate to Pronote.
*/
deviceUUID: string

/**
* By default, Pawnote is going to use `fetch` (Fetch API).
* If, for some reason, you need to use another method to make an
* HTTP request, you'll have to provide it here.
*/
fetcher?: PawnoteFetcher
}

export interface AuthenticatePronoteCredentialsOptions extends AuthenticateBaseOptions {
Expand Down
Loading

0 comments on commit a36a500

Please sign in to comment.