import { Type } from "class-transformer" ; import { IsArray , IsBoolean , IsDate , IsDefined , IsEnum , IsInt , IsNotEmpty , IsNumber , IsObject , IsOptional , IsString , ValidateIf , } from "class-validator" ; import "reflect-metadata" ; export const PROD_URL = "https://officehours.khoury.northeastern.edu" ; export const STAGING_URL = "https://staging.khouryofficehours.com" ; const domain = ( ) : string | false => process . env . DOMAIN || ( typeof window !== "undefined" && window ?. location ?. origin ) ; export const getEnv = ( ) : "production" | "staging" | "dev" => { switch ( domain ( ) ) { case PROD_URL : return "production" ; case STAGING_URL : return "staging" ; default : return "dev" ; } } ; export const isProd = ( ) : boolean => domain ( ) === PROD_URL ; export function timeDiffInMins ( a , b ) { return ( a . getTime ( ) - b . getTime ( ) ) / ( 1000 * 60 ) ; } param id param email param name param photoURL param courses param desktopNotifs export class User { id ! : number ; email ! : string ; firstName ? : string ; lastName ? : string ; name ! : string ; photoURL ! : string ; defaultMessage ? : string ; includeDefaultMessage ! : boolean ; courses ! : UserCourse [ ] ; pendingCourses ? : KhouryProfCourse [ ] ; desktopNotifsEnabled ! : boolean ; @ Type ( ( ) => DesktopNotifPartial ) desktopNotifs ! : DesktopNotifPartial [ ] ; phoneNotifsEnabled ! : boolean ; phoneNumber ! : string ; insights ! : string [ ] ; } export class DesktopNotifPartial { id ! : number ; endpoint ! : string ; name ? : string ; @ Type ( ( ) => Date ) createdAt ! : Date ; } param id param name param photoURL export class UserPartial { id ! : number ; email ? : string ; name ? : string ; photoURL ? : string ; } param id param name export type CoursePartial = { id : number ; name : string ; } ; param course param role export type UserCourse = { course : CoursePartial ; role : Role ; } ; /**
 * Represents one of three possible user roles in a course.
 */ export enum Role { STUDENT = "student" , TA = "ta" , PROFESSOR = "professor" } class OfficeHourPartial { id ! : number ; title ! : string ; @ Type ( ( ) => Date ) startTime ! : Date ; @ Type ( ( ) => Date ) endTime ! : Date ; } param id param course param room param staffList param questions param startTime param endTime export interface Queue { id : number ; course : CoursePartial ; room : string ; staffList : UserPartial [ ] ; questions : Question [ ] ; startTime ? : Date ; endTime ? : Date ; allowQuestions : boolean ; } param id param room param staffList param startTime param endTime export class QueuePartial { id ! : number ; room ! : string ; @ Type ( ( ) => UserPartial ) staffList ! : UserPartial [ ] ; queueSize ! : number ; notes ? : string ; isOpen ! : boolean ; @ Type ( ( ) => Date ) startTime ? : Date ; @ Type ( ( ) => Date ) endTime ? : Date ; allowQuestions ! : boolean ; isProfessorQueue ! : boolean ; } export type Heatmap = Array < number > ; param id param creator param text param createdAt param helpedAt param closedAt param questionType param status param position param location param isOnline export class Question { id ! : number ; @ Type ( ( ) => UserPartial ) creator ! : UserPartial ; text ? : string ; @ Type ( ( ) => UserPartial ) taHelped ? : UserPartial ; @ Type ( ( ) => Date ) createdAt ! : Date ; @ Type ( ( ) => Date ) helpedAt ? : Date ; @ Type ( ( ) => Date ) closedAt ? : Date ; questionType ? : QuestionType ; groupable ! : boolean ; status ! : QuestionStatus ; location ? : string ; isOnline ? : boolean ; } export enum QuestionType { Concept = "Concept" , Clarification = "Clarification" , Testing = "Testing" , Bug = "Bug" , Setup = "Setup" , Other = "Other" } export enum OpenQuestionStatus { Drafting = "Drafting" , Queued = "Queued" , Helping = "Helping" , PriorityQueued = "PriorityQueued" } /**
 * Limbo statuses are awaiting some confirmation from the student
 */ export enum LimboQuestionStatus { CantFind = "CantFind" , ReQueueing = "ReQueueing" , TADeleted = "TADeleted" } export enum ClosedQuestionStatus { Resolved = "Resolved" , DeletedDraft = "DeletedDraft" , ConfirmedDeleted = "ConfirmedDeleted" , Stale = "Stale" } export const StatusInQueue = [ OpenQuestionStatus . Drafting , OpenQuestionStatus . Queued , ] ; export const StatusInPriorityQueue = [ OpenQuestionStatus . PriorityQueued ] ; export const StatusSentToCreator = [ ... StatusInPriorityQueue , ... StatusInQueue , OpenQuestionStatus . Helping , LimboQuestionStatus . ReQueueing , LimboQuestionStatus . CantFind , LimboQuestionStatus . TADeleted , ] ; export type QuestionStatus = keyof typeof QuestionStatusKeys ; export const QuestionStatusKeys = { ... OpenQuestionStatus , ... ClosedQuestionStatus , ... LimboQuestionStatus , } ; export class QuestionGroup { @ IsInt ( ) id ! : number ; @ Type ( ( ) => Question ) questions ! : Array < Question > ; @ Type ( ( ) => UserPartial ) creator ! : UserPartial ; } /**
 * Represents one of the seasons in which a course can take place.
 */ export type Season = "Fall" | "Spring" | "Summer_1" | "Summer_2" | "Summer_Full" ; export type DesktopNotifBody = { endpoint : string ; expirationTime ? : number ; keys : { p256dh : string ; auth : string ; } ; name ? : string ; } ; export type PhoneNotifBody = { phoneNumber : string ; } ; export class GetProfileResponse extends User { } export class KhouryDataParams { @ IsString ( ) email ! : string ; @ IsString ( ) first_name ! : string ; @ IsString ( ) last_name ! : string ; @ IsInt ( ) campus ! : number ; @ IsOptional ( ) @ IsString ( ) photo_url ! : string ; @ IsOptional ( ) @ IsDefined ( ) courses ! : KhouryCourse [ ] | KhouryProfCourse [ ] ; } export class KhouryCourse { @ IsInt ( ) crn ! : number ; @ IsString ( ) semester ! : string ; @ IsEnum ( String ) role ! : "TA" | "Student" ; } export class KhouryProfCourse { @ IsArray ( ) crns ! : number [ ] ; @ IsString ( ) semester ! : string ; @ IsString ( ) name ! : string ; } export function isKhouryCourse ( c ) { return ( ( c as KhouryCourse ) . role !== undefined && ( c as KhouryCourse ) . crn !== undefined ) ; } export interface KhouryRedirectResponse { redirect : string ; } export class UpdateProfileParams { @ IsBoolean ( ) @ IsOptional ( ) desktopNotifsEnabled ? : boolean ; @ IsBoolean ( ) @ IsOptional ( ) phoneNotifsEnabled ? : boolean ; @ ValidateIf ( ( o ) => o . phoneNotifsEnabled ) @ IsString ( ) @ IsNotEmpty ( ) phoneNumber ? : string ; @ IsString ( ) @ IsOptional ( ) firstName ? : string ; @ IsString ( ) @ IsOptional ( ) lastName ? : string ; @ IsString ( ) @ IsOptional ( ) defaultMessage ? : string ; @ IsBoolean ( ) @ IsOptional ( ) includeDefaultMessage ? : boolean ; } export class GetCourseResponse { id ! : number ; name ! : string ; @ Type ( ( ) => OfficeHourPartial ) officeHours ! : Array < OfficeHourPartial > ; @ Type ( ( ) => QueuePartial ) queues ! : QueuePartial [ ] ; coordinator_email ! : string ; heatmap ! : Heatmap | false ; selfEnroll ! : boolean ; } export class GetSelfEnrollResponse { courses ! : CoursePartial [ ] ; } export class GetCourseOverridesRow { id ! : number ; role ! : string ; name ! : string ; email ! : string ; } export class GetCourseOverridesResponse { @ Type ( ( ) => GetCourseOverridesRow ) data ! : GetCourseOverridesRow [ ] ; } export class UpdateCourseOverrideBody { @ IsString ( ) email ! : string ; @ IsString ( ) role ! : Role ; } export class UpdateCourseOverrideResponse extends GetCourseOverridesRow { } export class GetQueueResponse extends QueuePartial { } export class GetCourseQueuesResponse extends Array < QueuePartial > { } export class ListQuestionsResponse { @ Type ( ( ) => Question ) yourQuestion ? : Question ; @ Type ( ( ) => Question ) questionsGettingHelp ! : Array < Question > ; @ Type ( ( ) => Question ) queue ! : Array < Question > ; @ Type ( ( ) => Question ) priorityQueue ! : Array < Question > ; @ Type ( ( ) => QuestionGroup ) groups ! : Array < QuestionGroup > ; } export class GetQuestionResponse extends Question { } export class GetStudentQuestionResponse extends Question { @ IsInt ( ) queueId ! : number ; } export class CreateQuestionParams { @ IsString ( ) text ! : string ; @ IsEnum ( QuestionType ) @ IsOptional ( ) questionType ? : QuestionType ; @ IsBoolean ( ) groupable ! : boolean ; @ IsInt ( ) queueId ! : number ; @ IsBoolean ( ) @ IsOptional ( ) isOnline ? : boolean ; @ IsString ( ) @ IsOptional ( ) location ? : string ; @ IsBoolean ( ) force ! : boolean ; } export class CreateQuestionResponse extends Question { } export class UpdateQuestionParams { @ IsString ( ) @ IsOptional ( ) text ? : string ; @ IsEnum ( QuestionType ) @ IsOptional ( ) questionType ? : QuestionType ; @ IsBoolean ( ) @ IsOptional ( ) groupable ? : boolean ; @ IsInt ( ) @ IsOptional ( ) queueId ? : number ; @ IsEnum ( QuestionStatusKeys ) @ IsOptional ( ) status ? : QuestionStatus ; @ IsBoolean ( ) @ IsOptional ( ) isOnline ? : boolean ; @ IsString ( ) @ IsOptional ( ) location ? : string ; } export class UpdateQuestionResponse extends Question { } export class GroupQuestionsParams { @ IsArray ( ) @ Type ( ( ) => Number ) questionIds ! : number [ ] ; @ IsInt ( ) queueId ! : number ; } export class ResolveGroupParams { @ IsInt ( ) queueId ! : number ; } export type TAUpdateStatusResponse = QueuePartial ; export type QueueNotePayloadType = { notes : string ; } ; export class TACheckoutResponse { queueId ! : number ; canClearQueue ! : boolean ; @ Type ( ( ) => Date ) nextOfficeHourTime ? : Date ; } export class UpdateQueueParams { @ IsString ( ) @ IsOptional ( ) notes ? : string ; @ IsBoolean ( ) allowQuestions ? : boolean ; } export class TACheckinTimesResponse { @ Type ( ( ) => TACheckinPair ) taCheckinTimes ! : TACheckinPair [ ] ; } export class TACheckinPair { @ IsString ( ) name ! : string ; @ IsDate ( ) @ Type ( ( ) => Date ) checkinTime ! : Date ; @ IsDate ( ) @ Type ( ( ) => Date ) checkoutTime ? : Date ; @ IsBoolean ( ) forced ! : boolean ; @ IsBoolean ( ) inProgress ! : boolean ; @ IsNumber ( ) numHelped ! : number ; } export enum AlertType { REPHRASE_QUESTION = "rephraseQuestion" } export class AlertPayload { } export class Alert { @ IsEnum ( AlertType ) alertType ! : AlertType ; @ IsDate ( ) sent ! : Date ; @ Type ( ( ) => AlertPayload ) payload ! : AlertPayload ; @ IsInt ( ) id ! : number ; } export class RephraseQuestionPayload extends AlertPayload { @ IsInt ( ) questionId ! : number ; @ IsInt ( ) queueId ! : number ; @ IsInt ( ) courseId ! : number ; } export class CreateAlertParams { @ IsEnum ( AlertType ) alertType ! : AlertType ; @ IsInt ( ) courseId ! : number ; @ IsObject ( ) payload ! : AlertPayload ; @ IsInt ( ) targetUserId ! : number ; } export class CreateAlertResponse extends Alert { } export class GetAlertsResponse { @ Type ( ( ) => Alert ) alerts ! : Alert [ ] ; } param sectionGroupName param name param semester param iCalURL param coordinator_email param timezone export class RegisterCourseParams { @ IsString ( ) sectionGroupName ! : string ; @ IsString ( ) name ! : string ; @ IsString ( ) iCalURL ! : string ; @ IsString ( ) coordinator_email ! : string ; @ IsString ( ) timezone ! : string ; } export class SemesterPartial { id ! : number ; season ! : string ; year ! : number ; } export class SSEQueueResponse { queue ? : GetQueueResponse ; questions ? : ListQuestionsResponse ; } export interface TwilioBody { ToCountry : string ; ToState : string ; SmsMessageSid : string ; NumMedia : string ; ToCity : string ; FromZip : string ; SmsSid : string ; FromState : string ; SmsStatus : string ; FromCity : string ; Body : string ; FromCountry : string ; To : string ; ToZip : string ; NumSegments : string ; MessageSid : string ; AccountSid : string ; From : string ; ApiVersion : string ; } export interface GetReleaseNotesResponse { releaseNotes : unknown ; lastUpdatedUnixTime : number ; } export type GetInsightOutputResponse = PossibleOutputTypes ; export type ListInsightsResponse = Record < string , InsightDisplayInfo > ; export type InsightDisplayInfo = { displayName : string ; description : string ; component : InsightComponent ; size : "small" | "default" ; } ; export interface InsightObject { displayName : string ; description : string ; roles : Role [ ] ; component : InsightComponent ; size : "default" | "small" ; compute : ( insightFilters : any ) => Promise < PossibleOutputTypes > ; } export enum InsightComponent { SimpleDisplay = "SimpleDisplay" , BarChart = "BarChart" , SimpleTable = "SimpleTable" } export type PossibleOutputTypes = SimpleDisplayOutputType | BarChartOutputType | SimpleTableOutputType ; export type SimpleDisplayOutputType = number | string ; export type BarChartOutputType = { data : StringMap < number > [ ] ; xField : string ; yField : string ; seriesField : string ; xAxisName ? : string ; yAxisName ? : string ; } ; export type SimpleTableOutputType = { dataSource : StringMap < string > [ ] ; columns : StringMap < string > [ ] ; } ; export type StringMap < T > = { [ key ] : T ; } ; export type DateRangeType = { start : string ; end : string ; } ; export const ERROR_MESSAGES = { courseController : { checkIn : { cannotCreateNewQueueIfNotProfessor : "You can't create a new queue if you're not a professor" , cannotCheckIntoMultipleQueues : "Cannot check into multiple queues at the same time" , } , courseAlreadyRegistered : "One or more of the courses is already registered" , courseNotFound : "The course was not found" , sectionGroupNotFound : "One or more of the section groups was not found" , courseOfficeHourError : "Unable to find a course's office hours" , courseHeatMapError : "Unable to get course's cached heatmap" , courseModelError : "Course Model not found" , noUserFound : "No user found with given email" , noSemesterFound : "No semester exists for the submitted course" , updatedQueueError : "Error updating a course queue" , queuesNotFound : "Queues not found" , queueNotFound : "Queue not found" , saveQueueError : "Unable to save queue" , clearQueueError : "Unable to determine if queue can be cleared" , createEventError : "An error occurred while creating an event" , icalCalendarUpdate : "Unable to update calendar" , checkInTime : "Unable to get TA check in times" , removeCourse : "Error occurred while trying to remove a course" , createCourse : "Error occurred while trying to create a course" , updateCourse : "Error occurred while trying to update a course" , createCourseMappings : "Unable to create a course mappings" , updateProfLastRegistered : "Unable to update professor's last registered semester" , invalidApplyURL : "You are unauthorized to submit an application. Please email help@khouryofficehours.com for the correct URL." , } , questionController : { createQuestion : { invalidQueue : "Posted to an invalid queue" , noNewQuestions : "Queue not allowing new questions" , closedQueue : "Queue is closed" , oneQuestionAtATime : "You can't create more than one question at a time." , } , updateQuestion : { fsmViolation : ( role , questionStatus , bodyStatus ) : string => `${ role } cannot change status from ${ questionStatus } to ${ bodyStatus }` , taOnlyEditQuestionStatus : "TA/Professors can only edit question status" , otherTAHelping : "Another TA is currently helping with this question" , otherTAResolved : "Another TA has already resolved this question" , taHelpingOther : "TA is already helping someone else" , loginUserCantEdit : "Logged-in user does not have edit access" , } , groupQuestions : { notGroupable : "One or more of the questions is not groupable" , } , saveQError : "Unable to save a question" , notFound : "Question not found" , unableToNotifyUser : "Unable to notify user" , } , loginController : { receiveDataFromKhoury : "Invalid request signature" , invalidPayload : "The decoded JWT payload is invalid" , invalidTempJWTToken : "Error occurred while signing a JWT token" , addUserFromKhoury : "Error occurred while translating account from Khoury to Office Hours" , } , notificationController : { messageNotFromTwilio : "Message not from Twilio" , } , notificationService : { registerPhone : "phone number invalid" , } , questionRoleGuard : { questionNotFound : "Question not found" , queueOfQuestionNotFound : "Cannot find queue of question" , queueDoesNotExist : "This queue does not exist!" , } , queueController : { getQueue : "An error occurred while trying to retrieve a Queue" , getQuestions : "Unable to get questions from queue" , saveQueue : "Unable to save queue" , cleanQueue : "Unable to clean queue" , } , queueRoleGuard : { queueNotFound : "Queue not found" , } , releaseNotesController : { releaseNotesTime : ( e ) : string => "Error Parsing release notes time: " + e , } , insightsController : { insightUnathorized : "User is not authorized to view this insight" , insightNameNotFound : "The insight requested was not found" , } , roleGuard : { notLoggedIn : "Must be logged in" , noCourseIdFound : "No courseid found" , notInCourse : "Not In This Course" , mustBeRoleToJoinCourse : ( roles ) : string => `You must have one of roles [${ roles . join ( ", " ) }] to access this course` , } , profileController : { accountNotAvailable : "The user account is undefined" , userResponseNotFound : "The user response was not found" , noDiskSpace : "There is no disk space left to store an image. Please immediately contact your course staff and let them know. They will contact the Khoury Office Hours team as soon as possible." , } , alertController : { duplicateAlert : "This alert has already been sent" , notActiveAlert : "This is not an alert that's open for the current user" , } , sseService : { getSubClient : "Unable to get the redis subscriber client" , getDBClient : "Unable to get the redis database client" , getPubClient : "Unable to get publisher client" , moduleDestroy : "Unable to destroy the redis module" , cleanupConnection : "Unable to cleanup the connection" , clientIdSubscribe : "Client ID not found during subscribing to client" , subscribe : "Unable to subscribe to the client" , unsubscribe : "Unable to unsubscribe from the client" , removeFromRoom : "Error removing from redis room" , directConnections : "Unable to cleanup direct connections" , roomMembers : "Unable to get room members" , serialize : "Unable to serialize payload" , publish : "Publisher client is unable to publish" , clientIdNotFound : "Client ID not found during subscribing to client" , } , } ;