Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: clear conversations without purpose #717

Merged
merged 54 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
80bebc2
add participant to chat on subcourse join
LomyW Jun 2, 2023
d1b4b97
mark matchee convo as readonly or writeable
LomyW Jun 7, 2023
4d44be1
Merge branch 'master' into feat/724-reactivate-conversation
LomyW Jun 7, 2023
db94d66
subcourse purposes
LomyW Jun 7, 2023
5592f7f
Merge branch 'master' into feat/724-reactivate-conversation
LomyW Jun 12, 2023
55c2817
add chat access enum
LomyW Jun 12, 2023
3b699f4
check if members have write rights
LomyW Jun 12, 2023
77fb22d
Merge remote-tracking branch 'origin/master' into feat/724-reactivate…
LomyW Jun 13, 2023
ab3699f
add new chat meta data
LomyW Jun 13, 2023
2a4b2ef
convert meta data as string
LomyW Jun 14, 2023
b0f172d
Merge remote-tracking branch 'origin/master' into feat/chat-meta-data
LomyW Jun 14, 2023
fe907de
Merge remote-tracking branch 'origin/master' into feat/724-reactivate…
LomyW Jun 14, 2023
032ef88
add groupType to metadata, dont stringify strings
LomyW Jun 14, 2023
1b33905
Merge branch 'master' into feat/chat-meta-data
LomyW Jun 15, 2023
603d11a
get matchId and subcourseIds for contact
LomyW Jun 15, 2023
4a8e8bd
add mutation for contact chats
LomyW Jun 15, 2023
71a51bf
Merge branch 'master' into feat/chat-meta-data
LomyW Jun 15, 2023
7fd7207
Merge branch 'master' into feat/chat-meta-data
LomyW Jun 16, 2023
edd53cb
adjust integration-test
LomyW Jun 16, 2023
3f1c13b
Merge remote-tracking branch 'origin/master' into feat/chat-meta-data
LomyW Jun 16, 2023
81bd15a
review changes: renamings and types
LomyW Jun 16, 2023
5e3b85e
Fix tests & type
Jun 16, 2023
5ebd062
Merge branch 'master' into feat/chat-meta-data
LomyW Jun 19, 2023
46c2de7
Merge remote-tracking branch 'origin/feat/chat-meta-data' into feat/7…
LomyW Jun 19, 2023
057bb4a
functions to remove subcourse and match
LomyW Jun 19, 2023
f40eaac
no dissolved or cancelled contact
LomyW Jun 19, 2023
c774b3e
get all conversations
LomyW Jun 19, 2023
a27ff73
Merge branch 'master' into feat/chat-meta-data
LomyW Jun 26, 2023
a9c7b40
Merge branch 'feat/chat-meta-data' into feat/724-reactivate-conversation
LomyW Jun 26, 2023
ed3e445
Merge branch 'master' into feat/chat-meta-data
LomyW Jun 28, 2023
37ea77a
review changes
LomyW Jun 28, 2023
8d54216
Merge remote-tracking branch 'origin/feat/chat-meta-data' into feat/7…
LomyW Jun 29, 2023
1e9ec46
Merge remote-tracking branch 'origin/master' into feat/724-reactivate…
LomyW Jul 3, 2023
40624ce
Merge branch 'master' into feat/724-reactivate-conversation
LomyW Jul 4, 2023
3e4f9ab
add cron job file
LomyW Jul 4, 2023
b3e2daa
remove useless functions
LomyW Jul 4, 2023
3249d13
add function to flag inactive conversations
LomyW Jul 5, 2023
a4d3b09
Merge branch 'master' into feat/724-reactivate-conversation
LomyW Jul 6, 2023
57967a6
Merge remote-tracking branch 'origin/feat/724-reactivate-conversation…
LomyW Jul 6, 2023
08bec64
fix async
LomyW Jul 7, 2023
c244b5e
Merge remote-tracking branch 'origin/master' into feat/724-reactivate…
LomyW Jul 12, 2023
5ff99e3
Merge branch 'feat/724-reactivate-conversation' into feat/723-clear-c…
LomyW Jul 12, 2023
d43e5c8
add reduce
LomyW Jul 12, 2023
92c2678
remove bbb
LomyW Jul 12, 2023
775b56c
adjust clean up function
LomyW Jul 17, 2023
1e7eb07
Merge remote-tracking branch 'origin/master' into feat/723-clear-conv…
LomyW Jul 18, 2023
ffad981
add send system message
LomyW Jul 19, 2023
69d711b
add message on clean up
LomyW Jul 19, 2023
6fa41b0
remove comment
LomyW Jul 19, 2023
f311f48
review changes
LomyW Jul 19, 2023
4809472
no contacts from past courses
LomyW Jul 19, 2023
2e7d386
Merge branch 'master' into feat/723-clear-conversations-without-purpose
LomyW Jul 19, 2023
31ee30b
every to some
LomyW Jul 20, 2023
d4fad73
adjust to yesterday
LomyW Jul 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions common/chat/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,6 +95,7 @@ const getMatchContactsForUser = async (user: User): Promise<MatchContactStudent[
};
const getInstructorsForPupilSubcourses = async (pupil: User): Promise<SubcourseContactStudent[]> => {
assert(pupil.pupilId, 'Pupil must have an pupilId');
const yesterday = moment().subtract(1, 'day').toISOString();
const studentsWithSubcourseIds = await prisma.student.findMany({
where: {
subcourse_instructors_student: {
Expand All @@ -102,6 +104,7 @@ const getInstructorsForPupilSubcourses = async (pupil: User): Promise<SubcourseC
allowChatContactParticipants: true,
subcourse_participants_pupil: { some: { pupilId: pupil.pupilId } },
cancelled: false,
lecture: { some: { start: { gte: yesterday } } },
},
},
},
Expand Down Expand Up @@ -138,6 +141,8 @@ const getInstructorsForPupilSubcourses = async (pupil: User): Promise<SubcourseC
};
const getSubcourseParticipantContactForUser = async (student: User): Promise<SubcourseContactPupil[]> => {
assert(student.studentId, 'Student must have an studentId');
const yesterday = moment().subtract(1, 'day').toISOString();

const pupilsWithSubcourseIds = await prisma.pupil.findMany({
where: {
subcourse_participants_pupil: {
Expand All @@ -146,6 +151,7 @@ const getSubcourseParticipantContactForUser = async (student: User): Promise<Sub
allowChatContactParticipants: true,
subcourse_instructors_student: { some: { studentId: student.studentId } },
cancelled: false,
lecture: { some: { start: { gte: yesterday } } },
},
},
},
Expand Down
11 changes: 7 additions & 4 deletions common/chat/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import {
import { User } from '../user';
import { getOrCreateChatUser } from './user';
import { prisma } from '../prisma';
import { AllConversations, ChatAccess, ContactReason, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types';
import { AllConversations, ChatAccess, ChatType, ContactReason, Conversation, ConversationInfos, SystemMessage, TJConversation } from './types';
import { getMyContacts } from './contacts';
import { chat_type } from '@prisma/client';
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`;
Expand Down Expand Up @@ -112,6 +113,7 @@ const getOrCreateOneOnOneConversation = async (

if (isChatReadOnly) {
await markConversationAsWriteable(participantsConversationId);
await sendSystemMessage(systemMessages.de.reactivated, participantsConversationId, SystemMessage.ONE_ON_ONE_REACTIVATE);
}
}

Expand Down Expand Up @@ -267,7 +269,7 @@ async function deleteConversation(conversationId: string): Promise<void> {
}
}

async function addParticipant(user: User, conversationId: string, chatType?: chat_type): Promise<void> {
async function addParticipant(user: User, conversationId: string, chatType?: ChatType): Promise<void> {
const userId = userIdToTalkJsId(user.userID);
try {
const response = await fetch(`${TALKJS_CONVERSATION_API_URL}/${conversationId}/participants/${userId}`, {
Expand All @@ -277,7 +279,7 @@ async function addParticipant(user: User, conversationId: string, chatType?: cha
'Content-Type': 'application/json',
},
body: JSON.stringify({
access: chatType === chat_type.NORMAL ? ChatAccess.READWRITE : ChatAccess.READ,
access: chatType === ChatType.NORMAL ? ChatAccess.READWRITE : ChatAccess.READ,
}),
});
await checkResponseStatus(response);
Expand Down Expand Up @@ -348,6 +350,7 @@ async function markConversationAsReadOnlyForPupils(conversationId: string): Prom
await checkResponseStatus(response);
}
} catch (error) {
logger.error('Could not mark conversation as readonly', error);
throw new Error(error);
}
}
Expand Down
10 changes: 7 additions & 3 deletions common/chat/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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
? {
Expand All @@ -183,7 +187,6 @@ const convertTJConversation = (conversation: TJConversation): Conversation => {
return {
id,
subject,
topicId,
photoUrl,
welcomeMessages,
custom: convertedCustom,
Expand All @@ -203,6 +206,7 @@ export {
createChatSignature,
getMatchByMatchees,
createOneOnOneId,
countChatParticipants,
getConversationId,
getMatcheeConversation,
checkChatMembersAccessRights,
Expand Down
4 changes: 2 additions & 2 deletions common/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export enum ContactReason {
CONTACT = 'contact',
}

export type Conversation = Pick<ConversationData, 'topicId' | 'id' | 'subject' | 'photoUrl' | 'welcomeMessages'> & {
export type Conversation = Pick<ConversationData, 'id' | 'subject' | 'photoUrl' | 'welcomeMessages'> & {
custom?: ChatMetaData;
lastMessage?: Message;
participants: {
Expand Down Expand Up @@ -71,5 +71,5 @@ export enum ChatAccess {
}

export type AllConversations = {
data: Conversation[];
data: TJConversation[];
};
12 changes: 4 additions & 8 deletions graphql/subcourse/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions jobs/manualExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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';
import { postStatisticsToSlack } from './slack-statistics';

// Run inside the Web Dyno via GraphQL (mutation _executeJob)
Expand Down Expand Up @@ -50,6 +51,10 @@ export const executeJob = async (job) => {
await migrateLecturesToAppointment();
break;
}
case 'flagOldConversations': {
await flagInactiveConversationsAsReadonly();
LomyW marked this conversation as resolved.
Show resolved Hide resolved
break;
}
case 'sendSlackStatistics': {
await postStatisticsToSlack();
break;
Expand Down
100 changes: 100 additions & 0 deletions jobs/periodic/flag-old-conversations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import moment from 'moment';
import { getMatch, getSubcourse } from '../../../graphql/util';
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';
import { checkChatMembersAccessRights, countChatParticipants } from '../../../common/chat/helper';

const logger = getLogger('FlagOldConversationsAsRO');

enum ConversationType {
GROUP = 'group',
ONE_ON_ONE = 'one_on_one',
}

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<boolean> {
const today = moment().endOf('day');
const match = await getMatch(id);
const dissolvedAtPlus30Days = moment(match.dissolvedAt).add(30, 'days');
return !dissolvedAtPlus30Days.isBefore(today);
LomyW marked this conversation as resolved.
Show resolved Hide resolved
}

async function isActiveSubcourse(id: number): Promise<boolean> {
const today = moment().endOf('day');
const subcourse = await getSubcourse(id, true);
const isSubcourseCancelled = subcourse.cancelled;

if (isSubcourseCancelled) {
return false;
}

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();
LomyW marked this conversation as resolved.
Show resolved Hide resolved
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)));
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 (conversation.custom.match) {
const match = JSON.parse(conversation.custom.match);
const matchId = match.matchId;
const isMatchActive = await isActiveMatch(matchId);
shouldMarkAsReadonly = !isMatchActive;
}

if (shouldMarkAsReadonly) {
conversationsToFlag.push({
id: conversation.id,
conversationType: conversation.custom.match ? ConversationType.ONE_ON_ONE : ConversationType.GROUP,
});
}
}

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);
}
LomyW marked this conversation as resolved.
Show resolved Hide resolved
logger.info('Mark converstation as readonly', { conversationId: convo.id, conversationType: convo.conversationType });
}
} else {
logger.info('No conversation to mark as readonly');
}
}
Loading