edupage-api
Version:
Simple node.js package to manage your EduPage account.
407 lines (350 loc) • 12.5 kB
JavaScript
const debug = require("debug")("edupage:log");
const error = require("debug")("edupage:error");
const {default: fetch} = require("node-fetch");
const CookieJar = require("../lib/CookieJar");
const {LoginError, ParseError, EdupageError, APIError, MessageError, FatalError} = require("./exceptions");
const {GENDER, ENDPOINT, ENTITY_TYPE, API_STATUS, TIMELINE_ITEM_TYPE} = require("./enums");
const Edupage = require("./Edupage");
const RawData = require("../lib/RawData");
const Attachment = require("./Attachment");
debug.log = console.log.bind(console);
/**
* @typedef {import("../lib/RawData").RawDataObject} RawDataObject
*/
/**
* @typedef {import("./Teacher")} Teacher
*/
/**
* @typedef {import("./Student")} Student
*/
/**
* @typedef {import("./Parent")} Parent
*/
/**
* @typedef {import("./Message")} Message
*/
/**
* @typedef {import("./enums").EntityType} EntityType
*/
/**
* @typedef {Object} LoginOptions
* @prop {string | null | undefined} [code2FA=undefined] If the provided value is typeof `string`, it's considired as a 2FA code (should be provided after first unsuccessful login). If it's `null` the 2FA will be skipped. If omitted or `undefined` and the 2FA is requested by the Edupage, the function will resolve with `null`.
* @prop {string} [user] Can be used to select a specific user in case there are more.
* @prop {string} [edupage=""] The edupage subdomain (origin) to login to. Try to set this if you have trouble logging in (e.g. incorrect password error).
*/
class User extends RawData {
/**
* Creates an instance of User.
* @param {RawDataObject} [data={}] Raw data to initialize the instance with.
* @param {Edupage} [edupage=null] Edupage instance to use.
* @memberof User
*/
constructor(data = {}, edupage = null) {
super(data);
/**
* Edupage instance associated to this object.
* @type {Edupage}
*/
this.edupage = edupage;
/**
* Date since when the user is registered
* @type {Date}
*/
this.dateFrom = data.datefrom ? new Date(data.datefrom) : null;
/**
* Date of expected leave of the user
* @type {Date}
*/
this.dateTo = data.dateto ? new Date(data.dateto) : null;
/**
* Firstname of the user
* @type {string}
*/
this.firstname = data.firstname;
/**
* Lastname of the user
* @type {string}
*/
this.lastname = data.lastname;
/**
* Gender of the user
* @type {GENDER}
*/
this.gender = data.gender;
/**
* Edupage identifier of the user in format of number
* @example "845796"
* @type {string}
*/
this.id = data.id;
/**
* Edupage userstring of the user
* @example "Student845796"
* @type {string}
*/
this.userString = null;
/**
* Flag marking if the user has left the school
* @type {boolean}
*/
this.isOut = data.isOut;
/**
* Edupage origin of the user (subdomain)
* @type {string}
*/
this.origin = null;
/**
* Login credentials of the user. Set if the user is logged in or has attempted to log in.
* @type {{username: string, password: string} | null}
*/
this.credentials = null;
/**
* CookieJar object storing current session cookies for logged in user
* @type {CookieJar}
*/
this.cookies = null;
/**
* Flag telling if the user is logged in
* @type {boolean}
*/
this.isLoggedIn = false;
/**
* Email address of the logged in user
* @type {string}
*/
this.email = null;
if(this.edupage) User.prototype.init.call(this);
}
/**
* Initializes instance with raw data
* @param {Edupage} [edupage=null]
* @memberof Class
*/
init(edupage = null) {
if(edupage) this.edupage = edupage;
this.origin = this.edupage.user.origin;
}
/**
* @typedef {Object} PollOption
* @prop {string} text Text of the option.
* @prop {string} [id] Id of the option. If not provided, a new one will be generated.
*/
/**
* @typedef {Object} PollOptions
* @prop {PollOption[]} options Options to be added to the poll.
* @prop {boolean} [multiple=false] If `true` multiple choices can be selected.
*/
/**
* @typedef {Object} MessageOptions
* @prop {string} text Text of the message.
* @prop {boolean} [important=false] If `true` the message will be marked as important. You will also be able to track who has read the message.
* @prop {boolean} [parents=false] If `true` the message will be sent to student as well as their parents.
* @prop {boolean} [allowReplies=true] Allows to disable replies to the message.
* @prop {boolean} [repliesToAuthorOnly=false] Allows to reply only to the author of the message. By default, replies are sent to all users.
* @prop {Attachment[]} [attachments=[]] Attachments to be added to the message.
* @prop {PollOptions} [poll] Poll to be added to the message.
*/
/**
* Sends a message to the user
* @param {MessageOptions} options Message options
* @this {User | Teacher | Student | Parent}
* @memberof User
*/
async sendMessage(options) {
const {
text = "",
important = false,
parents = false,
allowReplies = true,
repliesToAuthorOnly = false,
attachments = [],
poll = null
} = options;
if(!this.edupage) throw new EdupageError(`User does not have assigned Edupage instance yet`);
//Post message
const hasPoll = poll && poll.options && poll.options;
const res = await this.edupage.api({
url: ENDPOINT.TIMELINE_CREATE_ITEM,
data: {
attachements: JSON.stringify(attachments.reduce((a, b) => ({...a, ...b.toJSON()}), {})),
receipt: important ? "1" : "0",
repliesDisabled: !allowReplies ? "1" : "0",
repliesToAllDisabled: !allowReplies || repliesToAuthorOnly ? "1" : "0",
selectedUser: this.getUserString(parents),
text: text,
typ: TIMELINE_ITEM_TYPE.MESSAGE,
votingParams: hasPoll ? JSON.stringify({
"answers": poll.options.map(e => ({
text: e.text,
id: e.id || Math.random().toString(16).slice(2)
})),
"multiple": false
}) : undefined
}
});
//Request failed
if(res.status !== API_STATUS.OK) {
error(`Received invalid status from the server '${res.status}'`);
throw new APIError(`Failed to send message: Invalid status received '${res.status}'`, res);
}
//No changes
if(!res.changes?.length) {
error(`Failed to send message (no changes made) (${res.changes})`);
throw new MessageError(`Failed to send message: No changes were made`, res);
}
//Debug
if(res.changes.length > 1) debug(`[Message] Multiple changes after posting single message`, res.changes);
//Return Message object
return new (require("./Message"))(res.changes[0], this.edupage);
}
/**
* @typedef {import("../lib/ResponseTypings").MAuthResponse} MAuthResponse
*/
/**
* Logs in the user. Provide third parameter as login options if you have troubles logging in.
* @param {string} username Username of the user
* @param {string} password Password of the user
* @param {LoginOptions} [options] Login options
* @return {Promise<User | null>} Returns a promise that resolves with the `User` object if successful. If the 2FA is requested by the Edupage, the promise will resolve with `null`.
* @memberof User
*/
async login(username, password, options = {}) {
if(!username || !password) throw new LoginError(`Invalid credentials`);
//Setup options
if(!options) options = {};
if(!options.code2FA) options.code2FA = undefined;
if(!options.user) options.user = undefined;
//Create a new CookieJar instance to store required cookies
this.cookies = new CookieJar();
return new Promise((resolve, reject) => {
debug(`[Login] Logging in as ${username}...`);
const payload = {
"m": username,
"h": password,
"edupage": options.edupage || "",
"plgc": null,
"ajheslo": "1",
"hasujheslo": "1",
"ajportal": "1",
"ajportallogin": "1",
"mobileLogin": "1",
"version": "2020.0.18",
"fromEdupage": options.edupage || "",
"device_name": null,
"device_id": null,
"device_key": "",
"os": null,
"murl": null,
"edid": ""
};
const loginServer = options.edupage || "login1";
const skip2Fa = options.code2FA === null;
if(typeof options.code2FA === "string") payload.t2fasec = options.code2FA;
fetch(`https://${loginServer}.edupage.org/login/mauth`, {
"headers": {
"accept": "application/json, text/javascript, */*; q=0.01",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
"method": "POST",
"body": new URLSearchParams(payload).toString()
}).then(res => {
debug(`[Login] Saving received cookies...`);
this.cookies.setCookie(res);
return res.json();
}).then(async (/**@type {MAuthResponse}*/json) => {
let selectedUser = null;
//Error handling
if(!json.users.length) {
if(json.needEdupage) {
reject(new LoginError(`Failed to login: Incorrect username. (If you are sure that the username is correct, try providing 'edupage' option)`));
} else {
reject(new LoginError(`Failed to login: Incorrect password. (If you are sure that the password is correct, try providing 'edupage' option)`));
}
return;
}
//Process response
if(json.users.length == 1) {
if(json.users[0].need2fa == "1" && !skip2Fa) {
if(json.t2fasec) {
debug(`[Login] 2FA code is invalid`);
return reject(new LoginError(`Invalid 2FA code`));
} else {
debug(`[Login] 2FA was requested by the Edupage`);
return resolve(null);
}
} else {
debug(`[Login] Successfully logged in`);
selectedUser = json.users[0];
}
} else {
debug(`[Login] Found multiple users with the same username`);
if(options.user) {
const user = json.users.find(user => user.userid == options.user);
if(user) {
debug(`[Login] Selected user by 'user' option`);
selectedUser = user;
}
}
if(!selectedUser) {
error(`[Login] No user selected`);
return reject(new LoginError(`Multiple users found: ${json.users.map(user => `${user.userid} (${user.firstname} ${user.lastname})`).join(", ")}. Please, pass the selected user as 'user' option to login options.`));
}
}
//Update values
this.cookies.setCookie("PHPSESSID", selectedUser.esid);
this.origin = selectedUser.edupage;
this.isLoggedIn = true;
this.credentials = {
username,
password
};
debug(`[Login] Login successful`);
resolve(this);
}).catch(err => {
error(`[Login] Failed to login user:`, err);
reject(err);
});
});
}
/**
* Retruns Edupage's representation of the user id.
* @return {string} Userstring
* @memberof User
*/
getUserString() {
return this.userString;
}
/**
* Creates an instance of Student or Teacher from user data.
* @static
* @param {string} userString Userstring to create an instance of
* @param {RawDataObject} [data={}] Raw data to use for the instance
* @param {Edupage} [edupage=null] Edupage instance to use for the instance
* @return {User | Teacher | Student | Parent} User instance
* @memberof User
*/
static from(userString, data = {}, edupage = null) {
const {id, type} = this.parseUserString(userString);
data.id = id || null;
if(type == ENTITY_TYPE.TEACHER) return new (require("./Teacher"))(data, edupage);
if(type == ENTITY_TYPE.STUDENT) return new (require("./Student"))(data, edupage);
if(type == ENTITY_TYPE.PARENT) return new (require("./Parent"))(data, edupage);
const user = new User(data, edupage);
user.userString = userString;
return user;
}
/**
* Parses the user string to provide some useful information
* @static
* @param {string} userString Userstring to parse
* @return {{id: string, type: EntityType, wildcard: boolean}} Parsed userstring
* @memberof User
*/
static parseUserString(userString) {
const id = (userString.match(/-?\d+/) || [])[0];
const type = ((userString.match(/[a-z]+/i) || [])[0] || "");
const wildcard = userString.indexOf("*") > -1;
return {id, type, wildcard};
}
}
module.exports = User;