Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

linter: refactor all dependecy circles #878

Merged
merged 13 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions common/chat/conversation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import dotenv from 'dotenv';
import { v4 as uuidv4 } from 'uuid';
// eslint-disable-next-line import/no-cycle
import { checkResponseStatus, convertConversationInfosToString, createOneOnOneId, userIdToTalkJsId } from './helper';
import { User } from '../user';
import { User, userForPupil, userForStudent } from '../user';
import { getPupil, getStudent } from '../../graphql/util';
import { AllConversations, ChatAccess, ChatType, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types';
import { getLogger } from '../logger/logger';
import assert from 'assert';
Expand Down Expand Up @@ -76,6 +76,16 @@ const getConversation = async (conversationId: string): Promise<TJConversation |
}
};

const getMatcheeConversation = async (matchees: { studentId: number; pupilId: number }): Promise<{ conversation: Conversation; conversationId: string }> => {
const student = await getStudent(matchees.studentId);
const pupil = await getPupil(matchees.pupilId);
const studentUser = userForStudent(student);
const pupilUser = userForPupil(pupil);
const conversationId = createOneOnOneId(studentUser, pupilUser);
const conversation = await getConversation(conversationId);
return { conversation, conversationId };
};

const getAllConversations = async (): Promise<AllConversations> => {
assert(TALKJS_SECRET_KEY, `No TalkJS secret key found to get all conversations.`);
assureChatFeatureActive();
Expand Down Expand Up @@ -335,6 +345,7 @@ export {
markConversationAsWriteable,
sendSystemMessage,
getConversation,
getMatcheeConversation,
getAllConversations,
deleteConversation,
markConversationAsReadOnlyForPupils,
Expand Down
15 changes: 13 additions & 2 deletions common/chat/create.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { User } from '../user';
import { ChatMetaData, ContactReason, Conversation, ConversationInfos, FinishedReason, SystemMessage, TJConversation } from './types';
import { checkChatMembersAccessRights, convertTJConversation, createOneOnOneId } from './helper';
import { checkChatMembersAccessRights, convertTJConversation, createOneOnOneId, userIdToTalkJsId } from './helper';
import { createConversation, getConversation, markConversationAsWriteable, sendSystemMessage, updateConversation } from './conversation';
import { getOrCreateChatUser } from './user';
import { prisma } from '../prisma';
import { getMyContacts } from './contacts';
import systemMessages from './localization';
import { getLogger } from '../logger/logger';
import assert from 'assert';
import { createHmac } from 'crypto';

const logger = getLogger('Chat');
const getOrCreateOneOnOneConversation = async (
Expand Down Expand Up @@ -63,6 +65,15 @@ async function ensureChatUsersExist(participants: [User, User] | User[]): Promis
);
}

const createChatSignature = async (user: User): Promise<string> => {
const TALKJS_SECRET_KEY = process.env.TALKJS_API_KEY;
assert(TALKJS_SECRET_KEY, `No TalkJS secret key to create a chat signature for user ${user.userID}.`);
const userId = (await getOrCreateChatUser(user)).id;
const key = TALKJS_SECRET_KEY;
const hash = createHmac('sha256', key).update(userIdToTalkJsId(userId));
return hash.digest('hex');
};

async function handleExistingConversation(
conversationId: string,
reason: ContactReason,
Expand Down Expand Up @@ -195,4 +206,4 @@ async function createContactChat(meUser: User, contactUser: User): Promise<strin
return conversation.id;
}

export { getOrCreateOneOnOneConversation, getOrCreateGroupConversation, createContactChat };
export { getOrCreateOneOnOneConversation, getOrCreateGroupConversation, createContactChat, createChatSignature };
30 changes: 1 addition & 29 deletions common/chat/helper.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
import { match } from '@prisma/client';
import { prisma } from '../prisma';
import { User, getUser, userForPupil, userForStudent } from '../user';
// eslint-disable-next-line import/no-cycle
import { getOrCreateChatUser } from './user';
import { User, getUser } from '../user';
import { sha1 } from 'object-hash';
import { truncate } from 'lodash';
import { createHmac } from 'crypto';
import { Subcourse } from '../../graphql/generated';
import { getPupil, getStudent } from '../../graphql/util';
// eslint-disable-next-line import/no-cycle
import { getConversation } from './conversation';
import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types';
import { type MatchContactPupil, type MatchContactStudent } from './contacts';
import assert from 'assert';

type TalkJSUserId = `${'pupil' | 'student'}_${number}`;
export type UserId = `${'pupil' | 'student'}/${number}`;

const TALKJS_SECRET_KEY = process.env.TALKJS_API_KEY;

const userIdToTalkJsId = (userId: string): TalkJSUserId => {
return userId.replace('/', '_') as TalkJSUserId;
};

const talkJsIdToUserId = (userId: string): UserId => {
return userId.replace('_', '/') as UserId;
};
const createChatSignature = async (user: User): Promise<string> => {
assert(TALKJS_SECRET_KEY, `No TalkJS secret key to create a chat signature for user ${user.userID}.`);
const userId = (await getOrCreateChatUser(user)).id;
const key = TALKJS_SECRET_KEY;
const hash = createHmac('sha256', key).update(userIdToTalkJsId(userId));
return hash.digest('hex');
};

function createOneOnOneId(userA: User, userB: User): string {
const userIds = JSON.stringify([userA.userID, userB.userID].sort());
Expand Down Expand Up @@ -126,16 +110,6 @@ const getMembersForSubcourseGroupChat = async (subcourse: Subcourse) => {
return members;
};

const getMatcheeConversation = async (matchees: { studentId: number; pupilId: number }): Promise<{ conversation: Conversation; conversationId: string }> => {
const student = await getStudent(matchees.studentId);
const pupil = await getPupil(matchees.pupilId);
const studentUser = userForStudent(student);
const pupilUser = userForPupil(pupil);
const conversationId = createOneOnOneId(studentUser, pupilUser);
const conversation = await getConversation(conversationId);
return { conversation, conversationId };
};

const countChatParticipants = (conversation: Conversation): number => {
return Object.keys(conversation.participants).length;
};
Expand Down Expand Up @@ -210,11 +184,9 @@ export {
talkJsIdToUserId,
parseUnderscoreToSlash,
checkResponseStatus,
createChatSignature,
getMatchByMatchees,
createOneOnOneId,
countChatParticipants,
getMatcheeConversation,
checkChatMembersAccessRights,
isSubcourseParticipant,
getMembersForSubcourseGroupChat,
Expand Down
1 change: 0 additions & 1 deletion common/chat/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import dotenv from 'dotenv';
// eslint-disable-next-line import/no-cycle
import { checkResponseStatus, userIdToTalkJsId } from './helper';
import { User as TalkJsUser } from 'talkjs/all';
import { User } from '../user';
Expand Down
6 changes: 2 additions & 4 deletions common/courses/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { prisma } from '../prisma';
import { getLogger } from '../logger/logger';
import * as Notification from '../../common/notification';
import { getFullName, userForPupil, userForStudent } from '../user';
import { userForPupil } from '../user';
import * as Prisma from '@prisma/client';
import { getFirstLecture } from './lectures';
import { parseSubjectString } from '../util/subjectsutils';
// eslint-disable-next-line import/no-cycle
import { getCourseCapacity, getCourseFreePlaces, isParticipant } from './participants';
import { getCourseImageURL } from './util';
import { getCourseCapacity, getCourseFreePlaces, getCourseImageURL } from './util';
import { getCourse } from '../../graphql/util';
import { shuffleArray } from '../../common/util/basic';
import { NotificationContext } from '../notification/types';
Expand Down
13 changes: 0 additions & 13 deletions common/courses/participants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { addParticipant } from '../chat';
import { ChatType } from '../chat/types';
import { isChatFeatureActive } from '../chat/util';
import { getCourseOfSubcourse, getSubcourseInstructors } from './util';
// eslint-disable-next-line import/no-cycle
import { getNotificationContextForSubcourse } from '../courses/notifications';

const delay = (time: number) => new Promise((res) => setTimeout(res, time));
Expand Down Expand Up @@ -319,15 +318,3 @@ export async function fillSubcourse(subcourse: Subcourse) {
}
}
}

export async function getCourseParticipantCount(subcourse: Subcourse) {
return await prisma.subcourse_participants_pupil.count({ where: { subcourseId: subcourse.id } });
}

export async function getCourseCapacity(subcourse: Subcourse) {
return (await getCourseParticipantCount(subcourse)) / (subcourse.maxParticipants || 1);
}

export async function getCourseFreePlaces(subcourse: Subcourse) {
return Math.max(0, subcourse.maxParticipants - (await getCourseParticipantCount(subcourse)));
}
14 changes: 13 additions & 1 deletion common/courses/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { course as Course, subcourse as Subcourse } from '@prisma/client';
import { subcourse as Subcourse } from '@prisma/client';
import { accessURLForKey } from '../file-bucket';
import { join } from 'path';
import { prisma } from '../prisma';
Expand Down Expand Up @@ -33,3 +33,15 @@ export async function getCourseOfSubcourse(subcourse: Subcourse) {
where: { id: subcourse.courseId },
});
}

export async function getCourseParticipantCount(subcourse: Subcourse) {
return await prisma.subcourse_participants_pupil.count({ where: { subcourseId: subcourse.id } });
}

export async function getCourseCapacity(subcourse: Subcourse) {
return (await getCourseParticipantCount(subcourse)) / (subcourse.maxParticipants || 1);
}

export async function getCourseFreePlaces(subcourse: Subcourse) {
return Math.max(0, subcourse.maxParticipants - (await getCourseParticipantCount(subcourse)));
}
1 change: 0 additions & 1 deletion common/match/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { v4 as generateUUID } from 'uuid';
import { getPupilGradeAsString } from '../pupil';
import * as Notification from '../notification';
import { removeInterest } from './interest';
// eslint-disable-next-line import/no-cycle
import { getJitsiTutoringLink, getMatchHash, getOverlappingSubjects } from './util';
import { getLogger } from '../../common/logger/logger';
import { PrerequisiteError } from '../util/error';
Expand Down
21 changes: 3 additions & 18 deletions common/match/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { prisma } from '../prisma';
import type { Prisma, pupil as Pupil, student as Student } from '@prisma/client';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The matching algorithm is optional, to allow for slim local setups
import type { Helpee, Helper, Settings, SubjectWithGradeRestriction } from 'corona-school-matching';
// eslint-disable-next-line import/no-cycle
import type { Helpee, Helper, Settings } from 'corona-school-matching';
import { createMatch } from './create';
import { parseSubjectString, Subject } from '../util/subjectsutils';
import { parseSubjectString } from '../util/subjectsutils';
import { gradeAsInt } from '../util/gradestrings';
import { assertExists } from '../util/basic';
import { getLogger } from '../logger/logger';
Expand All @@ -14,11 +13,7 @@ import { cleanupUnconfirmed, InterestConfirmationStatus, requestInterestConfirma
import { userSearch } from '../user/search';
import { addPupilScreening } from '../pupil/screening';
import assert from 'assert';

export const DEFAULT_TUTORING_GRADERESTRICTIONS = {
MIN: 1,
MAX: 13,
};
import { formattedSubjectToSubjectWithGradeRestriction } from './util';

const logger = getLogger('MatchingPool');

Expand Down Expand Up @@ -151,16 +146,6 @@ async function pupilToHelpee(pupil: Pupil): Promise<Helpee> {
};
}

function formattedSubjectToSubjectWithGradeRestriction(subject: Subject): SubjectWithGradeRestriction {
return {
name: subject.name,
gradeRestriction: {
min: subject.grade?.min ?? DEFAULT_TUTORING_GRADERESTRICTIONS.MIN, //due to a screening tool's bug (or how it is designed), those values may be null (which causes the algorithm to fail)
max: subject.grade?.max ?? DEFAULT_TUTORING_GRADERESTRICTIONS.MAX,
},
};
}

const INTEREST_CONFIRMATION_TOGGLES = ['confirmation-success', 'confirmation-pending', 'confirmation-unknown'] as const;
type InterestConfirmationToggle = (typeof INTEREST_CONFIRMATION_TOGGLES)[number];

Expand Down
22 changes: 19 additions & 3 deletions common/match/util.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { match as Match, pupil as Pupil, student as Student } from '@prisma/client';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The matching algorithm is optional, to allow for slim local setups
import type { SubjectWithGradeRestriction } from 'corona-school-matching';
import { prisma } from '../prisma';
import { parseSubjectString } from '../util/subjectsutils';
import { parseSubjectString, Subject } from '../util/subjectsutils';
import { gradeAsInt } from '../util/gradestrings';
import { hashToken } from '../util/hashing';
// eslint-disable-next-line import/no-cycle
import { DEFAULT_TUTORING_GRADERESTRICTIONS } from './pool';

export const DEFAULT_TUTORING_GRADERESTRICTIONS = {
MIN: 1,
MAX: 13,
};

export function formattedSubjectToSubjectWithGradeRestriction(subject: Subject): SubjectWithGradeRestriction {
return {
name: subject.name,
gradeRestriction: {
min: subject.grade?.min ?? DEFAULT_TUTORING_GRADERESTRICTIONS.MIN, //due to a screening tool's bug (or how it is designed), those values may be null (which causes the algorithm to fail)
max: subject.grade?.max ?? DEFAULT_TUTORING_GRADERESTRICTIONS.MAX,
},
};
}

export function getJitsiTutoringLink(match: Match) {
return `https://meet.jit.si/CoronaSchool-${encodeURIComponent(match.uuid)}`;
Expand Down
3 changes: 1 addition & 2 deletions common/notification/channels/mailjet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import * as assert from 'assert';
import { AttachmentGroup } from '../../attachments';
import { isDev } from '../../util/environment';
import { User } from '../../user';
// eslint-disable-next-line import/no-cycle
import { createSecretEmailToken } from '../../secret';
import moment from 'moment';
import { createSecretEmailToken } from '../../secret/emailToken';

// ------------ Mailjet Interface -------------------------------

Expand Down
22 changes: 0 additions & 22 deletions common/notification/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@

import { student as Student, pupil as Pupil } from '@prisma/client';
import { getPupil, getStudent, User } from '../user';
// eslint-disable-next-line import/no-cycle
import { getNotifications } from './notification';
import { prisma } from '../prisma';
import { ActionID, ConcreteNotificationState } from './types';

type NotificationHook = { fn: (user: User) => Promise<void>; description: string };

Expand Down Expand Up @@ -40,21 +36,3 @@ export const registerStudentHook = (hookID: string, description: string, hook: (

export const registerPupilHook = (hookID: string, description: string, hook: (pupil: Pupil) => Promise<void>) =>
registerHook(hookID, description, (user) => getPupil(user).then(hook));

// Predicts when a hook will run for a certain user as caused by a certain action
// i.e. 'When will a user by deactivated (hook) due to Certificate of Conduct reminders (action) ?'
// Returns null if no date is known or hook was already triggered
export async function predictedHookActionDate(action: ActionID, hookID: string, user: User): Promise<Date | null> {
const viableNotifications = ((await getNotifications()).get(action)?.toSend ?? []).filter((it) => it.hookID === hookID);

const possibleTrigger = await prisma.concrete_notification.findFirst({
where: {
state: ConcreteNotificationState.DELAYED,
userId: user.userID,
notificationID: { in: viableNotifications.map((it) => it.id) },
},
select: { sentAt: true },
});

return possibleTrigger?.sentAt;
}
21 changes: 20 additions & 1 deletion common/notification/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-cycle
import { mailjetChannel } from './channels/mailjet';
import { NotificationID, NotificationContext, Context, Notification, ConcreteNotification, ConcreteNotificationState, Channel } from './types';
import { prisma } from '../prisma';
Expand Down Expand Up @@ -345,6 +344,26 @@ export async function cancelDraftedAndDelayed(notification: Notification, contex
logger.info(`Cancelled ${publishedCount} drafted notifications for Notification(${notification.id})`);
}

/* -------------------------------- Hook ----------------------------------------------------------- */

// Predicts when a hook will run for a certain user as caused by a certain action
// i.e. 'When will a user by deactivated (hook) due to Certificate of Conduct reminders (action) ?'
// Returns null if no date is known or hook was already triggered
export async function predictedHookActionDate(action: ActionID, hookID: string, user: User): Promise<Date | null> {
const viableNotifications = ((await getNotifications()).get(action)?.toSend ?? []).filter((it) => it.hookID === hookID);

const possibleTrigger = await prisma.concrete_notification.findFirst({
where: {
state: ConcreteNotificationState.DELAYED,
userId: user.userID,
notificationID: { in: viableNotifications.map((it) => it.id) },
},
select: { sentAt: true },
});

return possibleTrigger?.sentAt;
}

export * from './hook';

/* -------------------------------- Public API exposed to other components ----------------------------------------------------------- */
Expand Down
6 changes: 2 additions & 4 deletions common/notification/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
New notifications can be created / modified at runtime. This module contains various utilities to do that */

import { prisma } from '../prisma';
import { Context, Notification, NotificationID, NotificationMessage, NotificationRecipient } from './types';
import { Context, Notification, NotificationID, NotificationRecipient } from './types';
import { Prisma } from '@prisma/client';
import { getLogger } from '../../common/logger/logger';
// eslint-disable-next-line import/no-cycle
import { hookExists } from './hook';
import { ActionID, getNotificationActions, sampleUser } from './actions';
import { MessageTemplateType } from '../../graphql/types/notificationMessage';
import { getNotificationActions, sampleUser } from './actions';
import { NotificationUpdateInput } from '../../graphql/generated';
import { USER_APP_DOMAIN } from '../util/environment';

Expand Down
1 change: 0 additions & 1 deletion common/notification/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { concrete_notification as ConcreteNotification, notification as Notification, student as Student, pupil as Pupil } from '.prisma/client';
import { AttachmentGroup } from '../attachments';
import { User } from '../user';
// eslint-disable-next-line import/no-cycle
import { ActionID } from './actions';

export type NotificationID = number; // either our own or we reuse them from Mailjet. Maybe we can structure them a bit better
Expand Down
Loading