From 80bebc263bec4fd6604698c9b487edcffd3cea1e Mon Sep 17 00:00:00 2001 From: Lomy Date: Fri, 2 Jun 2023 17:55:46 -0400 Subject: [PATCH 01/31] add participant to chat on subcourse join --- common/chat/conversation.ts | 7 +++++-- graphql/subcourse/mutations.ts | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index e61357d98..ff07e5d87 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -217,7 +217,7 @@ async function deleteConversation(conversationId: string): Promise { } } -async function addParticipant(user: User, conversationId: string): Promise { +async function addParticipant(user: User, conversationId: string, chatType?: 'normal' | 'announcement'): Promise { const userId = userIdToTalkJsId(user.userID); try { const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}/participants/${userId}`, { @@ -226,6 +226,9 @@ async function addParticipant(user: User, conversationId: string): Promise Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, + body: JSON.stringify({ + access: chatType === 'normal' ? 'Readwrite' : 'Read', + }), }); await checkResponseStatus(response); } catch (error) { @@ -249,7 +252,7 @@ async function removeParticipant(user: User, conversationId: string): Promise { +async function markConversationAsReadOnly(conversationId: string, reason?: 'announcement' | 'deactivate'): Promise { try { const conversation = await getConversation(conversationId); const participantIds = Object.keys(conversation.participants); diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index f26c2525a..7073f8900 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -28,6 +28,8 @@ import * as GraphQLModel from '../generated/models'; import { getCourse, getLecture, getPupil, getStudent, getSubcourse } from '../util'; import { validateEmail } from '../validators'; import { chat_type } from '../generated'; +import { addParticipant } from '../../common/chat/conversation'; +import { ChatType } from '../../common/chat/types'; const logger = getLogger('MutateCourseResolver'); @@ -332,6 +334,7 @@ export class MutateSubcourseResolver { const subcourse = await getSubcourse(subcourseId); await joinSubcourse(subcourse, pupil, true); await addGroupAppointmentsParticipant(subcourseId, user.userID); + await addParticipant(user, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? 'announcement' : 'normal'); return true; } From d1b4b972e913884df1be111116d948007b7db6f4 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 7 Jun 2023 10:37:43 -0400 Subject: [PATCH 02/31] mark matchee convo as readonly or writeable --- common/chat/helper.ts | 14 +++++++++++++- graphql/match/mutations.ts | 12 +++++++++++- graphql/subcourse/mutations.ts | 4 ++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 044e54ef4..8f4c48519 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -1,11 +1,13 @@ import { match } from '@prisma/client'; import { prisma } from '../prisma'; -import { User, getUser } from '../user'; +import { User, getUser, userForPupil, userForStudent } from '../user'; import { getOrCreateChatUser } from './user'; import { sha1 } from 'object-hash'; import { truncate } from 'lodash'; import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; +import { Conversation, getConversation } from './conversation'; +import { getPupil, getStudent } from '../../graphql/util'; const userIdToTalkJsId = (userId: string): string => { return userId.replace('/', '_'); @@ -114,6 +116,15 @@ 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 = getConversationId([studentUser, pupilUser]); + const conversation = await getConversation(conversationId); + return { conversation, conversationId }; +}; export { userIdToTalkJsId, parseUnderscoreToSlash, @@ -122,6 +133,7 @@ export { getMatchByMatchees, createOneOnOneId, getConversationId, + getMatcheeConversation, checkIfSubcourseParticipation, getMembersForSubcourseGroupChat, }; diff --git a/graphql/match/mutations.ts b/graphql/match/mutations.ts index b32cfb2be..fb9214a29 100644 --- a/graphql/match/mutations.ts +++ b/graphql/match/mutations.ts @@ -8,6 +8,9 @@ import { createMatch } from '../../common/match/create'; import { GraphQLContext } from '../context'; import { ConcreteMatchPool, pools } from '../../common/match/pool'; import { removeInterest } from '../../common/match/interest'; +import { getConversationId, getMatcheeConversation } from '../../common/chat/helper'; +import { userForPupil, userForStudent } from '../../common/user'; +import { getConversation, markConversationAsReadOnly, markConversationAsWriteable } from '../../common/chat'; @Resolver((of) => GraphQLModel.Match) export class MutateMatchResolver { @@ -34,7 +37,11 @@ export class MutateMatchResolver { await hasAccess(context, 'Match', match); await dissolveMatch(match, dissolveReason, /* dissolver:*/ null); + const { conversation, conversationId } = await getMatcheeConversation({ studentId: match.studentId, pupilId: match.pupilId }); + if (conversation) { + await markConversationAsReadOnly(conversationId); + } return true; } @@ -43,9 +50,12 @@ export class MutateMatchResolver { async matchReactivate(@Ctx() context: GraphQLContext, @Arg('matchId', (type) => Int) matchId: number): Promise { const match = await getMatch(matchId); await hasAccess(context, 'Match', match); - await reactivateMatch(match); + const { conversation, conversationId } = await getMatcheeConversation({ studentId: match.studentId, pupilId: match.pupilId }); + if (conversation) { + await markConversationAsWriteable(conversationId); + } return true; } } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 7073f8900..c54b4d7bd 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -335,6 +335,7 @@ export class MutateSubcourseResolver { await joinSubcourse(subcourse, pupil, true); await addGroupAppointmentsParticipant(subcourseId, user.userID); await addParticipant(user, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? 'announcement' : 'normal'); + return true; } @@ -350,6 +351,7 @@ export class MutateSubcourseResolver { const subcourse = await getSubcourse(subcourseId); await joinSubcourse(subcourse, pupil, false); await addGroupAppointmentsParticipant(subcourseId, user.userID); + await addParticipant(user, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? 'announcement' : 'normal'); return true; } @@ -381,6 +383,7 @@ export class MutateSubcourseResolver { // Joining the subcourse will automatically remove the pupil from the waitinglist await joinSubcourse(subcourse, pupil, true); await addGroupAppointmentsParticipant(subcourseId, user.userID); + await addParticipant(user, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? 'announcement' : 'normal'); return true; } @@ -399,6 +402,7 @@ export class MutateSubcourseResolver { await leaveSubcourse(subcourse, pupil); await removeGroupAppointmentsParticipant(subcourse.id, user.userID); + return true; } From db94d66e3f6365cbae1c047a6f76d36d58e292ec Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 7 Jun 2023 11:04:38 -0400 Subject: [PATCH 03/31] subcourse purposes --- graphql/subcourse/mutations.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 5d6ec1da1..62126b5b9 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -16,7 +16,7 @@ import { Student } from '../../common/entity/Student'; import { getLogger } from '../../common/logger/logger'; import { sendGuestInvitationMail, sendPupilCoursePromotion } from '../../common/mails/courses'; import { prisma } from '../../common/prisma'; -import { getUserIdTypeORM, getUserTypeORM } from '../../common/user'; +import { getUserIdTypeORM, getUserTypeORM, userForPupil, userForStudent } from '../../common/user'; import { createBBBMeeting, createOrUpdateCourseAttendanceLog, getMeetingUrl, isBBBMeetingRunning, startBBBMeeting } from '../../common/util/bbb'; import { PrerequisiteError } from '../../common/util/error'; import { getSessionPupil, getSessionStudent, isElevated, isSessionPupil, isSessionStudent } from '../authentication'; @@ -28,7 +28,7 @@ import * as GraphQLModel from '../generated/models'; import { getCourse, getLecture, getPupil, getStudent, getSubcourse } from '../util'; import { validateEmail } from '../validators'; import { chat_type } from '../generated'; -import { addParticipant } from '../../common/chat/conversation'; +import { addParticipant, markConversationAsReadOnly, removeParticipant } from '../../common/chat/conversation'; import { ChatType } from '../../common/chat/types'; const logger = getLogger('MutateCourseResolver'); @@ -129,9 +129,12 @@ export class MutateSubcourseResolver { const subcourse = await getSubcourse(subcourseId); await hasAccess(context, 'Subcourse', subcourse); const newInstructor = await getStudent(studentId); + const newInstructorUser = userForStudent(newInstructor); const studentUserId = getUserIdTypeORM(newInstructor); await prisma.subcourse_instructors_student.create({ data: { subcourseId, studentId } }); await addGroupAppointmentsOrganizer(subcourseId, studentUserId); + await addParticipant(newInstructorUser, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? 'announcement' : 'normal'); + logger.info(`Student (${studentId}) was added as an instructor to Subcourse(${subcourseId}) by User(${context.user!.userID})`); return true; } @@ -147,9 +150,11 @@ export class MutateSubcourseResolver { const subcourse = await getSubcourse(subcourseId); await hasAccess(context, 'Subcourse', subcourse); const instructorToBeRemoved = await getStudent(studentId); + const instructorUser = userForStudent(instructorToBeRemoved); const studentUserId = getUserIdTypeORM(instructorToBeRemoved); await prisma.subcourse_instructors_student.delete({ where: { subcourseId_studentId: { subcourseId, studentId } } }); await removeGroupAppointmentsOrganizer(subcourseId, studentUserId); + await removeParticipant(instructorUser, subcourse.conversationId); logger.info(`Student(${studentId}) was deleted from Subcourse(${subcourseId}) by User(${context.user!.userID})`); return true; } @@ -195,7 +200,7 @@ export class MutateSubcourseResolver { await hasAccess(context, 'Subcourse', subcourse); await cancelSubcourse(subcourse); - + await markConversationAsReadOnly(subcourse.conversationId, 'deactivate'); return true; } @@ -397,12 +402,13 @@ export class MutateSubcourseResolver { ): Promise { const { user } = context; const pupil = await getSessionPupil(context, pupilId); + const pupilUser = userForPupil(pupil); const subcourse = await getSubcourse(subcourseId); await hasAccess(context, 'Subcourse', subcourse); await leaveSubcourse(subcourse, pupil); await removeGroupAppointmentsParticipant(subcourse.id, user.userID); - + await removeParticipant(pupilUser, subcourse.conversationId); return true; } From 55c2817adf132ec0f80bfeaf2781b04a56c823fe Mon Sep 17 00:00:00 2001 From: Lomy Date: Mon, 12 Jun 2023 13:51:30 +0200 Subject: [PATCH 04/31] add chat access enum --- common/chat/conversation.ts | 15 ++++++++++----- graphql/match/mutations.ts | 5 ++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index ff07e5d87..d53fe71d2 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -6,6 +6,7 @@ import { Message } from 'talkjs/all'; import { User } from '../user'; import { getOrCreateChatUser } from './user'; import { prisma } from '../prisma'; +import { ChatType } from './types'; dotenv.config(); @@ -40,7 +41,7 @@ type Conversation = { custom?: CustomProps; lastMessage?: LastMessage; participants: { - [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; + [id: string]: { access: ChatAccess; notify: boolean }; }; createdAt: number; }; @@ -52,6 +53,10 @@ type ConversationInfos = { custom: CustomProps; }; +enum ChatAccess { + READ = 'Read', + READWRITE = 'ReadWrite', +} const createConversation = async (participants: User[], conversationInfos: ConversationInfos): Promise => { let conversationId: string; const { type } = conversationInfos.custom; @@ -217,7 +222,7 @@ async function deleteConversation(conversationId: string): Promise { } } -async function addParticipant(user: User, conversationId: string, chatType?: 'normal' | 'announcement'): Promise { +async function addParticipant(user: User, conversationId: string, chatType?: ChatType): Promise { const userId = userIdToTalkJsId(user.userID); try { const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}/participants/${userId}`, { @@ -227,7 +232,7 @@ async function addParticipant(user: User, conversationId: string, chatType?: 'no 'Content-Type': 'application/json', }, body: JSON.stringify({ - access: chatType === 'normal' ? 'Readwrite' : 'Read', + access: chatType === ChatType.NORMAL ? ChatAccess.READWRITE : ChatAccess.READ, }), }); await checkResponseStatus(response); @@ -264,7 +269,7 @@ async function markConversationAsReadOnly(conversationId: string, reason?: 'anno 'Content-Type': 'application/json', }, body: JSON.stringify({ - access: 'Read', + access: ChatAccess.READ, }), }); await checkResponseStatus(response); @@ -286,7 +291,7 @@ async function markConversationAsWriteable(conversationId: string): Promise GraphQLModel.Match) export class MutateMatchResolver { From 3b699f4962578edc7289ff87a096f7769a68ef9b Mon Sep 17 00:00:00 2001 From: Lomy Date: Mon, 12 Jun 2023 14:45:46 +0200 Subject: [PATCH 05/31] check if members have write rights --- common/chat/conversation.ts | 10 +++++++++- common/chat/helper.ts | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index d53fe71d2..dfa3980be 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import dotenv from 'dotenv'; import { v4 as uuidv4 } from 'uuid'; -import { checkResponseStatus, createOneOnOneId, getConversationId, userIdToTalkJsId } from './helper'; +import { checkChatMembersAccessRights, checkResponseStatus, createOneOnOneId, getConversationId, userIdToTalkJsId } from './helper'; import { Message } from 'talkjs/all'; import { User } from '../user'; import { getOrCreateChatUser } from './user'; @@ -57,6 +57,7 @@ enum ChatAccess { READ = 'Read', READWRITE = 'ReadWrite', } + const createConversation = async (participants: User[], conversationInfos: ConversationInfos): Promise => { let conversationId: string; const { type } = conversationInfos.custom; @@ -127,6 +128,13 @@ const getOrCreateConversation = async (participants: [User, User], conversationI const conversationIdOfParticipants = getConversationId(participants); const participantsConversation = await getConversation(conversationIdOfParticipants); + const { readMembers } = checkChatMembersAccessRights(participantsConversation); + const isChatReadOnly = readMembers.length > 0; + + if (isChatReadOnly) { + await markConversationAsWriteable(conversationIdOfParticipants); + } + const updatedConversation = { id: conversationIdOfParticipants, custom: { type: conversationInfos.custom.type } }; await updateConversation(updatedConversation); diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 8f4c48519..31a06a581 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -125,6 +125,26 @@ const getMatcheeConversation = async (matchees: { studentId: number; pupilId: nu const conversation = await getConversation(conversationId); return { conversation, conversationId }; }; + +const checkChatMembersAccessRights = (conversation: Conversation): { readWriteMembers: string[]; readMembers: string[] } => { + const readWriteMembers: string[] = []; + const readMembers: string[] = []; + + for (const participantId in conversation.participants) { + const participant = conversation.participants[participantId]; + const access = participant.access; + + if (access === 'ReadWrite') { + readWriteMembers.push(participantId); + } else if (access === 'Read') { + readMembers.push(participantId); + } else { + console.log(`Teilnehmer mit der ID ${participantId} hat unbekannte Zugriffsrechte.`); + } + } + return { readWriteMembers, readMembers }; +}; + export { userIdToTalkJsId, parseUnderscoreToSlash, @@ -136,4 +156,5 @@ export { getMatcheeConversation, checkIfSubcourseParticipation, getMembersForSubcourseGroupChat, + checkChatMembersAccessRights, }; From ab3699f7dbaf933f20300dd1b9d42e2770cac265 Mon Sep 17 00:00:00 2001 From: Lomy Date: Tue, 13 Jun 2023 15:37:18 +0200 Subject: [PATCH 06/31] add new chat meta data --- common/chat/conversation.ts | 85 ++++++++++++++++++++++++------------- common/chat/helper.ts | 19 +++++++++ graphql/chat/mutations.ts | 18 ++++---- 3 files changed, 82 insertions(+), 40 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index c1511fc81..c6bf76520 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import dotenv from 'dotenv'; import { v4 as uuidv4 } from 'uuid'; -import { checkResponseStatus, createOneOnOneId, getConversationId, userIdToTalkJsId } from './helper'; +import { checkResponseStatus, convertConversationInfosToStringified, createOneOnOneId, getConversationId, userIdToTalkJsId } from './helper'; import { Message } from 'talkjs/all'; import { User } from '../user'; import { getOrCreateChatUser } from './user'; @@ -25,11 +25,13 @@ type LastMessage = { type: Message['type']; }; -type CustomProps = { - start?: string; - type: 'course' | 'match' | 'announcement' | 'participant' | 'prospect'; - finished?: 'match_dissolved' | 'course_over'; -}; +export enum ContactReason { + COURSE = 'course', + MATCH = 'match', + ANNOUNCEMENT = 'announcement', + PARTICIPANT = 'participant', + PROSPECT = 'prospect', +} type Conversation = { id: string; @@ -37,7 +39,7 @@ type Conversation = { topicId?: string; photoUrl?: string; welcomeMessages?: string[]; - custom?: CustomProps; + custom?: ChatMetaData; lastMessage?: LastMessage; participants: { [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; @@ -49,26 +51,24 @@ type ConversationInfos = { subject?: string; photoUrl?: string; welcomeMessages?: string[]; - custom: CustomProps; + custom: ChatMetaData; }; -const createConversation = async (participants: User[], conversationInfos: ConversationInfos): Promise => { +export type ChatMetaData = { + start?: string; + match?: { matchId: number }; + subcourse?: number[]; + prospectSubcourse?: number[]; + finished?: 'match_dissolved' | 'course_over'; +}; + +const createConversation = async (participants: User[], conversationInfos: ConversationInfos, type: 'oneOnOne' | 'group'): Promise => { let conversationId: string; - const { type } = conversationInfos.custom; switch (type) { - case 'match': + case 'oneOnOne': conversationId = createOneOnOneId(participants[0], participants[1]); break; - case 'participant': - conversationId = createOneOnOneId(participants[0], participants[1]); - break; - case 'prospect': - conversationId = createOneOnOneId(participants[0], participants[1]); - break; - case 'course': - conversationId = uuidv4(); - break; - case 'announcement': + case 'group': conversationId = uuidv4(); break; default: @@ -77,7 +77,15 @@ const createConversation = async (participants: User[], conversationInfos: Conve try { const body = JSON.stringify({ - ...conversationInfos, + subject: conversationInfos.subject ?? '', + welcomeMessages: conversationInfos.welcomeMessages ?? [], + custom: { + ...(conversationInfos.custom.start && { start: conversationInfos.custom.start }), + ...(conversationInfos.custom.match && { match: JSON.stringify(conversationInfos.custom.match) }), + ...(conversationInfos.custom.subcourse && { subcourse: JSON.stringify(conversationInfos.custom.subcourse) }), + ...(conversationInfos.custom.prospectSubcourse && { prospectSubcourse: JSON.stringify(conversationInfos.custom.prospectSubcourse) }), + ...(conversationInfos.custom.finished && { finished: conversationInfos.custom.finished }), + }, participants: participants.map((participant: User) => userIdToTalkJsId(participant.userID)), }); @@ -112,7 +120,15 @@ const getConversation = async (conversationId: string): Promise => { +// TODO: remove subcourse from custom prop, if subcourse cancel... + +const getOrCreateConversation = async ( + participants: [User, User], + conversationInfos: ConversationInfos, + type: 'oneOnOne' | 'group', + reason: ContactReason, + subcourseId?: number +): Promise => { // * every participants need a talk js user await Promise.all( participants.map(async (participant) => { @@ -122,12 +138,22 @@ const getOrCreateConversation = async (participants: [User, User], conversationI const conversationIdOfParticipants = getConversationId(participants); const participantsConversation = await getConversation(conversationIdOfParticipants); - // TODO: DO NOT UPDATE when the conversation does not exist yet! - const updatedConversation = { id: conversationIdOfParticipants, custom: { type: conversationInfos.custom.type } }; + const subcoursesFromConversation = participantsConversation?.custom.subcourse ?? []; + const prospectSubcoursesFromConversation = participantsConversation?.custom.prospectSubcourse ?? []; + + const updatedConversation = { + id: conversationIdOfParticipants, + custom: { + ...(reason === ContactReason.MATCH && { match: conversationInfos.custom.match }), + ...(reason === ContactReason.PARTICIPANT && { subcourse: [...subcoursesFromConversation, subcourseId] }), + ...(reason === ContactReason.PROSPECT && { prospectSubcourse: [...prospectSubcoursesFromConversation, subcourseId] }), + }, + }; + await updateConversation(updatedConversation); if (participantsConversation === undefined) { - const newConversationId = await createConversation(participants, conversationInfos); + const newConversationId = await createConversation(participants, conversationInfos, type); const newConversation = await getConversation(newConversationId); await sendSystemMessage('Willkommen im Lern-Fair Chat!', newConversationId, 'first'); return newConversation; @@ -136,7 +162,7 @@ const getOrCreateConversation = async (participants: [User, User], conversationI return participantsConversation; }; -const getOrCreateGroupConversation = async (participants: User[], subcourseId: number, conversationInfos?: ConversationInfos): Promise => { +const getOrCreateGroupConversation = async (participants: User[], subcourseId: number, conversationInfos: ConversationInfos): Promise => { await Promise.all( participants.map(async (participant) => { await getOrCreateChatUser(participant); @@ -149,7 +175,7 @@ const getOrCreateGroupConversation = async (participants: User[], subcourseId: n }); if (subcourse.conversationId === null) { - const newConversationId = await createConversation(participants, conversationInfos); + const newConversationId = await createConversation(participants, conversationInfos, 'group'); const newConversation = await getConversation(newConversationId); await sendSystemMessage('Willkommen im Lern-Fair Chat!', newConversationId, 'first'); await prisma.subcourse.update({ @@ -185,7 +211,6 @@ async function getLastUnreadConversation(user: User): Promise<{ data: Conversati */ async function updateConversation(conversationToBeUpdated: { id: string } & ConversationInfos): Promise { try { - // check if conversation exists // TODO: This does not check anything! await getConversation(conversationToBeUpdated.id); const response = await fetch(`${talkjsConversationApiUrl}/${conversationToBeUpdated.id}`, { @@ -194,7 +219,7 @@ async function updateConversation(conversationToBeUpdated: { id: string } & Conv Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, - body: JSON.stringify({ ...conversationToBeUpdated }), + body: JSON.stringify(convertConversationInfosToStringified(conversationToBeUpdated)), }); await checkResponseStatus(response); } catch (error) { diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 044e54ef4..33fd18187 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -6,6 +6,7 @@ import { sha1 } from 'object-hash'; import { truncate } from 'lodash'; import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; +import { ChatMetaData, ConversationInfos } from './conversation'; const userIdToTalkJsId = (userId: string): string => { return userId.replace('/', '_'); @@ -114,6 +115,23 @@ const getMembersForSubcourseGroupChat = async (subcourse: Subcourse) => { return members; }; +const convertConversationInfosToStringified = (conversationInfos: ConversationInfos): ConversationInfos => { + const convertedObj: ConversationInfos = { + subject: conversationInfos.subject, + photoUrl: conversationInfos.photoUrl, + welcomeMessages: conversationInfos.welcomeMessages, + custom: {} as ChatMetaData, + }; + + for (const key in conversationInfos.custom) { + if (conversationInfos.custom.hasOwnProperty(key)) { + convertedObj.custom[key] = JSON.stringify(conversationInfos.custom[key]); + } + } + + return convertedObj; +}; + export { userIdToTalkJsId, parseUnderscoreToSlash, @@ -124,4 +142,5 @@ export { getConversationId, checkIfSubcourseParticipation, getMembersForSubcourseGroupChat, + convertConversationInfosToStringified, }; diff --git a/graphql/chat/mutations.ts b/graphql/chat/mutations.ts index 6f7900eae..27ecaf98b 100644 --- a/graphql/chat/mutations.ts +++ b/graphql/chat/mutations.ts @@ -4,7 +4,7 @@ import { GraphQLContext } from '../context'; import { AuthorizedDeferred, hasAccess } from '../authorizations'; import { getLogger } from '../../common/logger/logger'; import { prisma } from '../../common/prisma'; -import { ConversationInfos, getOrCreateConversation, getOrCreateGroupConversation } from '../../common/chat'; +import { ContactReason, ConversationInfos, getOrCreateConversation, getOrCreateGroupConversation } from '../../common/chat'; import { User, getUser } from '../../common/user'; import { checkIfSubcourseParticipation, getMatchByMatchees, getMembersForSubcourseGroupChat } from '../../common/chat/helper'; @@ -23,29 +23,29 @@ export class MutateChatResolver { const conversationInfos: ConversationInfos = { custom: { - type: 'match', + match: { matchId: match.id }, }, }; - const conversation = await getOrCreateConversation(matchees, conversationInfos); + const conversation = await getOrCreateConversation(matchees, conversationInfos, 'oneOnOne', ContactReason.MATCH); return conversation.id; } @Mutation(() => String) @Authorized(Role.USER) - async participantChatCreate(@Ctx() context: GraphQLContext, @Arg('participantUserId') participantUserId: string) { + async participantChatCreate(@Ctx() context: GraphQLContext, @Arg('memberUserId') memberUserId: string, @Arg('subcourseId') subcourseId: number) { const { user } = context; - const participantUser = await getUser(participantUserId); + const memberUser = await getUser(memberUserId); - const allowed = await checkIfSubcourseParticipation([user.userID, participantUserId]); + const allowed = await checkIfSubcourseParticipation([user.userID, memberUserId]); const conversationInfos: ConversationInfos = { custom: { - type: 'participant', + subcourse: [subcourseId], }, }; if (allowed) { - const conversation = await getOrCreateConversation([user, participantUser], conversationInfos); + const conversation = await getOrCreateConversation([user, memberUser], conversationInfos, 'oneOnOne', ContactReason.PARTICIPANT, subcourseId); return conversation.id; } throw new Error('Participant is not allowed to create conversation.'); @@ -54,7 +54,6 @@ export class MutateChatResolver { @Mutation(() => String) @AuthorizedDeferred(Role.OWNER) async subcourseGroupChatCreate(@Ctx() context: GraphQLContext, @Arg('subcourseId') subcourseId: number) { - const { user } = context; const subcourse = await prisma.subcourse.findUnique({ where: { id: subcourseId }, include: { subcourse_participants_pupil: true, subcourse_instructors_student: true, lecture: true, course: true }, @@ -65,7 +64,6 @@ export class MutateChatResolver { subject: subcourse.course.name, custom: { start: subcourse.lecture[0].start.toISOString(), - type: 'course', }, }; const subcourseMembers = await getMembersForSubcourseGroupChat(subcourse); From 2a4b2ef55d14d38317196444486dc25e7055739a Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 14 Jun 2023 08:55:18 +0200 Subject: [PATCH 07/31] convert meta data as string --- common/chat/conversation.ts | 150 +++++++++++++++++------------------- common/chat/helper.ts | 29 ++++++- common/chat/types.ts | 71 +++++++++++++++++ graphql/chat/mutations.ts | 5 +- 4 files changed, 173 insertions(+), 82 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index c6bf76520..97ad45224 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -1,11 +1,19 @@ /* eslint-disable camelcase */ import dotenv from 'dotenv'; import { v4 as uuidv4 } from 'uuid'; -import { checkResponseStatus, convertConversationInfosToStringified, createOneOnOneId, getConversationId, userIdToTalkJsId } from './helper'; +import { + checkResponseStatus, + convertConversationInfosToStringified, + convertTJConversation, + createOneOnOneId, + getConversationId, + userIdToTalkJsId, +} from './helper'; import { Message } from 'talkjs/all'; import { User } from '../user'; import { getOrCreateChatUser } from './user'; import { prisma } from '../prisma'; +import { ContactReason, Conversation, ConversationInfos, TJConversation } from './types'; dotenv.config(); @@ -14,53 +22,6 @@ const talkjsConversationApiUrl = `${talkjsApiUrl}/conversations`; const apiKey = process.env.TALKJS_API_KEY; // adding "own" message type, since Message from 'talkjs/all' is either containing too many or too less attributes -type LastMessage = { - attachment: Message['attachment']; - conversationId: string; - createdAt: number; - custom: Message['custom']; - id: Message['id']; - senderId: Message['senderId']; - text: string; - type: Message['type']; -}; - -export enum ContactReason { - COURSE = 'course', - MATCH = 'match', - ANNOUNCEMENT = 'announcement', - PARTICIPANT = 'participant', - PROSPECT = 'prospect', -} - -type Conversation = { - id: string; - subject?: string; - topicId?: string; - photoUrl?: string; - welcomeMessages?: string[]; - custom?: ChatMetaData; - lastMessage?: LastMessage; - participants: { - [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; - }; - createdAt: number; -}; - -type ConversationInfos = { - subject?: string; - photoUrl?: string; - welcomeMessages?: string[]; - custom: ChatMetaData; -}; - -export type ChatMetaData = { - start?: string; - match?: { matchId: number }; - subcourse?: number[]; - prospectSubcourse?: number[]; - finished?: 'match_dissolved' | 'course_over'; -}; const createConversation = async (participants: User[], conversationInfos: ConversationInfos, type: 'oneOnOne' | 'group'): Promise => { let conversationId: string; @@ -75,20 +36,13 @@ const createConversation = async (participants: User[], conversationInfos: Conve throw new Error(`No matching case for conversationType found: ${type}`); } - try { - const body = JSON.stringify({ - subject: conversationInfos.subject ?? '', - welcomeMessages: conversationInfos.welcomeMessages ?? [], - custom: { - ...(conversationInfos.custom.start && { start: conversationInfos.custom.start }), - ...(conversationInfos.custom.match && { match: JSON.stringify(conversationInfos.custom.match) }), - ...(conversationInfos.custom.subcourse && { subcourse: JSON.stringify(conversationInfos.custom.subcourse) }), - ...(conversationInfos.custom.prospectSubcourse && { prospectSubcourse: JSON.stringify(conversationInfos.custom.prospectSubcourse) }), - ...(conversationInfos.custom.finished && { finished: conversationInfos.custom.finished }), - }, - participants: participants.map((participant: User) => userIdToTalkJsId(participant.userID)), - }); + const conversationInfosWithParticipants = { + ...convertConversationInfosToStringified(conversationInfos), + participants: participants.map((participant: User) => userIdToTalkJsId(participant.userID)), + }; + try { + const body = JSON.stringify(conversationInfosWithParticipants); const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}`, { method: 'PUT', headers: { @@ -104,7 +58,7 @@ const createConversation = async (participants: User[], conversationInfos: Conve } }; -const getConversation = async (conversationId: string): Promise => { +const getConversation = async (conversationId: string): Promise => { const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}`, { method: 'GET', headers: { @@ -136,30 +90,62 @@ const getOrCreateConversation = async ( }) ); - const conversationIdOfParticipants = getConversationId(participants); - const participantsConversation = await getConversation(conversationIdOfParticipants); - const subcoursesFromConversation = participantsConversation?.custom.subcourse ?? []; - const prospectSubcoursesFromConversation = participantsConversation?.custom.prospectSubcourse ?? []; - - const updatedConversation = { - id: conversationIdOfParticipants, - custom: { - ...(reason === ContactReason.MATCH && { match: conversationInfos.custom.match }), - ...(reason === ContactReason.PARTICIPANT && { subcourse: [...subcoursesFromConversation, subcourseId] }), - ...(reason === ContactReason.PROSPECT && { prospectSubcourse: [...prospectSubcoursesFromConversation, subcourseId] }), - }, - }; + const participantsConversationId = getConversationId(participants); + const participantsConversation = await getConversation(participantsConversationId); + + if (participantsConversation) { + if (reason === ContactReason.MATCH) { + const updatedConversation = { + id: participantsConversationId, + custom: { + match: conversationInfos.custom.match, + }, + }; + + await updateConversation(updatedConversation); + } else if (reason === ContactReason.PARTICIPANT) { + const subcoursesFromConversation = participantsConversation?.custom.subcourse ?? ''; + const subcourseIds: number[] = JSON.parse(subcoursesFromConversation); + + const updatedSubcourses: number[] = [...subcourseIds, subcourseId]; + const returnUpdatedSubcourses = Array.from(new Set(updatedSubcourses)); + + const updatedConversation = { + id: participantsConversationId, + custom: { + subcourse: returnUpdatedSubcourses, + }, + }; + + await updateConversation(updatedConversation); + } else if (reason === ContactReason.PROSPECT) { + const prospectSubcoursesFromConversation = participantsConversation?.custom.prospectSubcourse ?? ''; + const prospectSubcourseIds: number[] = JSON.parse(prospectSubcoursesFromConversation); - await updateConversation(updatedConversation); + const updatedProspectSubcourse: number[] = [...prospectSubcourseIds, subcourseId]; + const returnUpdatedProspectSubcourses = Array.from(new Set(updatedProspectSubcourse)); + + const updatedConversation = { + id: participantsConversationId, + custom: { + prospectSubcourse: returnUpdatedProspectSubcourses, + }, + }; + + await updateConversation(updatedConversation); + } + } if (participantsConversation === undefined) { const newConversationId = await createConversation(participants, conversationInfos, type); const newConversation = await getConversation(newConversationId); await sendSystemMessage('Willkommen im Lern-Fair Chat!', newConversationId, 'first'); - return newConversation; + const convertedConversation = convertTJConversation(newConversation); + return convertedConversation; } - return participantsConversation; + const convertedParticipantsConversation = convertTJConversation(participantsConversation); + return convertedParticipantsConversation; }; const getOrCreateGroupConversation = async (participants: User[], subcourseId: number, conversationInfos: ConversationInfos): Promise => { @@ -182,11 +168,15 @@ const getOrCreateGroupConversation = async (participants: User[], subcourseId: n where: { id: subcourseId }, data: { conversationId: newConversationId }, }); - return newConversation; + const convertedConversation = convertTJConversation(newConversation); + + return convertedConversation; } const subcourseGroupChat = await getConversation(subcourse.conversationId); - return subcourseGroupChat; + const convertedSubcourseGroupChatConversation = convertTJConversation(subcourseGroupChat); + + return convertedSubcourseGroupChatConversation; }; async function getLastUnreadConversation(user: User): Promise<{ data: Conversation[] }> { diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 33fd18187..54e745cd4 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -6,7 +6,7 @@ import { sha1 } from 'object-hash'; import { truncate } from 'lodash'; import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; -import { ChatMetaData, ConversationInfos } from './conversation'; +import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types'; const userIdToTalkJsId = (userId: string): string => { return userId.replace('/', '_'); @@ -132,6 +132,32 @@ const convertConversationInfosToStringified = (conversationInfos: ConversationIn return convertedObj; }; +const convertTJConversation = (conversation: TJConversation): Conversation => { + const { id, subject, topicId, photoUrl, welcomeMessages, custom, lastMessage, participants, createdAt } = conversation; + + const convertedCustom: ChatMetaData = custom + ? { + ...(custom.start && { start: custom.start }), + ...(custom.match && { match: { matchId: parseInt(custom.match) } }), + ...(custom.subcourse && { subcourse: custom.subcourse.split(',').map(Number) }), + ...(custom.prospectSubcourse && { prospectSubcourse: custom.prospectSubcourse.split(',').map(Number) }), + ...(custom.finished && { finished: custom.finished }), + } + : {}; + + return { + id, + subject, + topicId, + photoUrl, + welcomeMessages, + custom: convertedCustom, + lastMessage, + participants, + createdAt, + }; +}; + export { userIdToTalkJsId, parseUnderscoreToSlash, @@ -143,4 +169,5 @@ export { checkIfSubcourseParticipation, getMembersForSubcourseGroupChat, convertConversationInfosToStringified, + convertTJConversation, }; diff --git a/common/chat/types.ts b/common/chat/types.ts index 5ba7dddfd..8e32c7a01 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -1,3 +1,74 @@ +import { Message } from 'talkjs/all'; + +type LastMessage = { + attachment: Message['attachment']; + conversationId: string; + createdAt: number; + custom: Message['custom']; + id: Message['id']; + senderId: Message['senderId']; + text: string; + type: Message['type']; +}; + +export enum ContactReason { + COURSE = 'course', + MATCH = 'match', + ANNOUNCEMENT = 'announcement', + PARTICIPANT = 'participant', + PROSPECT = 'prospect', +} + +export type Conversation = { + id: string; + subject?: string; + topicId?: string; + photoUrl?: string; + welcomeMessages?: string[]; + custom?: ChatMetaData; + lastMessage?: LastMessage; + participants: { + [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; + }; + createdAt: number; +}; + +export type ChatMetaData = { + start?: string; + match?: { matchId: number }; + subcourse?: number[]; + prospectSubcourse?: number[]; + finished?: 'match_dissolved' | 'course_over'; +}; + +export type TJConversation = { + id: string; + subject?: string; + topicId?: string; + photoUrl?: string; + welcomeMessages?: string[]; + custom?: TJChatMetaData; + lastMessage?: LastMessage; + participants: { + [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; + }; + createdAt: number; +}; + +export type TJChatMetaData = { + start?: string; + match?: string; + subcourse?: string; + prospectSubcourse?: string; + finished?: 'match_dissolved' | 'course_over'; +}; +export type ConversationInfos = { + subject?: string; + photoUrl?: string; + welcomeMessages?: string[]; + custom: ChatMetaData; +}; + export enum ChatType { NORMAL = 'NORMAL', ANNOUNCEMENT = 'ANNOUNCEMENT', diff --git a/graphql/chat/mutations.ts b/graphql/chat/mutations.ts index 27ecaf98b..0cb4e4d9b 100644 --- a/graphql/chat/mutations.ts +++ b/graphql/chat/mutations.ts @@ -4,9 +4,10 @@ import { GraphQLContext } from '../context'; import { AuthorizedDeferred, hasAccess } from '../authorizations'; import { getLogger } from '../../common/logger/logger'; import { prisma } from '../../common/prisma'; -import { ContactReason, ConversationInfos, getOrCreateConversation, getOrCreateGroupConversation } from '../../common/chat'; +import { ConversationInfos, getOrCreateConversation, getOrCreateGroupConversation } from '../../common/chat'; import { User, getUser } from '../../common/user'; import { checkIfSubcourseParticipation, getMatchByMatchees, getMembersForSubcourseGroupChat } from '../../common/chat/helper'; +import { ContactReason } from '../../common/chat/types'; const logger = getLogger('MutateChatResolver'); @Resolver() @@ -64,6 +65,7 @@ export class MutateChatResolver { subject: subcourse.course.name, custom: { start: subcourse.lecture[0].start.toISOString(), + subcourse: [subcourseId], }, }; const subcourseMembers = await getMembersForSubcourseGroupChat(subcourse); @@ -74,6 +76,7 @@ export class MutateChatResolver { @Mutation(() => Boolean) @AuthorizedDeferred(Role.USER) async prospectChatCreate(@Ctx() context: GraphQLContext, @Arg('subcourseId') subcourseId: number) { + // TODO if prospect creation is merged -> adjust this const subcourse = await prisma.subcourse.findUnique({ where: { id: subcourseId } }); await hasAccess(context, 'Subcourse', subcourse); return true; From 032ef883c99ccb08b504a90f6e762709b27a4644 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 14 Jun 2023 16:09:13 +0200 Subject: [PATCH 08/31] add groupType to metadata, dont stringify strings --- common/chat/conversation.ts | 4 +++- common/chat/helper.ts | 3 ++- common/chat/types.ts | 10 ++-------- graphql/chat/mutations.ts | 1 + 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 89b9ef63e..33baf6881 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -92,6 +92,7 @@ const getOrCreateConversation = async ( const participantsConversationId = getConversationId(participants); const participantsConversation = await getConversation(participantsConversationId); + // if conversation already exists, furhter subcourseIds or matchId will be added to the conversation if (participantsConversation) { if (reason === ContactReason.MATCH) { const updatedConversation = { @@ -135,6 +136,7 @@ const getOrCreateConversation = async ( } } + // if no conversation exists, a new one will be created if (participantsConversation === undefined) { const newConversationId = await createConversation(participants, conversationInfos, 'oneOnOne'); const newConversation = await getConversation(newConversationId); @@ -162,7 +164,7 @@ const getOrCreateGroupConversation = async (participants: User[], subcourseId: n if (subcourse.conversationId === null) { const newConversationId = await createConversation(participants, conversationInfos, 'group'); const newConversation = await getConversation(newConversationId); - await sendSystemMessage('Willkommen im Lern-Fair Chat!', newConversationId, 'first'); + await sendSystemMessage('Schön dass du da bist!', newConversationId, 'first'); await prisma.subcourse.update({ where: { id: subcourseId }, data: { conversationId: newConversationId }, diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 54e745cd4..afdfa9695 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -125,7 +125,8 @@ const convertConversationInfosToStringified = (conversationInfos: ConversationIn for (const key in conversationInfos.custom) { if (conversationInfos.custom.hasOwnProperty(key)) { - convertedObj.custom[key] = JSON.stringify(conversationInfos.custom[key]); + const value = conversationInfos.custom[key]; + convertedObj.custom[key] = typeof value === 'string' ? value : JSON.stringify(value); } } diff --git a/common/chat/types.ts b/common/chat/types.ts index 6d71507ed..c0074e262 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -35,6 +35,7 @@ export type Conversation = { export type ChatMetaData = { start?: string; + groupType?: string; match?: { matchId: number }; subcourse?: number[]; prospectSubcourse?: number[]; @@ -57,6 +58,7 @@ export type TJConversation = { export type TJChatMetaData = { start?: string; + groupType?: string; match?: string; subcourse?: string; prospectSubcourse?: string; @@ -73,11 +75,3 @@ export enum ChatType { NORMAL = 'NORMAL', ANNOUNCEMENT = 'ANNOUNCEMENT', } - -export enum ContactReason { - COURSE = 'course', - MATCH = 'match', - ANNOUNCEMENT = 'announcement', - PARTICIPANT = 'participant', - PROSPECT = 'prospect', -} diff --git a/graphql/chat/mutations.ts b/graphql/chat/mutations.ts index e600729e9..8bee0c70e 100644 --- a/graphql/chat/mutations.ts +++ b/graphql/chat/mutations.ts @@ -65,6 +65,7 @@ export class MutateChatResolver { subject: subcourse.course.name, custom: { start: subcourse.lecture[0].start.toISOString(), + groupType: groupChatType, subcourse: [subcourseId], }, }; From 603d11a96d2958d974cba4745ce05cca37484592 Mon Sep 17 00:00:00 2001 From: Lomy Date: Thu, 15 Jun 2023 12:10:58 +0200 Subject: [PATCH 09/31] get matchId and subcourseIds for contact --- common/chat/contacts.ts | 133 ++++++++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/common/chat/contacts.ts b/common/chat/contacts.ts index 424b54936..8a1eeb008 100644 --- a/common/chat/contacts.ts +++ b/common/chat/contacts.ts @@ -3,6 +3,7 @@ import { prisma } from '../prisma'; import { isPupil, isStudent, userForPupil, userForStudent } from '../user'; import { pupil as Pupil, student as Student } from '@prisma/client'; import { User } from '../user'; +import { ContactReason } from './types'; export type UserContactType = { userID: string; @@ -14,29 +15,68 @@ export type UserContactType = { type UserContactlist = { [userId: string]: { user: UserContactType; - contactReasons: string[]; + contactReasons: ContactReason[]; + match?: { matchId: number }; + subcourse?: number[]; }; }; -const getMatchContacts = async (user: User): Promise => { +type PupilWithSubcourseIds = { + pupil: Pupil; + subcourseIds: number[]; +}; + +type StudentWithSubcourseIds = { + student: Student; + subcourseIds: number[]; +}; + +type PupilWithMatchId = { + pupil: Pupil; + matchId: number; +}; + +type StudentWithMatchId = { + student: Student; + matchId: number; +}; +const getMatchContacts = async (user: User): Promise => { if (user.pupilId) { - return await prisma.student.findMany({ + const studentsWithMatchId = await prisma.student.findMany({ where: { match: { some: { pupilId: user.pupilId } }, }, + include: { match: true }, }); + + const studentWithMatchId: StudentWithMatchId[] = studentsWithMatchId.map((studentWithMatchId) => { + return { + student: studentWithMatchId, + matchId: studentWithMatchId.match[0].id, + }; + }); + return studentWithMatchId; } if (user.studentId) { - return await prisma.pupil.findMany({ + const pupilsWithMatchId = await prisma.pupil.findMany({ where: { match: { some: { studentId: user.studentId } }, }, + include: { match: true }, + }); + const pupilWithMatchId: PupilWithMatchId[] = pupilsWithMatchId.map((pupilWithMatchId) => { + return { + pupil: pupilWithMatchId, + matchId: pupilWithMatchId.match[0].id, + }; }); + + return pupilWithMatchId; } }; -const getSubcourseInstructorContacts = async (pupil: User) => { +const getSubcourseInstructorContacts = async (pupil: User): Promise => { assert(pupil.pupilId, 'Pupil must have an pupilId'); - return await prisma.student.findMany({ + const studentsWithSubcourseIds = await prisma.student.findMany({ where: { subcourse_instructors_student: { some: { @@ -47,11 +87,32 @@ const getSubcourseInstructorContacts = async (pupil: User) => { }, }, }, + include: { + subcourse_instructors_student: { + select: { + subcourse: { + select: { + id: true, + }, + }, + }, + }, + }, + }); + + const instructorsWithSubcourseIds: StudentWithSubcourseIds[] = studentsWithSubcourseIds.map((studentWithSubcourseIds) => { + const subcourseIds = studentWithSubcourseIds.subcourse_instructors_student.map((participant) => participant.subcourse.id); + return { + student: studentWithSubcourseIds, + subcourseIds: subcourseIds, + }; }); + + return instructorsWithSubcourseIds; }; -const getSubcourseParticipantContact = async (student: User) => { +const getSubcourseParticipantContact = async (student: User): Promise => { assert(student.studentId, 'Student must have an studentId'); - return await prisma.pupil.findMany({ + const pupilsWithSubcourseIds = await prisma.pupil.findMany({ where: { subcourse_participants_pupil: { some: { @@ -62,7 +123,28 @@ const getSubcourseParticipantContact = async (student: User) => { }, }, }, + include: { + subcourse_participants_pupil: { + select: { + subcourse: { + select: { + id: true, + }, + }, + }, + }, + }, + }); + + const participantsWithSubcourseIds: PupilWithSubcourseIds[] = pupilsWithSubcourseIds.map((pupilWithSubcourseIds) => { + const subcourseIds = pupilWithSubcourseIds.subcourse_participants_pupil.map((participant) => participant.subcourse.id); + return { + pupil: pupilWithSubcourseIds, + subcourseIds: subcourseIds, + }; }); + + return participantsWithSubcourseIds; }; const getMySubcourseContacts = async (user: User): Promise => { let subcourseContactsList: UserContactlist = {}; @@ -70,17 +152,18 @@ const getMySubcourseContacts = async (user: User): Promise => { if (user.pupilId) { const subcourseContacts = await getSubcourseInstructorContacts(user); for (const subcourseContact of subcourseContacts) { - const instructorSubcourseId = userForStudent(subcourseContact).userID; - const contactReasons = ['subcourse']; + const instructorSubcourseId = userForStudent(subcourseContact.student).userID; + const contactReasons = [ContactReason.COURSE]; subcourseContactsList[instructorSubcourseId] = { user: { - firstname: subcourseContact.firstname, - lastname: subcourseContact.lastname, + firstname: subcourseContact.student.firstname, + lastname: subcourseContact.student.lastname, userID: instructorSubcourseId, - email: subcourseContact.email, + email: subcourseContact.student.email, }, contactReasons: contactReasons, + subcourse: subcourseContact.subcourseIds, }; } } @@ -88,17 +171,17 @@ const getMySubcourseContacts = async (user: User): Promise => { if (user.studentId) { const subcourseContacts = await getSubcourseParticipantContact(user); for (const subcourseContact of subcourseContacts) { - const participantSubcourseId = userForPupil(subcourseContact).userID; - const contactReasons = ['subcourse']; - + const participantSubcourseId = userForPupil(subcourseContact.pupil).userID; + const contactReasons = [ContactReason.COURSE]; subcourseContactsList[participantSubcourseId] = { user: { - firstname: subcourseContact.firstname, - lastname: subcourseContact.lastname, + firstname: subcourseContact.pupil.firstname, + lastname: subcourseContact.pupil.lastname, userID: participantSubcourseId, - email: subcourseContact.email, + email: subcourseContact.pupil.email, }, contactReasons: contactReasons, + subcourse: subcourseContact.subcourseIds, }; } } @@ -110,17 +193,18 @@ const getMyMatchContacts = async (user: User): Promise => { const matchContacts = await getMatchContacts(user); for (const matchContact of matchContacts) { let matchee: User; - if (isStudent(matchContact)) { - matchee = userForStudent(matchContact as Student); + if ('student' in matchContact) { + matchee = userForStudent(matchContact.student as Student); } - if (isPupil(matchContact)) { - matchee = userForPupil(matchContact as Pupil); + if ('pupil' in matchContact) { + matchee = userForPupil(matchContact.pupil as Pupil); } - const contactReasons = ['match']; + const contactReasons = [ContactReason.MATCH]; matchContactList[matchee.userID] = { user: { firstname: matchee.firstname, lastname: matchee.lastname, userID: matchee.userID, email: matchee.email }, contactReasons: contactReasons, + match: { matchId: matchContact.matchId }, }; } @@ -134,6 +218,7 @@ export const getMyContacts = async (user: User) => { const doubleContact = matchContacts[contactId]; if (doubleContact) { subcourseContacts[contactId].contactReasons.push(...doubleContact.contactReasons); + subcourseContacts[contactId].match = doubleContact.match; delete matchContacts[contactId]; } } From 4a8e8bd5cfb0515dd699474c2888b320555c0c76 Mon Sep 17 00:00:00 2001 From: Lomy Date: Thu, 15 Jun 2023 12:11:53 +0200 Subject: [PATCH 10/31] add mutation for contact chats --- common/chat/conversation.ts | 13 ++++++++----- common/chat/types.ts | 1 + graphql/chat/mutations.ts | 22 +++++++++++++++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 33baf6881..cf4bcac17 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -92,7 +92,6 @@ const getOrCreateConversation = async ( const participantsConversationId = getConversationId(participants); const participantsConversation = await getConversation(participantsConversationId); - // if conversation already exists, furhter subcourseIds or matchId will be added to the conversation if (participantsConversation) { if (reason === ContactReason.MATCH) { const updatedConversation = { @@ -133,11 +132,15 @@ const getOrCreateConversation = async ( }; await updateConversation(updatedConversation); - } - } + } else if (reason === ContactReason.CONTACT) { + const updatedConversation = { + id: participantsConversationId, + ...conversationInfos, + }; - // if no conversation exists, a new one will be created - if (participantsConversation === undefined) { + await updateConversation(updatedConversation); + } + } else { const newConversationId = await createConversation(participants, conversationInfos, 'oneOnOne'); const newConversation = await getConversation(newConversationId); await sendSystemMessage('Willkommen im Lern-Fair Chat!', newConversationId, 'first'); diff --git a/common/chat/types.ts b/common/chat/types.ts index c0074e262..952e538d9 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -17,6 +17,7 @@ export enum ContactReason { ANNOUNCEMENT = 'announcement', PARTICIPANT = 'participant', PROSPECT = 'prospect', + CONTACT = 'contact', } export type Conversation = { diff --git a/graphql/chat/mutations.ts b/graphql/chat/mutations.ts index 8bee0c70e..bd116ef3d 100644 --- a/graphql/chat/mutations.ts +++ b/graphql/chat/mutations.ts @@ -8,6 +8,7 @@ import { ConversationInfos, getOrCreateConversation, getOrCreateGroupConversatio import { User, getUser } from '../../common/user'; import { checkIfSubcourseParticipation, getMatchByMatchees, getMembersForSubcourseGroupChat } from '../../common/chat/helper'; import { ChatType, ContactReason } from '../../common/chat/types'; +import { getMyContacts } from '../../common/chat/contacts'; const logger = getLogger('MutateChatResolver'); @Resolver() @@ -41,7 +42,7 @@ export class MutateChatResolver { const allowed = await checkIfSubcourseParticipation([user.userID, memberUserId]); const conversationInfos: ConversationInfos = { custom: { - subcourse: [subcourseId], + ...(subcourseId && { subcourse: [subcourseId] }), }, }; @@ -85,4 +86,23 @@ export class MutateChatResolver { await hasAccess(context, 'Subcourse', subcourse); return true; } + + @Mutation(() => String) + @Authorized(Role.USER) + async contactChatCreate(@Ctx() context: GraphQLContext, @Arg('contactUserId') contactUserId: string) { + const { user } = context; + const contactUser = await getUser(contactUserId); + const myContacts = await getMyContacts(user); + const contact = myContacts.find((c) => c.user.userID === contactUserId); + + const conversationInfos: ConversationInfos = { + custom: { + ...(contact.match && { match: { matchId: contact.match.matchId } }), + ...(contact.subcourse && { subcourse: [...new Set(contact.subcourse)] }), + }, + }; + + const conversation = await getOrCreateConversation([user, contactUser], conversationInfos, ContactReason.CONTACT); + return conversation.id; + } } From edd53cba3d367888e0e970aa55a0a3235a482d7f Mon Sep 17 00:00:00 2001 From: Lomy Date: Fri, 16 Jun 2023 09:44:42 +0200 Subject: [PATCH 11/31] adjust integration-test --- integration-tests/matching.ts | 101 ++++++++++++++-------------------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/integration-tests/matching.ts b/integration-tests/matching.ts index bf291c098..60d144531 100644 --- a/integration-tests/matching.ts +++ b/integration-tests/matching.ts @@ -85,7 +85,9 @@ const match1 = test('Manual Match creation', async () => { } `); - const { me: { pupil: p1 }} = await pupilClient.request(` + const { + me: { pupil: p1 }, + } = await pupilClient.request(` query PupilWithMatch { me { pupil { @@ -106,7 +108,9 @@ const match1 = test('Manual Match creation', async () => { assert.strictEqual(p1.matches[0].student.firstname, student.firstname); assert.strictEqual(p1.matches[0].student.lastname, student.lastname); - const { me: { student: s1 }} = await studentClient.request(` + const { + me: { student: s1 }, + } = await studentClient.request(` query StudentWithMatch { me { student { @@ -137,86 +141,61 @@ void test('Create Chat for Match', async () => { // The pupil does not have one, it gets created: expectFetch({ - method: "GET", + method: 'GET', url: `https://api.talkjs.com/v1/mocked-talkjs-appid/users/pupil_${pupil.pupil.id}`, - responseStatus: 404 + responseStatus: 404, }); // The student has one: expectFetch({ - method: "GET", - url: `https://api.talkjs.com/v1/mocked-talkjs-appid/users/student_${student.student.id}`, - responseStatus: 200, - response: {} // TODO: Mock properly + method: 'GET', + url: `https://api.talkjs.com/v1/mocked-talkjs-appid/users/student_${student.student.id}`, + responseStatus: 200, + response: {}, // TODO: Mock properly }); expectFetch({ url: `https://api.talkjs.com/v1/mocked-talkjs-appid/users/pupil_${pupil.pupil.id}`, - "method": "PUT", - "body": JSON.stringify({"name": `${pupil.firstname} ${pupil.lastname}`, "email": [pupil.email.toLowerCase()], "role": "pupil"}), - "responseStatus": 200 + method: 'PUT', + body: JSON.stringify({ name: `${pupil.firstname} ${pupil.lastname}`, email: [pupil.email.toLowerCase()], role: 'pupil' }), + responseStatus: 200, }); expectFetch({ - method: "GET", + method: 'GET', url: `https://api.talkjs.com/v1/mocked-talkjs-appid/users/pupil_${pupil.pupil.id}`, responseStatus: 200, - response: {} // TODO: Mock properly + response: {}, // TODO: Mock properly }); - // Then the conversion is created: expectFetch({ - url: "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*", - method: "GET", - responseStatus: 404 - }); - - // TODO: Remove duplicate fetch - expectFetch({ - url: "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*", - method: "GET", - responseStatus: 404 - }); - - // TODO: Why PUT twice? - expectFetch({ - url: "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*", - method: "PUT", - body: "{\"id\":\"*\",\"custom\":{\"type\":\"match\"}}", - responseStatus: 200 - }); - - expectFetch({ - url: "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*", - method: "PUT", - body: `{\"custom\":{\"type\":\"match\"},\"participants\":[\"pupil_${pupil.pupil.id}\",\"student_${student.student.id}\"]}`, - responseStatus: 200 - }); - - expectFetch({ - url: "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*", - method: "GET", - responseStatus: 200, - response: { "id": "mocked" } // TODO: mock propery - }); + url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*', + method: 'GET', + responseStatus: 404, + }); - // TODO: Why twice? - expectFetch({ - "url": "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*", - "method": "GET", - "responseStatus": 200, - "response": { "id": "mocked" } // TODO: mock properly - }); + expectFetch({ + url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*', + method: 'PUT', + body: `{\"custom\":\"{match:{matchId:${id}}}\",\"participants\":[\"pupil_${pupil.pupil.id}\",\"student_${student.student.id}\"]}`, + responseStatus: 200, + }); - expectFetch({ - "url": "https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*/messages", - "method": "POST", - "body": "[{\"text\":\"Willkommen im Lern-Fair Chat!\",\"type\":\"SystemMessage\",\"custom\":{\"type\":\"first\"}}]", - "responseStatus": 200 - }); + expectFetch({ + url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*', + method: 'GET', + responseStatus: 200, + response: { id: 'mocked' }, // TODO: mock propery + }); + expectFetch({ + url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*/messages', + method: 'POST', + body: '[{"text":"Willkommen im Lern-Fair Chat!","type":"SystemMessage","custom":{"type":"first"}}]', + responseStatus: 200, + }); const { matchChatCreate: conversationID } = await pupilClient.request(` mutation PupilCreatesChat { @@ -224,7 +203,7 @@ void test('Create Chat for Match', async () => { } `); - assert.strictEqual(conversationID, "mocked"); + assert.strictEqual(conversationID, 'mocked'); }); void test('Anyone Request Matching Statistics', async () => { From 81bd15a8b176a624fd61dc0a39872d4ace31d81f Mon Sep 17 00:00:00 2001 From: Lomy Date: Fri, 16 Jun 2023 15:31:27 +0200 Subject: [PATCH 12/31] review changes: renamings and types --- common/chat/conversation.ts | 60 +++++++++++++++++-------------------- common/chat/types.ts | 31 ++++--------------- graphql/chat/mutations.ts | 10 +++---- 3 files changed, 38 insertions(+), 63 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index cf4bcac17..f5afd965e 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -17,9 +17,9 @@ import { ContactReason, Conversation, ConversationInfos, TJConversation } from ' dotenv.config(); -const talkjsApiUrl = `https://api.talkjs.com/v1/${process.env.TALKJS_APP_ID}`; -const talkjsConversationApiUrl = `${talkjsApiUrl}/conversations`; -const apiKey = process.env.TALKJS_API_KEY; +const TALKJS_API_URL = `https://api.talkjs.com/v1/${process.env.TALKJS_APP_ID}`; +const TALKJS_CONVERSATION_API_URL = `${TALKJS_API_URL}/conversations`; +const TALKJS_API_KEY = process.env.TALKJS_API_KEY; // adding "own" message type, since Message from 'talkjs/all' is either containing too many or too less attributes @@ -43,10 +43,10 @@ const createConversation = async (participants: User[], conversationInfos: Conve try { const body = JSON.stringify(conversationInfosWithParticipants); - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}`, { method: 'PUT', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, body: body, @@ -59,10 +59,10 @@ const createConversation = async (participants: User[], conversationInfos: Conve }; const getConversation = async (conversationId: string): Promise => { - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}`, { method: 'GET', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, }); @@ -76,7 +76,7 @@ const getConversation = async (conversationId: string): Promise { const userId = userIdToTalkJsId(user.userID); try { - const response = await fetch(`${talkjsApiUrl}/users/${userId}/conversations?unreadsOnly=true`, { + const response = await fetch(`${TALKJS_API_URL}/users/${userId}/conversations?unreadsOnly=true`, { method: 'GET', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, }); @@ -207,10 +207,10 @@ async function updateConversation(conversationToBeUpdated: { id: string } & Conv try { // TODO: This does not check anything! await getConversation(conversationToBeUpdated.id); - const response = await fetch(`${talkjsConversationApiUrl}/${conversationToBeUpdated.id}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationToBeUpdated.id}`, { method: 'PUT', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(convertConversationInfosToStringified(conversationToBeUpdated)), @@ -225,10 +225,10 @@ async function deleteConversation(conversationId: string): Promise { try { // check if conversation exists await getConversation(conversationId); - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}`, { method: 'DELETE', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, }); @@ -241,10 +241,10 @@ async function deleteConversation(conversationId: string): Promise { async function addParticipant(user: User, conversationId: string): Promise { const userId = userIdToTalkJsId(user.userID); try { - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}/participants/${userId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}/participants/${userId}`, { method: 'PUT', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, }); @@ -257,10 +257,10 @@ async function addParticipant(user: User, conversationId: string): Promise async function removeParticipant(user: User, conversationId: string): Promise { const userId = userIdToTalkJsId(user.userID); try { - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}/participants/${userId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}/participants/${userId}`, { method: 'DELETE', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, }); @@ -270,20 +270,16 @@ async function removeParticipant(user: User, conversationId: string): Promise { try { const conversation = await getConversation(conversationId); const memberIds = Object.keys(conversation.participants); for (const memberId of memberIds) { - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}/participants/${memberId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}/participants/${memberId}`, { method: 'PATCH', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -303,10 +299,10 @@ async function markConversationAsReadOnlyForPupils(conversationId: string): Prom const pupilIds = memberIds.filter((memberId) => !memberId.includes('student')); for (const pupilId of pupilIds) { - const response = await fetch(`${talkjsConversationApiUrl}/${conversationId}/participants/${pupilId}`, { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}/participants/${pupilId}`, { method: 'PATCH', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -325,10 +321,10 @@ async function markConversationAsWriteable(conversationId: string): Promise & { custom?: ChatMetaData; - lastMessage?: LastMessage; + lastMessage?: Message; participants: { [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; }; @@ -43,14 +27,9 @@ export type ChatMetaData = { finished?: 'match_dissolved' | 'course_over'; }; -export type TJConversation = { - id: string; - subject?: string; - topicId?: string; - photoUrl?: string; - welcomeMessages?: string[]; +export type TJConversation = Conversation & { custom?: TJChatMetaData; - lastMessage?: LastMessage; + lastMessage?: Message; participants: { [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; }; diff --git a/graphql/chat/mutations.ts b/graphql/chat/mutations.ts index 4c8984ab8..559c69779 100644 --- a/graphql/chat/mutations.ts +++ b/graphql/chat/mutations.ts @@ -4,7 +4,7 @@ import { GraphQLContext } from '../context'; import { AuthorizedDeferred, hasAccess } from '../authorizations'; import { getLogger } from '../../common/logger/logger'; import { prisma } from '../../common/prisma'; -import { ConversationInfos, getOrCreateConversation, getOrCreateGroupConversation, markConversationAsReadOnlyForPupils } from '../../common/chat'; +import { ConversationInfos, getOrCreateOneOnOneConversation, getOrCreateGroupConversation, markConversationAsReadOnlyForPupils } from '../../common/chat'; import { User, getUser } from '../../common/user'; import { checkIfSubcourseParticipation, getMatchByMatchees, getMembersForSubcourseGroupChat } from '../../common/chat/helper'; import { ChatType, ContactReason } from '../../common/chat/types'; @@ -29,7 +29,7 @@ export class MutateChatResolver { }, }; - const conversation = await getOrCreateConversation(matchees, conversationInfos, ContactReason.MATCH); + const conversation = await getOrCreateOneOnOneConversation(matchees, conversationInfos, ContactReason.MATCH); return conversation.id; } @@ -47,7 +47,7 @@ export class MutateChatResolver { }; if (allowed) { - const conversation = await getOrCreateConversation([user, memberUser], conversationInfos, ContactReason.PARTICIPANT, subcourseId); + const conversation = await getOrCreateOneOnOneConversation([user, memberUser], conversationInfos, ContactReason.PARTICIPANT, subcourseId); return conversation.id; } throw new Error('Participant is not allowed to create conversation.'); @@ -90,7 +90,7 @@ export class MutateChatResolver { }, }; - const conversation = await getOrCreateConversation([prospectUser, instructorUser], conversationInfos, ContactReason.PROSPECT, subcourseId); + const conversation = await getOrCreateOneOnOneConversation([prospectUser, instructorUser], conversationInfos, ContactReason.PROSPECT, subcourseId); return conversation.id; } @@ -110,7 +110,7 @@ export class MutateChatResolver { }, }; - const conversation = await getOrCreateConversation([user, contactUser], conversationInfos, ContactReason.CONTACT); + const conversation = await getOrCreateOneOnOneConversation([user, contactUser], conversationInfos, ContactReason.CONTACT); return conversation.id; } } From 5e3b85e98eb6f0c2eb537cca9c2365e318dafd96 Mon Sep 17 00:00:00 2001 From: Jonas Wilms Date: Fri, 16 Jun 2023 15:55:06 +0200 Subject: [PATCH 13/31] Fix tests & type --- common/chat/types.ts | 2 +- integration-tests/matching.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/common/chat/types.ts b/common/chat/types.ts index 96de4fd01..e3ef3ce71 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -9,7 +9,7 @@ export enum ContactReason { CONTACT = 'contact', } -export type Conversation = Pick & { +export type Conversation = Pick & { custom?: ChatMetaData; lastMessage?: Message; participants: { diff --git a/integration-tests/matching.ts b/integration-tests/matching.ts index 60d144531..3fbc08970 100644 --- a/integration-tests/matching.ts +++ b/integration-tests/matching.ts @@ -179,7 +179,7 @@ void test('Create Chat for Match', async () => { expectFetch({ url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*', method: 'PUT', - body: `{\"custom\":\"{match:{matchId:${id}}}\",\"participants\":[\"pupil_${pupil.pupil.id}\",\"student_${student.student.id}\"]}`, + body: `{"custom":{"match":"{\\\\"matchId\\\\":${id}}"},"participants":["pupil_${pupil.pupil.id}","student_${student.student.id}"]}`, responseStatus: 200, }); @@ -190,6 +190,13 @@ void test('Create Chat for Match', async () => { response: { id: 'mocked' }, // TODO: mock propery }); + expectFetch({ + url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*', + method: 'GET', + responseStatus: 200, + response: { id: 'mocked' }, // TODO: mock propery + }); + expectFetch({ url: 'https://api.talkjs.com/v1/mocked-talkjs-appid/conversations/*/messages', method: 'POST', From 057bb4af1b05e57aeecdb5092c5d60df01fd554e Mon Sep 17 00:00:00 2001 From: Lomy Date: Mon, 19 Jun 2023 09:19:27 +0200 Subject: [PATCH 14/31] functions to remove subcourse and match mark past subcourse chat mark empty conversation --- common/chat/helper.ts | 79 ++++++++++++++++++++++++++++++++++ graphql/match/mutations.ts | 8 ++-- graphql/subcourse/mutations.ts | 2 + 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 000489cd0..63fa86e21 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -8,6 +8,7 @@ import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; import { getPupil, getStudent } from '../../graphql/util'; import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types'; +import { getAllConversations, getConversation, markConversationAsReadOnly, updateConversation } from './conversation'; const userIdToTalkJsId = (userId: string): string => { return userId.replace('/', '_'); @@ -189,6 +190,81 @@ const convertTJConversation = (conversation: TJConversation): Conversation => { }; }; +const removeSubcourseFromConversation = async (subcourse: Subcourse): Promise => { + const conversationId = subcourse.conversationId; + const conversation = await getConversation(conversationId); + + if (conversation.custom.subcourse.includes(subcourse.id)) { + const index = conversation.custom.subcourse.indexOf(subcourse.id); + conversation.custom.subcourse.splice(index, 1); + + const updatedConversation = { + id: conversationId, + custom: conversation.custom, + }; + + await updateConversation(updatedConversation); + } +}; + +const removeMatchFromConversation = async (conversation: Conversation): Promise => { + if (conversation.custom.match) { + delete conversation.custom.match; + + const updatedConversation = { + id: conversation.id, + custom: conversation.custom, + }; + + await updateConversation(updatedConversation); + } +}; + +const markPastSubcoursesAsReadOnly = async () => { + const prevDay = new Date(); + prevDay.setDate(prevDay.getDate() - 1); + + const conversations = await getAllConversations(); + + conversations.data.forEach(async (conversation) => { + conversation.custom.subcourse.forEach(async (subcourseId) => { + const subcourse = await prisma.subcourse.findUnique({ + where: { id: subcourseId }, + include: { lecture: true }, + }); + + const lectures = subcourse.lecture; + + if (lectures.length > 0) { + const sortedLectures = lectures.sort((a, b) => { + const startA = new Date(a.start); + const startB = new Date(b.start); + return startA.getTime() - startB.getTime(); + }); + + const lastLecture = sortedLectures[sortedLectures.length - 1]; + const prevDay = new Date(); + + const endOfLastLecture = new Date(lastLecture.start); + endOfLastLecture.setMinutes(endOfLastLecture.getMinutes() + lastLecture.duration); + + if (endOfLastLecture < prevDay) { + await markConversationAsReadOnly(subcourse.conversationId); + } + } + }); + }); +}; + +const markEmptyConversationsAsReadOnly = async () => { + const conversations = await getAllConversations(); + conversations.data.forEach(async (conversation) => { + if (!conversation.custom?.match && conversation.custom.subcourse.length === 0) { + await markConversationAsReadOnly(conversation.id); + } + }); +}; + export { userIdToTalkJsId, parseUnderscoreToSlash, @@ -203,4 +279,7 @@ export { checkChatMembersAccessRights, convertConversationInfosToStringified, convertTJConversation, + removeSubcourseFromConversation, + removeMatchFromConversation, + markEmptyConversationsAsReadOnly, }; diff --git a/graphql/match/mutations.ts b/graphql/match/mutations.ts index aaffa8307..2b8b4042a 100644 --- a/graphql/match/mutations.ts +++ b/graphql/match/mutations.ts @@ -8,8 +8,8 @@ import { createMatch } from '../../common/match/create'; import { GraphQLContext } from '../context'; import { ConcreteMatchPool, pools } from '../../common/match/pool'; import { removeInterest } from '../../common/match/interest'; -import { getMatcheeConversation } from '../../common/chat/helper'; -import { markConversationAsReadOnly, markConversationAsWriteable } from '../../common/chat'; +import { getMatcheeConversation, removeMatchFromConversation } from '../../common/chat/helper'; +import { markConversationAsWriteable } from '../../common/chat'; @Resolver((of) => GraphQLModel.Match) export class MutateMatchResolver { @@ -39,7 +39,9 @@ export class MutateMatchResolver { const { conversation, conversationId } = await getMatcheeConversation({ studentId: match.studentId, pupilId: match.pupilId }); if (conversation) { - await markConversationAsReadOnly(conversationId); + // TODO: cant set as readonly, because maybe subcourse exists + // await markConversationAsReadOnly(conversationId); + await removeMatchFromConversation(conversation); } return true; } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 15b4de58f..5e54a2acc 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -30,6 +30,7 @@ import { validateEmail } from '../validators'; import { chat_type } from '../generated'; import { addParticipant, markConversationAsReadOnly, removeParticipant } from '../../common/chat/conversation'; import { ChatType } from '../../common/chat/types'; +import { removeSubcourseFromConversation } from '../../common/chat/helper'; const logger = getLogger('MutateCourseResolver'); @@ -204,6 +205,7 @@ export class MutateSubcourseResolver { await hasAccess(context, 'Subcourse', subcourse); await cancelSubcourse(subcourse); + await removeSubcourseFromConversation(subcourse); await markConversationAsReadOnly(subcourse.conversationId, 'deactivate'); return true; } From f40eaac8663e97c1af492423c5a0a14dd29b3e06 Mon Sep 17 00:00:00 2001 From: Lomy Date: Mon, 19 Jun 2023 09:19:39 +0200 Subject: [PATCH 15/31] no dissolved or cancelled contact --- common/chat/contacts.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/chat/contacts.ts b/common/chat/contacts.ts index 8a1eeb008..50d8a0935 100644 --- a/common/chat/contacts.ts +++ b/common/chat/contacts.ts @@ -44,7 +44,7 @@ const getMatchContacts = async (user: User): Promise Date: Mon, 19 Jun 2023 11:44:09 +0200 Subject: [PATCH 16/31] get all conversations --- common/chat/conversation.ts | 19 +++++++++++++++++-- common/chat/types.ts | 4 ++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 56cd2bcf6..42bd091df 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -13,7 +13,7 @@ import { import { User } from '../user'; import { getOrCreateChatUser } from './user'; import { prisma } from '../prisma'; -import { ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, TJConversation } from './types'; +import { AllConversations, ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, TJConversation } from './types'; dotenv.config(); @@ -74,7 +74,21 @@ const getConversation = async (conversationId: string): Promise => { + const response = await fetch(`${TALKJS_CONVERSATION_API_URL}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${TALKJS_API_KEY}`, + 'Content-Type': 'application/json', + }, + }); + + if (response.status === 200) { + return await response.json(); + } else { + return undefined; + } +}; const getOrCreateOneOnOneConversation = async ( participants: [User, User], @@ -385,6 +399,7 @@ export { markConversationAsWriteable, sendSystemMessage, getConversation, + getAllConversations, getOrCreateOneOnOneConversation, getOrCreateGroupConversation, deleteConversation, diff --git a/common/chat/types.ts b/common/chat/types.ts index 2d80f2a80..38d548d0e 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -60,3 +60,7 @@ export enum ChatAccess { READ = 'Read', READWRITE = 'ReadWrite', } + +export type AllConversations = { + data: Conversation[]; +}; From 37ea77ae9198134f09c31b5b1b9937282cac9ebd Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 28 Jun 2023 15:23:29 +0200 Subject: [PATCH 17/31] review changes --- common/chat/contacts.ts | 147 ++++++++++++++++++++++-------------- common/chat/conversation.ts | 43 +++++++---- common/chat/helper.ts | 26 ++++--- common/chat/types.ts | 9 ++- graphql/chat/mutations.ts | 43 ++++++----- 5 files changed, 164 insertions(+), 104 deletions(-) diff --git a/common/chat/contacts.ts b/common/chat/contacts.ts index 8a1eeb008..2e27d124d 100644 --- a/common/chat/contacts.ts +++ b/common/chat/contacts.ts @@ -1,9 +1,10 @@ import assert from 'assert'; import { prisma } from '../prisma'; -import { isPupil, isStudent, userForPupil, userForStudent } from '../user'; +import { userForPupil, userForStudent } from '../user'; import { pupil as Pupil, student as Student } from '@prisma/client'; import { User } from '../user'; import { ContactReason } from './types'; +import { isPupilContact, isStudentContact } from './helper'; export type UserContactType = { userID: string; @@ -12,35 +13,52 @@ export type UserContactType = { email: string; }; -type UserContactlist = { +type BaseContactList = { [userId: string]: { user: UserContactType; contactReasons: ContactReason[]; + }; +}; + +type CompleteContactList = BaseContactList & { + [userId: string]: { match?: { matchId: number }; subcourse?: number[]; }; }; -type PupilWithSubcourseIds = { +type MatchContactList = BaseContactList & { + [userId: string]: { + match: { matchId: number }; + }; +}; + +type SubcourseContactList = BaseContactList & { + [userId: string]: { + subcourse: number[]; + }; +}; + +type SubcourseContactPupil = { pupil: Pupil; subcourseIds: number[]; }; -type StudentWithSubcourseIds = { +type SubcourseContactStudent = { student: Student; subcourseIds: number[]; }; -type PupilWithMatchId = { +export type MatchContactPupil = { pupil: Pupil; matchId: number; }; -type StudentWithMatchId = { +export type MatchContactStudent = { student: Student; matchId: number; }; -const getMatchContacts = async (user: User): Promise => { +const getMatchContactsForUser = async (user: User): Promise => { if (user.pupilId) { const studentsWithMatchId = await prisma.student.findMany({ where: { @@ -49,7 +67,7 @@ const getMatchContacts = async (user: User): Promise { + const studentWithMatchId: MatchContactStudent[] = studentsWithMatchId.map((studentWithMatchId) => { return { student: studentWithMatchId, matchId: studentWithMatchId.match[0].id, @@ -64,7 +82,7 @@ const getMatchContacts = async (user: User): Promise { + const pupilWithMatchId: MatchContactPupil[] = pupilsWithMatchId.map((pupilWithMatchId) => { return { pupil: pupilWithMatchId, matchId: pupilWithMatchId.match[0].id, @@ -74,7 +92,7 @@ const getMatchContacts = async (user: User): Promise => { +const getInstructorsForPupilSubcourses = async (pupil: User): Promise => { assert(pupil.pupilId, 'Pupil must have an pupilId'); const studentsWithSubcourseIds = await prisma.student.findMany({ where: { @@ -82,7 +100,9 @@ const getSubcourseInstructorContacts = async (pupil: User): Promise { + const instructorsWithSubcourseIds: SubcourseContactStudent[] = studentsWithSubcourseIds.map((studentWithSubcourseIds) => { const subcourseIds = studentWithSubcourseIds.subcourse_instructors_student.map((participant) => participant.subcourse.id); return { student: studentWithSubcourseIds, @@ -110,7 +137,7 @@ const getSubcourseInstructorContacts = async (pupil: User): Promise => { +const getSubcourseParticipantContactForUser = async (student: User): Promise => { assert(student.studentId, 'Student must have an studentId'); const pupilsWithSubcourseIds = await prisma.pupil.findMany({ where: { @@ -136,7 +163,7 @@ const getSubcourseParticipantContact = async (student: User): Promise { + const participantsWithSubcourseIds: SubcourseContactPupil[] = pupilsWithSubcourseIds.map((pupilWithSubcourseIds) => { const subcourseIds = pupilWithSubcourseIds.subcourse_participants_pupil.map((participant) => participant.subcourse.id); return { pupil: pupilWithSubcourseIds, @@ -146,57 +173,58 @@ const getSubcourseParticipantContact = async (student: User): Promise => { - let subcourseContactsList: UserContactlist = {}; +const getSubcourseContactsForPupil = async (pupil: User): Promise => { + let subcourseContactsList: SubcourseContactList = {}; - if (user.pupilId) { - const subcourseContacts = await getSubcourseInstructorContacts(user); - for (const subcourseContact of subcourseContacts) { - const instructorSubcourseId = userForStudent(subcourseContact.student).userID; - const contactReasons = [ContactReason.COURSE]; - - subcourseContactsList[instructorSubcourseId] = { - user: { - firstname: subcourseContact.student.firstname, - lastname: subcourseContact.student.lastname, - userID: instructorSubcourseId, - email: subcourseContact.student.email, - }, - contactReasons: contactReasons, - subcourse: subcourseContact.subcourseIds, - }; - } + const subcourseContacts = await getInstructorsForPupilSubcourses(pupil); + for (const subcourseContact of subcourseContacts) { + const instructorSubcourseId = userForStudent(subcourseContact.student).userID; + const contactReasons = [ContactReason.COURSE]; + + subcourseContactsList[instructorSubcourseId] = { + user: { + firstname: subcourseContact.student.firstname, + lastname: subcourseContact.student.lastname, + userID: instructorSubcourseId, + email: subcourseContact.student.email, + }, + contactReasons: contactReasons, + subcourse: subcourseContact.subcourseIds, + }; } - if (user.studentId) { - const subcourseContacts = await getSubcourseParticipantContact(user); - for (const subcourseContact of subcourseContacts) { - const participantSubcourseId = userForPupil(subcourseContact.pupil).userID; - const contactReasons = [ContactReason.COURSE]; - subcourseContactsList[participantSubcourseId] = { - user: { - firstname: subcourseContact.pupil.firstname, - lastname: subcourseContact.pupil.lastname, - userID: participantSubcourseId, - email: subcourseContact.pupil.email, - }, - contactReasons: contactReasons, - subcourse: subcourseContact.subcourseIds, - }; - } + return subcourseContactsList; +}; +const getSubcourseContactsForStudent = async (student: User): Promise => { + let subcourseContactsList: SubcourseContactList = {}; + + const subcourseContacts = await getSubcourseParticipantContactForUser(student); + for (const subcourseContact of subcourseContacts) { + const participantSubcourseId = userForPupil(subcourseContact.pupil).userID; + const contactReasons = [ContactReason.COURSE]; + subcourseContactsList[participantSubcourseId] = { + user: { + firstname: subcourseContact.pupil.firstname, + lastname: subcourseContact.pupil.lastname, + userID: participantSubcourseId, + email: subcourseContact.pupil.email, + }, + contactReasons: contactReasons, + subcourse: subcourseContact.subcourseIds, + }; } return subcourseContactsList; }; -const getMyMatchContacts = async (user: User): Promise => { - let matchContactList: UserContactlist = {}; - const matchContacts = await getMatchContacts(user); +const getMyMatchContacts = async (user: User): Promise => { + let matchContactList: MatchContactList = {}; + const matchContacts = await getMatchContactsForUser(user); for (const matchContact of matchContacts) { let matchee: User; - if ('student' in matchContact) { + if (isStudentContact(matchContact)) { matchee = userForStudent(matchContact.student as Student); } - if ('pupil' in matchContact) { + if (isPupilContact(matchContact)) { matchee = userForPupil(matchContact.pupil as Pupil); } const contactReasons = [ContactReason.MATCH]; @@ -211,7 +239,14 @@ const getMyMatchContacts = async (user: User): Promise => { return matchContactList; }; export const getMyContacts = async (user: User) => { - const subcourseContacts = await getMySubcourseContacts(user); + let subcourseContacts: CompleteContactList = {}; + if (user.pupilId) { + subcourseContacts = await getSubcourseContactsForPupil(user); + } + + if (user.studentId) { + subcourseContacts = await getSubcourseContactsForStudent(user); + } const matchContacts = await getMyMatchContacts(user); for (const contactId in subcourseContacts) { @@ -222,7 +257,7 @@ export const getMyContacts = async (user: User) => { delete matchContacts[contactId]; } } - const myContacts = { ...subcourseContacts, ...matchContacts }; + const myContacts: CompleteContactList = { ...subcourseContacts, ...matchContacts }; const myContactsAsArray = Object.values(myContacts); return myContactsAsArray; }; diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index f5afd965e..8a0caecb4 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -1,19 +1,13 @@ /* eslint-disable camelcase */ import dotenv from 'dotenv'; import { v4 as uuidv4 } from 'uuid'; -import { - checkResponseStatus, - convertConversationInfosToStringified, - convertTJConversation, - createOneOnOneId, - getConversationId, - userIdToTalkJsId, -} from './helper'; +import { checkResponseStatus, convertConversationInfosToString, convertTJConversation, createOneOnOneId, getConversationId, userIdToTalkJsId } from './helper'; import { Message } from 'talkjs/all'; -import { User } from '../user'; +import { User, getUser } from '../user'; import { getOrCreateChatUser } from './user'; import { prisma } from '../prisma'; -import { ContactReason, Conversation, ConversationInfos, TJConversation } from './types'; +import { AccessRight, ContactReason, Conversation, ConversationInfos, TJConversation } from './types'; +import { getMyContacts } from './contacts'; dotenv.config(); @@ -37,7 +31,7 @@ const createConversation = async (participants: User[], conversationInfos: Conve } const conversationInfosWithParticipants = { - ...convertConversationInfosToStringified(conversationInfos), + ...convertConversationInfosToString(conversationInfos), participants: participants.map((participant: User) => userIdToTalkJsId(participant.userID)), }; @@ -213,7 +207,7 @@ async function updateConversation(conversationToBeUpdated: { id: string } & Conv Authorization: `Bearer ${TALKJS_API_KEY}`, 'Content-Type': 'application/json', }, - body: JSON.stringify(convertConversationInfosToStringified(conversationToBeUpdated)), + body: JSON.stringify(convertConversationInfosToString(conversationToBeUpdated)), }); await checkResponseStatus(response); } catch (error) { @@ -283,7 +277,7 @@ async function markConversationAsReadOnly(conversationId: string): Promise 'Content-Type': 'application/json', }, body: JSON.stringify({ - access: 'Read', + access: AccessRight.READ, }), }); await checkResponseStatus(response); @@ -306,7 +300,7 @@ async function markConversationAsReadOnlyForPupils(conversationId: string): Prom 'Content-Type': 'application/json', }, body: JSON.stringify({ - access: 'Read', + access: AccessRight.READ, }), }); await checkResponseStatus(response); @@ -328,7 +322,7 @@ async function markConversationAsWriteable(conversationId: string): Promise { + const myContacts = await getMyContacts(meUser); + const contact = myContacts.find((c) => c.user.userID === contactUser.userID); + + if (!contact) { + throw new Error('Chat contact not found'); + } + + const conversationInfos: ConversationInfos = { + custom: { + ...(contact.match && { match: { matchId: contact.match.matchId } }), + ...(contact.subcourse && { subcourse: [...new Set(contact.subcourse)] }), + }, + }; + + const conversation = await getOrCreateOneOnOneConversation([meUser, contactUser], conversationInfos, ContactReason.CONTACT); + return conversation.id; +} export { getLastUnreadConversation, @@ -380,6 +392,7 @@ export { getOrCreateGroupConversation, deleteConversation, markConversationAsReadOnlyForPupils, + createContactChat, TALKJS_CONVERSATION_API_URL, Conversation, ConversationInfos, diff --git a/common/chat/helper.ts b/common/chat/helper.ts index afdfa9695..3a1ca399e 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -7,9 +7,12 @@ import { truncate } from 'lodash'; import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types'; +import { MatchContactPupil, MatchContactStudent } from './contacts'; -const userIdToTalkJsId = (userId: string): string => { - return userId.replace('/', '_'); +type TalkJSUserId = `${'pupil' | 'student'}_${number}`; + +const userIdToTalkJsId = (userId: string): TalkJSUserId => { + return userId.replace('/', '_') as TalkJSUserId; }; const createChatSignature = async (user: User): Promise => { const userId = (await getOrCreateChatUser(user)).id; @@ -62,7 +65,7 @@ const getMatchByMatchees = async (matchees: string[]): Promise => { return match; }; -const checkIfSubcourseParticipation = async (participants: string[]): Promise => { +const isSubcourseParticipant = async (participants: string[]): Promise => { const participantUser = participants.map(async (participant) => { const user = await getUser(participant); return user; @@ -115,8 +118,8 @@ const getMembersForSubcourseGroupChat = async (subcourse: Subcourse) => { return members; }; -const convertConversationInfosToStringified = (conversationInfos: ConversationInfos): ConversationInfos => { - const convertedObj: ConversationInfos = { +const convertConversationInfosToString = (conversationInfos: ConversationInfos): ConversationInfos => { + const convertedConversationInfos: ConversationInfos = { subject: conversationInfos.subject, photoUrl: conversationInfos.photoUrl, welcomeMessages: conversationInfos.welcomeMessages, @@ -126,11 +129,11 @@ const convertConversationInfosToStringified = (conversationInfos: ConversationIn for (const key in conversationInfos.custom) { if (conversationInfos.custom.hasOwnProperty(key)) { const value = conversationInfos.custom[key]; - convertedObj.custom[key] = typeof value === 'string' ? value : JSON.stringify(value); + convertedConversationInfos.custom[key] = typeof value === 'string' ? value : JSON.stringify(value); } } - return convertedObj; + return convertedConversationInfos; }; const convertTJConversation = (conversation: TJConversation): Conversation => { @@ -159,6 +162,9 @@ const convertTJConversation = (conversation: TJConversation): Conversation => { }; }; +const isStudentContact = (contact: MatchContactPupil | MatchContactStudent): contact is MatchContactStudent => contact.hasOwnProperty('student'); +const isPupilContact = (contact: MatchContactPupil | MatchContactStudent): contact is MatchContactPupil => contact.hasOwnProperty('pupil'); + export { userIdToTalkJsId, parseUnderscoreToSlash, @@ -167,8 +173,10 @@ export { getMatchByMatchees, createOneOnOneId, getConversationId, - checkIfSubcourseParticipation, + isSubcourseParticipant, getMembersForSubcourseGroupChat, - convertConversationInfosToStringified, + convertConversationInfosToString, convertTJConversation, + isStudentContact, + isPupilContact, }; diff --git a/common/chat/types.ts b/common/chat/types.ts index e3ef3ce71..6b70abf39 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -9,11 +9,16 @@ export enum ContactReason { CONTACT = 'contact', } +export enum AccessRight { + READ = 'Read', + READ_WRITE = 'ReadWrite', +} + export type Conversation = Pick & { custom?: ChatMetaData; lastMessage?: Message; participants: { - [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; + [id: string]: { access: AccessRight; notify: boolean }; }; createdAt: number; }; @@ -31,7 +36,7 @@ export type TJConversation = Conversation & { custom?: TJChatMetaData; lastMessage?: Message; participants: { - [id: string]: { access: 'ReadWrite' | 'Read'; notify: boolean }; + [id: string]: { access: AccessRight; notify: boolean }; }; createdAt: number; }; diff --git a/graphql/chat/mutations.ts b/graphql/chat/mutations.ts index 559c69779..4ea8fd2ff 100644 --- a/graphql/chat/mutations.ts +++ b/graphql/chat/mutations.ts @@ -4,11 +4,16 @@ import { GraphQLContext } from '../context'; import { AuthorizedDeferred, hasAccess } from '../authorizations'; import { getLogger } from '../../common/logger/logger'; import { prisma } from '../../common/prisma'; -import { ConversationInfos, getOrCreateOneOnOneConversation, getOrCreateGroupConversation, markConversationAsReadOnlyForPupils } from '../../common/chat'; +import { + ConversationInfos, + getOrCreateOneOnOneConversation, + getOrCreateGroupConversation, + markConversationAsReadOnlyForPupils, + createContactChat, +} from '../../common/chat'; import { User, getUser } from '../../common/user'; -import { checkIfSubcourseParticipation, getMatchByMatchees, getMembersForSubcourseGroupChat } from '../../common/chat/helper'; +import { isSubcourseParticipant, getMatchByMatchees, getMembersForSubcourseGroupChat } from '../../common/chat/helper'; import { ChatType, ContactReason } from '../../common/chat/types'; -import { getMyContacts } from '../../common/chat/contacts'; const logger = getLogger('MutateChatResolver'); @Resolver() @@ -39,13 +44,15 @@ export class MutateChatResolver { const { user } = context; const memberUser = await getUser(memberUserId); - const allowed = await checkIfSubcourseParticipation([user.userID, memberUserId]); + const allowed = await isSubcourseParticipant([user.userID, memberUserId]); const conversationInfos: ConversationInfos = { - custom: { - ...(subcourseId && { subcourse: [subcourseId] }), - }, + custom: {}, }; + if (subcourseId) { + conversationInfos.custom.subcourse = [subcourseId]; + } + if (allowed) { const conversation = await getOrCreateOneOnOneConversation([user, memberUser], conversationInfos, ContactReason.PARTICIPANT, subcourseId); return conversation.id; @@ -85,11 +92,13 @@ export class MutateChatResolver { const instructorUser = await getUser(instructorUserId); const conversationInfos: ConversationInfos = { - custom: { - ...(subcourseId && { subcourse: [subcourseId] }), - }, + custom: {}, }; + if (subcourseId) { + conversationInfos.custom.prospectSubcourse = [subcourseId]; + } + const conversation = await getOrCreateOneOnOneConversation([prospectUser, instructorUser], conversationInfos, ContactReason.PROSPECT, subcourseId); return conversation.id; @@ -100,17 +109,7 @@ export class MutateChatResolver { async contactChatCreate(@Ctx() context: GraphQLContext, @Arg('contactUserId') contactUserId: string) { const { user } = context; const contactUser = await getUser(contactUserId); - const myContacts = await getMyContacts(user); - const contact = myContacts.find((c) => c.user.userID === contactUserId); - - const conversationInfos: ConversationInfos = { - custom: { - ...(contact.match && { match: { matchId: contact.match.matchId } }), - ...(contact.subcourse && { subcourse: [...new Set(contact.subcourse)] }), - }, - }; - - const conversation = await getOrCreateOneOnOneConversation([user, contactUser], conversationInfos, ContactReason.CONTACT); - return conversation.id; + const contactConversationId = await createContactChat(user, contactUser); + return contactConversationId; } } From 3e4f9abf264e4db1f23abbc0cf5ce889326a75fe Mon Sep 17 00:00:00 2001 From: Lomy Date: Tue, 4 Jul 2023 13:31:13 +0200 Subject: [PATCH 18/31] add cron job file --- common/chat/helper.ts | 3 +++ jobs/periodic/flag-old-conversations/index.ts | 0 2 files changed, 3 insertions(+) create mode 100644 jobs/periodic/flag-old-conversations/index.ts diff --git a/common/chat/helper.ts b/common/chat/helper.ts index d0faa78b6..c0e4601ca 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -224,6 +224,8 @@ const removeMatchFromConversation = async (conversation: Conversation): Promise< }; const markPastSubcoursesAsReadOnly = async () => { + // TODO change to moment + // TODO check also match const prevDay = new Date(); prevDay.setDate(prevDay.getDate() - 1); @@ -251,6 +253,7 @@ const markPastSubcoursesAsReadOnly = async () => { const endOfLastLecture = new Date(lastLecture.start); endOfLastLecture.setMinutes(endOfLastLecture.getMinutes() + lastLecture.duration); + // TODO + 30 days if (endOfLastLecture < prevDay) { await markConversationAsReadOnly(subcourse.conversationId); } diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts new file mode 100644 index 000000000..e69de29bb From b3e2daa0bd9a7680f3f26946e72cf6185f1f4fef Mon Sep 17 00:00:00 2001 From: Lomy Date: Tue, 4 Jul 2023 13:44:41 +0200 Subject: [PATCH 19/31] remove useless functions --- common/chat/conversation.ts | 2 +- common/chat/helper.ts | 80 +--------------------------------- graphql/match/mutations.ts | 9 +--- graphql/subcourse/mutations.ts | 4 +- 4 files changed, 4 insertions(+), 91 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 3f427d512..96f14f528 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -296,7 +296,7 @@ async function removeParticipant(user: User, conversationId: string): Promise { +async function markConversationAsReadOnly(conversationId: string): Promise { try { const conversation = await getConversation(conversationId); const memberIds = Object.keys(conversation.participants); diff --git a/common/chat/helper.ts b/common/chat/helper.ts index d0faa78b6..3321ceb68 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -7,7 +7,7 @@ import { truncate } from 'lodash'; import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; import { getPupil, getStudent } from '../../graphql/util'; -import { getAllConversations, getConversation, markConversationAsReadOnly, updateConversation } from './conversation'; +import { getAllConversations, getConversation, markConversationAsReadOnly } from './conversation'; import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types'; import { MatchContactPupil, MatchContactStudent } from './contacts'; @@ -193,80 +193,6 @@ const convertTJConversation = (conversation: TJConversation): Conversation => { }; }; -const removeSubcourseFromConversation = async (subcourse: Subcourse): Promise => { - const conversationId = subcourse.conversationId; - const conversation = await getConversation(conversationId); - - if (conversation.custom.subcourse.includes(subcourse.id)) { - const index = conversation.custom.subcourse.indexOf(subcourse.id); - conversation.custom.subcourse.splice(index, 1); - - const updatedConversation = { - id: conversationId, - custom: conversation.custom, - }; - - await updateConversation(updatedConversation); - } -}; - -const removeMatchFromConversation = async (conversation: Conversation): Promise => { - if (conversation.custom.match) { - delete conversation.custom.match; - - const updatedConversation = { - id: conversation.id, - custom: conversation.custom, - }; - - await updateConversation(updatedConversation); - } -}; - -const markPastSubcoursesAsReadOnly = async () => { - const prevDay = new Date(); - prevDay.setDate(prevDay.getDate() - 1); - - const conversations = await getAllConversations(); - - conversations.data.forEach(async (conversation) => { - conversation.custom.subcourse.forEach(async (subcourseId) => { - const subcourse = await prisma.subcourse.findUnique({ - where: { id: subcourseId }, - include: { lecture: true }, - }); - - const lectures = subcourse.lecture; - - if (lectures.length > 0) { - const sortedLectures = lectures.sort((a, b) => { - const startA = new Date(a.start); - const startB = new Date(b.start); - return startA.getTime() - startB.getTime(); - }); - - const lastLecture = sortedLectures[sortedLectures.length - 1]; - const prevDay = new Date(); - - const endOfLastLecture = new Date(lastLecture.start); - endOfLastLecture.setMinutes(endOfLastLecture.getMinutes() + lastLecture.duration); - - if (endOfLastLecture < prevDay) { - await markConversationAsReadOnly(subcourse.conversationId); - } - } - }); - }); -}; - -const markEmptyConversationsAsReadOnly = async () => { - const conversations = await getAllConversations(); - conversations.data.forEach(async (conversation) => { - if (!conversation.custom?.match && conversation.custom.subcourse.length === 0) { - await markConversationAsReadOnly(conversation.id); - } - }); -}; const isStudentContact = (contact: MatchContactPupil | MatchContactStudent): contact is MatchContactStudent => contact.hasOwnProperty('student'); const isPupilContact = (contact: MatchContactPupil | MatchContactStudent): contact is MatchContactPupil => contact.hasOwnProperty('pupil'); @@ -280,10 +206,6 @@ export { getConversationId, getMatcheeConversation, checkChatMembersAccessRights, - removeSubcourseFromConversation, - removeMatchFromConversation, - markEmptyConversationsAsReadOnly, - markPastSubcoursesAsReadOnly, isSubcourseParticipant, getMembersForSubcourseGroupChat, convertConversationInfosToString, diff --git a/graphql/match/mutations.ts b/graphql/match/mutations.ts index 2b8b4042a..343c10ce2 100644 --- a/graphql/match/mutations.ts +++ b/graphql/match/mutations.ts @@ -8,7 +8,7 @@ import { createMatch } from '../../common/match/create'; import { GraphQLContext } from '../context'; import { ConcreteMatchPool, pools } from '../../common/match/pool'; import { removeInterest } from '../../common/match/interest'; -import { getMatcheeConversation, removeMatchFromConversation } from '../../common/chat/helper'; +import { getMatcheeConversation } from '../../common/chat/helper'; import { markConversationAsWriteable } from '../../common/chat'; @Resolver((of) => GraphQLModel.Match) @@ -36,13 +36,6 @@ export class MutateMatchResolver { await hasAccess(context, 'Match', match); await dissolveMatch(match, dissolveReason, /* dissolver:*/ null); - const { conversation, conversationId } = await getMatcheeConversation({ studentId: match.studentId, pupilId: match.pupilId }); - - if (conversation) { - // TODO: cant set as readonly, because maybe subcourse exists - // await markConversationAsReadOnly(conversationId); - await removeMatchFromConversation(conversation); - } return true; } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 5e54a2acc..73f82a27c 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -30,7 +30,6 @@ import { validateEmail } from '../validators'; import { chat_type } from '../generated'; import { addParticipant, markConversationAsReadOnly, removeParticipant } from '../../common/chat/conversation'; import { ChatType } from '../../common/chat/types'; -import { removeSubcourseFromConversation } from '../../common/chat/helper'; const logger = getLogger('MutateCourseResolver'); @@ -205,8 +204,7 @@ export class MutateSubcourseResolver { await hasAccess(context, 'Subcourse', subcourse); await cancelSubcourse(subcourse); - await removeSubcourseFromConversation(subcourse); - await markConversationAsReadOnly(subcourse.conversationId, 'deactivate'); + await markConversationAsReadOnly(subcourse.conversationId); return true; } From 3249d1307c1978fe3db0f22634be9d14033b8459 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 5 Jul 2023 10:21:58 +0200 Subject: [PATCH 20/31] add function to flag inactive conversations --- jobs/periodic/flag-old-conversations/index.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index e69de29bb..292dfa87d 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -0,0 +1,70 @@ +import moment from 'moment'; +import { getMatch, getSubcourse } from '../../../graphql/util'; +import { getAllConversations, markConversationAsReadOnly } from '../../../common/chat'; +import { getLogger } from '../../../common/logger/logger'; + +const logger = getLogger(); +enum ConversationType { + SUBCOURSE = 'subcourse', + PROSPECT_SUBCOURSE = 'prospectSubcourse', + MATCH = 'match', +} + +async function isActive(id: number, conversationType: ConversationType): Promise { + const today = moment().endOf('day'); + if (conversationType === 'match') { + const match = await getMatch(id); + const dissolvedAtPlus30Days = moment(match.dissolvedAt).add(30, 'days'); + return dissolvedAtPlus30Days < today; + } + + if (conversationType === 'subcourse' || conversationType === 'prospectSubcourse') { + const subcourse = await getSubcourse(id); + // TODO what if subcourse is cancelled + const isSubcourseCancelled = subcourse.cancelled; + + const lastLecutre = subcourse.lecture.sort((a, b) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); + const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); + + return lastLecturePlus30Days < today; + } + return false; +} + +export default async function flagInactiveConversationsAsReadonly() { + const conversations = await getAllConversations(); + const conversationIds = conversations.data + .map(async (conversation) => { + let shouldMarkAsReadonly = false; + + if (conversation.custom.subcourse) { + const subcourseIds: number[] = conversation.custom.subcourse; + const allSubcoursesActive = subcourseIds.some((id) => isActive(id, ConversationType.SUBCOURSE)); + shouldMarkAsReadonly = !allSubcoursesActive; + } else if (conversation.custom.prospectSubcourse) { + const prospectSubcourses: number[] = conversation.custom.prospectSubcourse; + const allProspectSubcoursesActive = prospectSubcourses.some((id) => isActive(id, ConversationType.PROSPECT_SUBCOURSE)); + shouldMarkAsReadonly = !allProspectSubcoursesActive; + } else if (conversation.custom.match) { + const matchId = conversation.custom.match.matchId; + const isActiveConversation = await isActive(matchId, ConversationType.MATCH); + shouldMarkAsReadonly = !isActiveConversation; + } + + if (shouldMarkAsReadonly) { + return conversation.id; + } + + return null; + }) + .reduce((acc, conversationId) => { + if (!null) { + acc.push(conversationId); + } + return acc; + }, []); + + conversationIds.forEach((id) => markConversationAsReadOnly(id)); + + logger.info(`Mark conversations without purpose as readonly.`); +} From 08bec644df1b0aaf1e9afa1bb73a6e7413bdb9a1 Mon Sep 17 00:00:00 2001 From: Lomy Date: Fri, 7 Jul 2023 12:21:13 +0200 Subject: [PATCH 21/31] fix async --- common/chat/conversation.ts | 3 +- jobs/manualExecution.ts | 5 +++ jobs/periodic/flag-old-conversations/index.ts | 37 +++++++++---------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 96f14f528..cc0e835d8 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -75,7 +75,7 @@ const getConversation = async (conversationId: string): Promise => { +const getAllConversations = async () => { const response = await fetch(`${TALKJS_CONVERSATION_API_URL}`, { method: 'GET', headers: { @@ -338,6 +338,7 @@ async function markConversationAsReadOnlyForPupils(conversationId: string): Prom await checkResponseStatus(response); } } catch (error) { + console.log('ERROR on mark convo as readonly', error); throw new Error(error); } } diff --git a/jobs/manualExecution.ts b/jobs/manualExecution.ts index f7a0ea833..840758785 100644 --- a/jobs/manualExecution.ts +++ b/jobs/manualExecution.ts @@ -14,6 +14,7 @@ import syncToWebflow from './periodic/sync-to-webflow'; import * as Notification from '../common/notification'; import { runInterestConfirmations } from '../common/match/pool'; import migrateLecturesToAppointment from './migrate-lectures-to-appointment'; +import flagInactiveConversationsAsReadonly from './periodic/flag-old-conversations'; // Run inside the Web Dyno via GraphQL (mutation _executeJob) // Run inside the Job Dyno via npm run jobs --execute { await migrateLecturesToAppointment(); break; } + case 'flagOldConversations': { + await flagInactiveConversationsAsReadonly(); + break; + } default: { throw new Error(`Did not find job ${job}`); } diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index 292dfa87d..e5e453e5b 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -15,7 +15,7 @@ async function isActive(id: number, conversationType: ConversationType): Promise if (conversationType === 'match') { const match = await getMatch(id); const dissolvedAtPlus30Days = moment(match.dissolvedAt).add(30, 'days'); - return dissolvedAtPlus30Days < today; + return dissolvedAtPlus30Days.isBefore(today); } if (conversationType === 'subcourse' || conversationType === 'prospectSubcourse') { @@ -26,29 +26,31 @@ async function isActive(id: number, conversationType: ConversationType): Promise const lastLecutre = subcourse.lecture.sort((a, b) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); - return lastLecturePlus30Days < today; + return lastLecturePlus30Days.isBefore(today); } return false; } export default async function flagInactiveConversationsAsReadonly() { const conversations = await getAllConversations(); - const conversationIds = conversations.data - .map(async (conversation) => { - let shouldMarkAsReadonly = false; - + const conversationIds = await Promise.all( + conversations.data.map(async (conversation) => { + let shouldMarkAsReadonly; if (conversation.custom.subcourse) { - const subcourseIds: number[] = conversation.custom.subcourse; + const subcourseIds: number[] = JSON.parse(conversation.custom.subcourse); const allSubcoursesActive = subcourseIds.some((id) => isActive(id, ConversationType.SUBCOURSE)); shouldMarkAsReadonly = !allSubcoursesActive; - } else if (conversation.custom.prospectSubcourse) { + } + if (conversation.custom.prospectSubcourse) { const prospectSubcourses: number[] = conversation.custom.prospectSubcourse; const allProspectSubcoursesActive = prospectSubcourses.some((id) => isActive(id, ConversationType.PROSPECT_SUBCOURSE)); shouldMarkAsReadonly = !allProspectSubcoursesActive; - } else if (conversation.custom.match) { - const matchId = conversation.custom.match.matchId; - const isActiveConversation = await isActive(matchId, ConversationType.MATCH); - shouldMarkAsReadonly = !isActiveConversation; + } + if (conversation.custom.match) { + const match = JSON.parse(conversation.custom.match); + const matchId = match.matchId; + const isInactiveConversation = await isActive(matchId, ConversationType.MATCH); + shouldMarkAsReadonly = isInactiveConversation ? true : false; } if (shouldMarkAsReadonly) { @@ -57,14 +59,11 @@ export default async function flagInactiveConversationsAsReadonly() { return null; }) - .reduce((acc, conversationId) => { - if (!null) { - acc.push(conversationId); - } - return acc; - }, []); + ); - conversationIds.forEach((id) => markConversationAsReadOnly(id)); + conversationIds.forEach(async (id) => { + await markConversationAsReadOnly(id); + }); logger.info(`Mark conversations without purpose as readonly.`); } From d43e5c8384a28cfb8cfbaa76c459ab773192a144 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 12 Jul 2023 15:05:43 +0200 Subject: [PATCH 22/31] add reduce --- jobs/periodic/flag-old-conversations/index.ts | 83 +++++++++++-------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index e5e453e5b..476e02398 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -2,6 +2,7 @@ import moment from 'moment'; import { getMatch, getSubcourse } from '../../../graphql/util'; import { getAllConversations, markConversationAsReadOnly } from '../../../common/chat'; import { getLogger } from '../../../common/logger/logger'; +import { TJConversation } from '../../../common/chat/types'; const logger = getLogger(); enum ConversationType { @@ -12,21 +13,23 @@ enum ConversationType { async function isActive(id: number, conversationType: ConversationType): Promise { const today = moment().endOf('day'); - if (conversationType === 'match') { + if (conversationType === ConversationType.MATCH) { const match = await getMatch(id); const dissolvedAtPlus30Days = moment(match.dissolvedAt).add(30, 'days'); return dissolvedAtPlus30Days.isBefore(today); } - if (conversationType === 'subcourse' || conversationType === 'prospectSubcourse') { - const subcourse = await getSubcourse(id); + if (conversationType === ConversationType.SUBCOURSE || conversationType === ConversationType.PROSPECT_SUBCOURSE) { + const subcourse = await getSubcourse(id, true); + // TODO what if subcourse is cancelled const isSubcourseCancelled = subcourse.cancelled; - const lastLecutre = subcourse.lecture.sort((a, b) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); - const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); - - return lastLecturePlus30Days.isBefore(today); + if (subcourse) { + const lastLecutre = subcourse.lecture.sort((a, b) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); + const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); + return lastLecturePlus30Days.isBefore(today); + } } return false; } @@ -34,36 +37,48 @@ async function isActive(id: number, conversationType: ConversationType): Promise export default async function flagInactiveConversationsAsReadonly() { const conversations = await getAllConversations(); const conversationIds = await Promise.all( - conversations.data.map(async (conversation) => { - let shouldMarkAsReadonly; - if (conversation.custom.subcourse) { - const subcourseIds: number[] = JSON.parse(conversation.custom.subcourse); - const allSubcoursesActive = subcourseIds.some((id) => isActive(id, ConversationType.SUBCOURSE)); - shouldMarkAsReadonly = !allSubcoursesActive; - } - if (conversation.custom.prospectSubcourse) { - const prospectSubcourses: number[] = conversation.custom.prospectSubcourse; - const allProspectSubcoursesActive = prospectSubcourses.some((id) => isActive(id, ConversationType.PROSPECT_SUBCOURSE)); - shouldMarkAsReadonly = !allProspectSubcoursesActive; - } - if (conversation.custom.match) { - const match = JSON.parse(conversation.custom.match); - const matchId = match.matchId; - const isInactiveConversation = await isActive(matchId, ConversationType.MATCH); - shouldMarkAsReadonly = isInactiveConversation ? true : false; - } + conversations.data + .map(async (conversation: TJConversation) => { + let shouldMarkAsReadonly: boolean; + if (conversation.custom.subcourse) { + const subcourseIds: number[] = JSON.parse(conversation.custom.subcourse); - if (shouldMarkAsReadonly) { - return conversation.id; - } + const allSubcoursesActive = subcourseIds.some(async (id) => await isActive(id, ConversationType.SUBCOURSE)); + shouldMarkAsReadonly = !allSubcoursesActive; + } + if (conversation.custom.prospectSubcourse) { + const prospectSubcourses: number[] = conversation.custom.prospectSubcourse; + const allProspectSubcoursesActive = prospectSubcourses.some((id) => isActive(id, ConversationType.PROSPECT_SUBCOURSE)); + shouldMarkAsReadonly = !allProspectSubcoursesActive; + } + if (conversation.custom.match) { + const match = JSON.parse(conversation.custom.match); + const matchId = match.matchId; + const isInactiveConversation = await isActive(matchId, ConversationType.MATCH); + shouldMarkAsReadonly = isInactiveConversation ? true : false; + } - return null; - }) + if (shouldMarkAsReadonly) { + return conversation.id; + } + + return null; + }) + .reduce((acc, curr) => { + if (curr !== null) { + acc.push(curr); + } + return acc; + }, []) ); - conversationIds.forEach(async (id) => { - await markConversationAsReadOnly(id); - }); + console.log('CONVERSATION IDS TO MARK', conversationIds); + if (conversationIds.length > 0) { + conversationIds.forEach(async (id) => { + await markConversationAsReadOnly(id); + }); + logger.info(`Mark conversations without purpose as readonly.`); + } - logger.info(`Mark conversations without purpose as readonly.`); + logger.info('No conversation to mark as readonly'); } From 92c2678d476a8cebf622d6bc9e1c4ff10cb517b2 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 12 Jul 2023 15:05:49 +0200 Subject: [PATCH 23/31] remove bbb --- graphql/subcourse/mutations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 2ffcf2955..24200ebcc 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -17,7 +17,6 @@ import { getLogger } from '../../common/logger/logger'; import { sendGuestInvitationMail, sendPupilCoursePromotion } from '../../common/mails/courses'; import { prisma } from '../../common/prisma'; import { getUserIdTypeORM, getUserTypeORM, userForPupil, userForStudent } from '../../common/user'; -import { createBBBMeeting, createOrUpdateCourseAttendanceLog, getMeetingUrl, isBBBMeetingRunning, startBBBMeeting } from '../../common/util/bbb'; import { PrerequisiteError } from '../../common/util/error'; import { getSessionPupil, getSessionStudent, isElevated, isSessionPupil, isSessionStudent } from '../authentication'; import { AuthorizedDeferred, hasAccess, Role } from '../authorizations'; From 775b56cb17728a70db98fd03b78af616d5dfeca5 Mon Sep 17 00:00:00 2001 From: Lomy Date: Mon, 17 Jul 2023 13:47:23 +0200 Subject: [PATCH 24/31] adjust clean up function --- jobs/periodic/flag-old-conversations/index.ts | 104 ++++++++---------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index 476e02398..eb168b45e 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -2,83 +2,67 @@ import moment from 'moment'; import { getMatch, getSubcourse } from '../../../graphql/util'; import { getAllConversations, markConversationAsReadOnly } from '../../../common/chat'; import { getLogger } from '../../../common/logger/logger'; -import { TJConversation } from '../../../common/chat/types'; +import { Lecture } from '../../../graphql/generated'; const logger = getLogger(); -enum ConversationType { - SUBCOURSE = 'subcourse', - PROSPECT_SUBCOURSE = 'prospectSubcourse', - MATCH = 'match', -} -async function isActive(id: number, conversationType: ConversationType): Promise { +async function isActiveMatch(id: number): Promise { const today = moment().endOf('day'); - if (conversationType === ConversationType.MATCH) { - const match = await getMatch(id); - const dissolvedAtPlus30Days = moment(match.dissolvedAt).add(30, 'days'); - return dissolvedAtPlus30Days.isBefore(today); - } - - if (conversationType === ConversationType.SUBCOURSE || conversationType === ConversationType.PROSPECT_SUBCOURSE) { - const subcourse = await getSubcourse(id, true); + const match = await getMatch(id); + const dissolvedAtPlus30Days = moment(match.dissolvedAt).add(30, 'days'); + return !dissolvedAtPlus30Days.isBefore(today); +} - // TODO what if subcourse is cancelled - const isSubcourseCancelled = subcourse.cancelled; +async function isActiveSubcourse(id: number): Promise { + const today = moment().endOf('day'); + const subcourse = await getSubcourse(id, true); + const isSubcourseCancelled = subcourse.cancelled; - if (subcourse) { - const lastLecutre = subcourse.lecture.sort((a, b) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); - const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); - return lastLecturePlus30Days.isBefore(today); - } + if (isSubcourseCancelled) { + return false; + } else { + const lastLecutre = subcourse.lecture.sort((a: Lecture, b: Lecture) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); + const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); + const is30DaysBeforeToday = lastLecturePlus30Days.isBefore(today); + return !is30DaysBeforeToday; } - return false; } export default async function flagInactiveConversationsAsReadonly() { const conversations = await getAllConversations(); - const conversationIds = await Promise.all( - conversations.data - .map(async (conversation: TJConversation) => { - let shouldMarkAsReadonly: boolean; - if (conversation.custom.subcourse) { - const subcourseIds: number[] = JSON.parse(conversation.custom.subcourse); + const conversationIds: string[] = []; - const allSubcoursesActive = subcourseIds.some(async (id) => await isActive(id, ConversationType.SUBCOURSE)); - shouldMarkAsReadonly = !allSubcoursesActive; - } - if (conversation.custom.prospectSubcourse) { - const prospectSubcourses: number[] = conversation.custom.prospectSubcourse; - const allProspectSubcoursesActive = prospectSubcourses.some((id) => isActive(id, ConversationType.PROSPECT_SUBCOURSE)); - shouldMarkAsReadonly = !allProspectSubcoursesActive; - } - if (conversation.custom.match) { - const match = JSON.parse(conversation.custom.match); - const matchId = match.matchId; - const isInactiveConversation = await isActive(matchId, ConversationType.MATCH); - shouldMarkAsReadonly = isInactiveConversation ? true : false; - } + for (const conversation of conversations.data) { + let shouldMarkAsReadonly: boolean; + if (conversation.custom.subcourse) { + const subcourseIds: number[] = JSON.parse(conversation.custom.subcourse); + const allSubcoursesActive = await Promise.all(subcourseIds.map((id) => isActiveSubcourse(id))); + shouldMarkAsReadonly = allSubcoursesActive.every((active) => active === false); + } + if (conversation.custom.prospectSubcourse) { + const prospectSubcourses: number[] = conversation.custom.prospectSubcourse; + const allProspectSubcoursesActive = await Promise.all(prospectSubcourses.map((id) => isActiveSubcourse(id))); + shouldMarkAsReadonly = allProspectSubcoursesActive.every((active) => active === false); + } - if (shouldMarkAsReadonly) { - return conversation.id; - } + if (conversation.custom.match) { + const match = JSON.parse(conversation.custom.match); + const matchId = match.matchId; + const isMatchActive = await isActiveMatch(matchId); + shouldMarkAsReadonly = !isMatchActive; + } - return null; - }) - .reduce((acc, curr) => { - if (curr !== null) { - acc.push(curr); - } - return acc; - }, []) - ); + if (shouldMarkAsReadonly) { + conversationIds.push(conversation.id); + } + } - console.log('CONVERSATION IDS TO MARK', conversationIds); if (conversationIds.length > 0) { - conversationIds.forEach(async (id) => { + for (const id of conversationIds) { await markConversationAsReadOnly(id); - }); + } logger.info(`Mark conversations without purpose as readonly.`); + } else { + logger.info('No conversation to mark as readonly'); } - - logger.info('No conversation to mark as readonly'); } From ffad981ba1ec3b237a8ea54bf56139fc415af417 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 19 Jul 2023 07:40:58 +0200 Subject: [PATCH 25/31] add send system message --- common/chat/conversation.ts | 3 +- graphql/subcourse/mutations.ts | 12 ++--- jobs/periodic/flag-old-conversations/index.ts | 49 ++++++++++++++++--- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 9f943a90d..6699f18c4 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -13,7 +13,7 @@ import { import { User } from '../user'; import { getOrCreateChatUser } from './user'; import { prisma } from '../prisma'; -import { AllConversations, ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types'; +import { ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types'; import { getMyContacts } from './contacts'; import systemMessages from './localization'; @@ -111,6 +111,7 @@ const getOrCreateOneOnOneConversation = async ( if (isChatReadOnly) { await markConversationAsWriteable(participantsConversationId); + await sendSystemMessage(systemMessages.de.reactivated, participantsConversationId, SystemMessage.ONE_ON_ONE_REACTIVATE); } } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index f0db7a13f..cbeabb526 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -130,11 +130,7 @@ export class MutateSubcourseResolver { await prisma.subcourse_instructors_student.create({ data: { subcourseId, studentId } }); await addGroupAppointmentsOrganizer(subcourseId, studentUserId); if (subcourse.conversationId) { - await addParticipant( - newInstructorUser, - subcourse.conversationId, - subcourse.groupChatType === ChatType.ANNOUNCEMENT ? ChatType.ANNOUNCEMENT : ChatType.NORMAL - ); + await addParticipant(newInstructorUser, subcourse.conversationId, subcourse.groupChatType as ChatType); } logger.info(`Student (${studentId}) was added as an instructor to Subcourse(${subcourseId}) by User(${context.user!.userID})`); @@ -223,7 +219,7 @@ export class MutateSubcourseResolver { await joinSubcourse(subcourse, pupil, true); await addGroupAppointmentsParticipant(subcourseId, user.userID); if (subcourse.conversationId) { - await addParticipant(user, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? ChatType.ANNOUNCEMENT : ChatType.NORMAL); + await addParticipant(user, subcourse.conversationId, subcourse.groupChatType as ChatType); } return true; @@ -241,7 +237,7 @@ export class MutateSubcourseResolver { const subcourse = await getSubcourse(subcourseId); await joinSubcourse(subcourse, pupil, false); await addGroupAppointmentsParticipant(subcourseId, user.userID); - await addParticipant(user, subcourse.conversationId, subcourse.groupChatType === ChatType.ANNOUNCEMENT ? ChatType.ANNOUNCEMENT : ChatType.NORMAL); + await addParticipant(user, subcourse.conversationId, subcourse.groupChatType as ChatType); return true; } @@ -274,7 +270,7 @@ export class MutateSubcourseResolver { await joinSubcourse(subcourse, pupil, true); await addGroupAppointmentsParticipant(subcourseId, user.userID); if (subcourse.conversationId) { - await addParticipant(user, subcourse.conversationId, subcourse.groupChatType); + await addParticipant(user, subcourse.conversationId, subcourse.groupChatType as ChatType); } return true; diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index eb168b45e..187a18d7d 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -1,11 +1,22 @@ import moment from 'moment'; import { getMatch, getSubcourse } from '../../../graphql/util'; -import { getAllConversations, markConversationAsReadOnly } from '../../../common/chat'; +import { getAllConversations, markConversationAsReadOnly, sendSystemMessage } from '../../../common/chat'; import { getLogger } from '../../../common/logger/logger'; import { Lecture } from '../../../graphql/generated'; +import { SystemMessage } from '../../../common/chat/types'; +import systemMessages from '../../../common/chat/localization'; const logger = getLogger(); +enum ConversationType { + GROUP = 'group', + ONE_ON_ONE = 'one_on_one', +} + +type conversationsToDeactivate = { + id: string; + conversationType: ConversationType; +}; async function isActiveMatch(id: number): Promise { const today = moment().endOf('day'); const match = await getMatch(id); @@ -30,7 +41,8 @@ async function isActiveSubcourse(id: number): Promise { export default async function flagInactiveConversationsAsReadonly() { const conversations = await getAllConversations(); - const conversationIds: string[] = []; + // const conversationIds: string[] = []; + const conversationsTo: conversationsToDeactivate[] = []; for (const conversation of conversations.data) { let shouldMarkAsReadonly: boolean; @@ -53,13 +65,38 @@ export default async function flagInactiveConversationsAsReadonly() { } if (shouldMarkAsReadonly) { - conversationIds.push(conversation.id); + // conversationIds.push(conversation.id); + if (conversation.custom.match) { + conversationsTo.push({ + id: conversation.id, + conversationType: ConversationType.ONE_ON_ONE, + }); + } else { + conversationsTo.push({ + id: conversation.id, + conversationType: ConversationType.GROUP, + }); + } } } - if (conversationIds.length > 0) { - for (const id of conversationIds) { - await markConversationAsReadOnly(id); + // if (conversationIds.length > 0) { + // for (const id of conversationIds) { + // await markConversationAsReadOnly(id); + // } + // logger.info(`Mark conversations without purpose as readonly.`); + // } else { + // logger.info('No conversation to mark as readonly'); + // } + + if (conversationsTo.length > 0) { + for (const convo of conversationsTo) { + await markConversationAsReadOnly(convo.id); + if (convo.conversationType === ConversationType.ONE_ON_ONE) { + await sendSystemMessage(systemMessages.de.deactivated, convo.id, SystemMessage.ONE_ON_ONE_OVER); + } else { + await sendSystemMessage(systemMessages.de.deactivated, convo.id, SystemMessage.GROUP_OVER); + } } logger.info(`Mark conversations without purpose as readonly.`); } else { From 69d711b51c3c9f0938311d2d04a82ac99670710b Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 19 Jul 2023 08:10:38 +0200 Subject: [PATCH 26/31] add message on clean up --- jobs/periodic/flag-old-conversations/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index 187a18d7d..2c0d0a99b 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -41,7 +41,6 @@ async function isActiveSubcourse(id: number): Promise { export default async function flagInactiveConversationsAsReadonly() { const conversations = await getAllConversations(); - // const conversationIds: string[] = []; const conversationsTo: conversationsToDeactivate[] = []; for (const conversation of conversations.data) { @@ -80,15 +79,6 @@ export default async function flagInactiveConversationsAsReadonly() { } } - // if (conversationIds.length > 0) { - // for (const id of conversationIds) { - // await markConversationAsReadOnly(id); - // } - // logger.info(`Mark conversations without purpose as readonly.`); - // } else { - // logger.info('No conversation to mark as readonly'); - // } - if (conversationsTo.length > 0) { for (const convo of conversationsTo) { await markConversationAsReadOnly(convo.id); From 6fa41b07403d2a9369ecdbc92e2c2cb0a1732d32 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 19 Jul 2023 08:12:44 +0200 Subject: [PATCH 27/31] remove comment --- jobs/periodic/flag-old-conversations/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index 2c0d0a99b..56de598ad 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -64,7 +64,6 @@ export default async function flagInactiveConversationsAsReadonly() { } if (shouldMarkAsReadonly) { - // conversationIds.push(conversation.id); if (conversation.custom.match) { conversationsTo.push({ id: conversation.id, From f311f48df4f00ef60df64795325b4792f790451f Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 19 Jul 2023 13:40:36 +0200 Subject: [PATCH 28/31] review changes --- common/chat/conversation.ts | 8 ++-- common/chat/helper.ts | 10 ++-- common/chat/types.ts | 4 +- jobs/manualExecution.ts | 1 + jobs/periodic/flag-old-conversations/index.ts | 48 +++++++++++-------- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/common/chat/conversation.ts b/common/chat/conversation.ts index 6699f18c4..18572e5f6 100644 --- a/common/chat/conversation.ts +++ b/common/chat/conversation.ts @@ -13,11 +13,13 @@ import { import { User } from '../user'; import { getOrCreateChatUser } from './user'; import { prisma } from '../prisma'; -import { ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types'; +import { AllConversations, ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types'; import { getMyContacts } from './contacts'; import systemMessages from './localization'; +import { getLogger } from '../logger/logger'; dotenv.config(); +const logger = getLogger('Conversation'); const TALKJS_API_URL = `https://api.talkjs.com/v1/${process.env.TALKJS_APP_ID}`; const TALKJS_CONVERSATION_API_URL = `${TALKJS_API_URL}/conversations`; @@ -75,7 +77,7 @@ const getConversation = async (conversationId: string): Promise { +const getAllConversations = async (): Promise => { const response = await fetch(`${TALKJS_CONVERSATION_API_URL}`, { method: 'GET', headers: { @@ -348,7 +350,7 @@ async function markConversationAsReadOnlyForPupils(conversationId: string): Prom await checkResponseStatus(response); } } catch (error) { - console.log('ERROR on mark convo as readonly', error); + logger.error('Could not mark conversation as readonly', error); throw new Error(error); } } diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 19bf6fecc..0bb17e58b 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -8,7 +8,7 @@ import { createHmac } from 'crypto'; import { Subcourse } from '../../graphql/generated'; import { getPupil, getStudent } from '../../graphql/util'; import { getConversation } from './conversation'; -import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types'; +import { ChatAccess, ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types'; import { MatchContactPupil, MatchContactStudent } from './contacts'; type TalkJSUserId = `${'pupil' | 'student'}_${number}`; @@ -130,6 +130,10 @@ const getMatcheeConversation = async (matchees: { studentId: number; pupilId: nu return { conversation, conversationId }; }; +const countChatParticipants = (conversation: Conversation): number => { + return Object.keys(conversation.participants).length; +}; + const checkChatMembersAccessRights = (conversation: Conversation): { readWriteMembers: string[]; readMembers: string[] } => { const readWriteMembers: string[] = []; const readMembers: string[] = []; @@ -168,7 +172,7 @@ const convertConversationInfosToString = (conversationInfos: ConversationInfos): }; const convertTJConversation = (conversation: TJConversation): Conversation => { - const { id, subject, topicId, photoUrl, welcomeMessages, custom, lastMessage, participants, createdAt } = conversation; + const { id, subject, photoUrl, welcomeMessages, custom, lastMessage, participants, createdAt } = conversation; const convertedCustom: ChatMetaData = custom ? { @@ -183,7 +187,6 @@ const convertTJConversation = (conversation: TJConversation): Conversation => { return { id, subject, - topicId, photoUrl, welcomeMessages, custom: convertedCustom, @@ -203,6 +206,7 @@ export { createChatSignature, getMatchByMatchees, createOneOnOneId, + countChatParticipants, getConversationId, getMatcheeConversation, checkChatMembersAccessRights, diff --git a/common/chat/types.ts b/common/chat/types.ts index c5432dcf5..f8ffe1a1f 100644 --- a/common/chat/types.ts +++ b/common/chat/types.ts @@ -9,7 +9,7 @@ export enum ContactReason { CONTACT = 'contact', } -export type Conversation = Pick & { +export type Conversation = Pick & { custom?: ChatMetaData; lastMessage?: Message; participants: { @@ -71,5 +71,5 @@ export enum ChatAccess { } export type AllConversations = { - data: Conversation[]; + data: TJConversation[]; }; diff --git a/jobs/manualExecution.ts b/jobs/manualExecution.ts index 07f704f5a..e3a1f811e 100644 --- a/jobs/manualExecution.ts +++ b/jobs/manualExecution.ts @@ -53,6 +53,7 @@ export const executeJob = async (job) => { } case 'flagOldConversations': { await flagInactiveConversationsAsReadonly(); + break; } case 'sendSlackStatistics': { await postStatisticsToSlack(); diff --git a/jobs/periodic/flag-old-conversations/index.ts b/jobs/periodic/flag-old-conversations/index.ts index 56de598ad..25ea0eb9c 100644 --- a/jobs/periodic/flag-old-conversations/index.ts +++ b/jobs/periodic/flag-old-conversations/index.ts @@ -5,8 +5,9 @@ import { getLogger } from '../../../common/logger/logger'; import { Lecture } from '../../../graphql/generated'; import { SystemMessage } from '../../../common/chat/types'; import systemMessages from '../../../common/chat/localization'; +import { checkChatMembersAccessRights, countChatParticipants } from '../../../common/chat/helper'; -const logger = getLogger(); +const logger = getLogger('FlagOldConversationsAsRO'); enum ConversationType { GROUP = 'group', @@ -17,6 +18,9 @@ type conversationsToDeactivate = { id: string; conversationType: ConversationType; }; + +// one to one chats (if match) whose match was dissolved 30 days ago should be "disabled" (readonly). +// This will allow users to continue writing for another 30 days after match disolving. async function isActiveMatch(id: number): Promise { const today = moment().endOf('day'); const match = await getMatch(id); @@ -31,20 +35,29 @@ async function isActiveSubcourse(id: number): Promise { if (isSubcourseCancelled) { return false; - } else { - const lastLecutre = subcourse.lecture.sort((a: Lecture, b: Lecture) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); - const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); - const is30DaysBeforeToday = lastLecturePlus30Days.isBefore(today); - return !is30DaysBeforeToday; } + + const lastLecutre = subcourse.lecture.sort((a: Lecture, b: Lecture) => moment(a.start).milliseconds() - moment(b.start).milliseconds()).pop(); + const lastLecturePlus30Days = moment(lastLecutre.start).add(30, 'days'); + const is30DaysBeforeToday = lastLecturePlus30Days.isBefore(today); + return !is30DaysBeforeToday; } export default async function flagInactiveConversationsAsReadonly() { const conversations = await getAllConversations(); - const conversationsTo: conversationsToDeactivate[] = []; + const conversationsToFlag: conversationsToDeactivate[] = []; for (const conversation of conversations.data) { let shouldMarkAsReadonly: boolean; + + // to prevent to flag already deactivated chats we check if the conversation is already readonly (only readMembers) + const countParticipants = countChatParticipants(conversation); + const { readMembers } = checkChatMembersAccessRights(conversation); + const isChatReadOnly = readMembers.length === countParticipants; + if (isChatReadOnly) { + return; + } + if (conversation.custom.subcourse) { const subcourseIds: number[] = JSON.parse(conversation.custom.subcourse); const allSubcoursesActive = await Promise.all(subcourseIds.map((id) => isActiveSubcourse(id))); @@ -64,30 +77,23 @@ export default async function flagInactiveConversationsAsReadonly() { } if (shouldMarkAsReadonly) { - if (conversation.custom.match) { - conversationsTo.push({ - id: conversation.id, - conversationType: ConversationType.ONE_ON_ONE, - }); - } else { - conversationsTo.push({ - id: conversation.id, - conversationType: ConversationType.GROUP, - }); - } + conversationsToFlag.push({ + id: conversation.id, + conversationType: conversation.custom.match ? ConversationType.ONE_ON_ONE : ConversationType.GROUP, + }); } } - if (conversationsTo.length > 0) { - for (const convo of conversationsTo) { + if (conversationsToFlag.length > 0) { + for (const convo of conversationsToFlag) { await markConversationAsReadOnly(convo.id); if (convo.conversationType === ConversationType.ONE_ON_ONE) { await sendSystemMessage(systemMessages.de.deactivated, convo.id, SystemMessage.ONE_ON_ONE_OVER); } else { await sendSystemMessage(systemMessages.de.deactivated, convo.id, SystemMessage.GROUP_OVER); } + logger.info('Mark converstation as readonly', { conversationId: convo.id, conversationType: convo.conversationType }); } - logger.info(`Mark conversations without purpose as readonly.`); } else { logger.info('No conversation to mark as readonly'); } From 480947299dfe254fdd3eff74e1f1d5bcff82bfad Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 19 Jul 2023 13:41:04 +0200 Subject: [PATCH 29/31] no contacts from past courses --- common/chat/contacts.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/chat/contacts.ts b/common/chat/contacts.ts index ddad539af..7e99111b6 100644 --- a/common/chat/contacts.ts +++ b/common/chat/contacts.ts @@ -5,6 +5,7 @@ import { pupil as Pupil, student as Student } from '@prisma/client'; import { User } from '../user'; import { ContactReason } from './types'; import { isPupilContact, isStudentContact } from './helper'; +import moment from 'moment'; export type UserContactType = { userID: string; @@ -94,6 +95,7 @@ const getMatchContactsForUser = async (user: User): Promise => { assert(pupil.pupilId, 'Pupil must have an pupilId'); + const today = moment().toISOString(); const studentsWithSubcourseIds = await prisma.student.findMany({ where: { subcourse_instructors_student: { @@ -102,6 +104,7 @@ const getInstructorsForPupilSubcourses = async (pupil: User): Promise => { assert(student.studentId, 'Student must have an studentId'); + const today = moment().toISOString(); + const pupilsWithSubcourseIds = await prisma.pupil.findMany({ where: { subcourse_participants_pupil: { @@ -146,6 +151,7 @@ const getSubcourseParticipantContactForUser = async (student: User): Promise Date: Thu, 20 Jul 2023 07:51:50 +0200 Subject: [PATCH 30/31] every to some --- common/chat/contacts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/chat/contacts.ts b/common/chat/contacts.ts index 7e99111b6..6697fa441 100644 --- a/common/chat/contacts.ts +++ b/common/chat/contacts.ts @@ -104,7 +104,7 @@ const getInstructorsForPupilSubcourses = async (pupil: User): Promise Date: Thu, 20 Jul 2023 12:16:41 +0200 Subject: [PATCH 31/31] adjust to yesterday --- common/chat/contacts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/chat/contacts.ts b/common/chat/contacts.ts index 6697fa441..d83ea2cb6 100644 --- a/common/chat/contacts.ts +++ b/common/chat/contacts.ts @@ -95,7 +95,7 @@ const getMatchContactsForUser = async (user: User): Promise => { assert(pupil.pupilId, 'Pupil must have an pupilId'); - const today = moment().toISOString(); + const yesterday = moment().subtract(1, 'day').toISOString(); const studentsWithSubcourseIds = await prisma.student.findMany({ where: { subcourse_instructors_student: { @@ -104,7 +104,7 @@ const getInstructorsForPupilSubcourses = async (pupil: User): Promise => { assert(student.studentId, 'Student must have an studentId'); - const today = moment().toISOString(); + const yesterday = moment().subtract(1, 'day').toISOString(); const pupilsWithSubcourseIds = await prisma.pupil.findMany({ where: { @@ -151,7 +151,7 @@ const getSubcourseParticipantContactForUser = async (student: User): Promise