You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * Contains the APIs and model plugins to send and receive messages. Currently, supports Twilio and * Expo push notifications. */importExpo,{ExpoPushErrorTicket,ExpoPushSuccessTicket,ExpoPushTicket}from"expo-server-sdk";importmongoose,{Document,Schema,Types}from"mongoose";import{logger}from"../logger";import{ConversationDocument,ConversationModelasGeneratedConversationModel,ConversationSchemaasGeneratedConversationSchema,ConversationUser,MessageData,MessageDocument,MessageModel,MessagePushStatus,MessageSchema,}from"./interfaces";importaxiosfrom"axios";// Selectively export from interfacesexport{MessageSchema,MessageModel,MessageDocument}from"./interfaces";constexpo=newExpo({accessToken: process.env.EXPO_ACCESS_TOKEN});constBACKOFF_SECONDS=[5,5,5,15,30];// TODO make these adjustable by the calling app.constDEFAULT_USER_MODEL="User";constDEFAULT_CONVERSATION_MODEL="Conversation";// const DEFAULT_MESSAGE_MODEL = "Message";functionisPopulated(field: any): boolean{if(Array.isArray(field)){if(field.length===0){returnfalse;}else{returnfield[0]._bsontype==="ObjectId";}}else{returnfield._bsontype==="ObjectId";}}functionisExpoPushTicketSuccess(data: ExpoPushTicket): data is ExpoPushSuccessTicket{returndata.status==="ok";}functionisExpoPushTicketError(data: ExpoPushTicket): data is ExpoPushErrorTicket{returndata.status==="error";}exportfunctionuserMessagingPlugin(schema: Schema){schema.add({expoToken: {type: String},messagingMethods: {push: {enabled: {type: Boolean,default: true},optedOut: {type: Boolean,default: false}},sms: {enabled: {type: Boolean,default: true},optedOut: {type: Boolean,default: false}},},conversations: [{conversationId: {type: Schema.Types.ObjectId,ref: DEFAULT_CONVERSATION_MODEL,required: true,},},],});}exportfunctionmessagePlugin(messageSchema: Schema){messageSchema.add({text: {type: String},// Not required, if not specified, shows up as a system message. Your app should handle this// on the frontend.from: {type: Schema.Types.ObjectId,ref: DEFAULT_USER_MODEL,},conversationId: {type: Schema.Types.ObjectId,ref: DEFAULT_CONVERSATION_MODEL,required: true,},pushStatuses: [{// Expo returns a push ticket which tells us whether the Expo servers have accepted our push message.userId: {type: Schema.Types.ObjectId,ref: DEFAULT_USER_MODEL,},ticketStatus: {type: String,enum: ["ok","error"]},// When a ticket is successful, we get a ticket id for querying for push receipt.ticketId: String,// If there was an error communicating with Expo, that message and type will be storied here.ticketErrorMessage: String,ticketErrorType: {type: String,enum: ["DeviceNotRegistered","InvalidCredentials","MessageTooBig","MessageRateExceeded",],},// Receipts come from the iOS and Google push servers and represent whether the push was actually delivered.receiptStatus: {type: String,enum: ["ok","error"]},receiptErrorMessage: String,receiptErrorDetails: String,},],// TODO: Add support for threading messages and replies.});messageSchema.methods={// Ask the Expo server for push receipts to see what the status from Google/Apple is for push.asyncupdatePushReceipts(backoffIndex: number=1){logger.debug(`Updating push receipts for ${this._id}`);constids=this.pushStatuses.map((s: MessagePushStatus)=>{if(s.ticketStatus==="ok"&&s.ticketId&&!s.receiptStatus){returns.ticketId;}returnnull;}).filter((s: string|null)=>s);// Get push receiptsconstres=awaitaxios.post("https://exp.host/--/api/v2/push/getReceipts",{
ids,});for(constticketIdofObject.keys(res.data.data)){constreceipt=res.data.data[ticketId];constpushStatus=this.pushStatuses.find((s: MessagePushStatus)=>s.ticketId===ticketId);if(!pushStatus){logger.error(`Could not update push status for ticketId ${ticketId} in message ${this._id}`);continue;}pushStatus.receiptStatus=receipt.status;if(receipt.status==="error"){pushStatus.receiptErrorMessage=receipt.message;pushStatus.receiptErrorDetails=receipt.details;}}awaitthis.save();// If we don't have all the receipts, we'll keep checking for one minute. After that, we should// check with a background job of some sort.letcount=0;for(conststatusofthis.pushStatuses){if(!status.receiptStatus){count+=1;}}if(count>0){if(backoffIndex>=BACKOFF_SECONDS.length){logger.warn(`Missing ${count}/${this.pushStatuses.length} push receipts after`+` 60s, giving up.`);return;}setTimeout(()=>this.updatePushReceipts(backoffIndex+1),BACKOFF_SECONDS[backoffIndex]);}},};messageSchema.statics={asynccreateFromMessageData(messageData: MessageData): Promise<MessageDocument>{returnthis.create({from: messageData.from,text: messageData.text,conversationId: messageData.conversationId,});},};}interfaceConversationMember{_id: Types.ObjectId;userId: Types.ObjectId|Document<any>;}exportinterfaceConversationSchemaextendsGeneratedConversationSchema{}exportinterfaceConversationModelextendsGeneratedConversationModel{onMemberAdded?: (this: ConversationModel,doc: ConversationDocument,member: ConversationMember)=>Promise<void>|void;onMemberRemoved?: (this: ConversationModel,doc: ConversationDocument,member: ConversationMember)=>Promise<void>|void;}exportfunctionconversationPlugin(conversationSchema: Schema){conversationSchema.add({members: [{userId: {type: Schema.Types.ObjectId,ref: DEFAULT_USER_MODEL,required: true,},},],});conversationSchema.methods={// Actually send the message. If the members have push tokens, sends the message via push. Can// also send va SMS if enabled. This function should be called from a worker or not awaited from// a request handler as it will try to update the push status for up to 1 minute.asyncsendMessage(message: MessageDocument){if(!isPopulated(this.members)){awaitthis.populate("members");awaitthis.populate("members.userId");}// const members = (this.members as ConversationMember[]).filter((m) => m._id !== message.from);constmembers=this.membersasConversationMember[];logger.debug(`Sending message ${message._id} to ${members.length} members`);this._sendPushNotifications(message,members);},// Private method to perform the push notification sending. Call sendMessage instead.async_sendPushNotifications(message: MessageDocument,members: ConversationMember[]){constpushNotificationData: any=[];constpushMembers: ConversationMember[]=[];for(constmemberofmembers){constdata=this._getExpoPushDataForMember(message,member.userId);if(data===null){continue;}pushNotificationData.push(data);pushMembers.push(member);}lettickets: ExpoPushTicket[]=[];try{tickets=(awaitexpo.sendPushNotificationsAsync(pushNotificationData))asExpoPushTicket[];}catch(error){logger.error("Error sending push notification to Expo: ",error);return;}logger.debug(`Result from sending message ${message._id}: ${JSON.stringify(tickets)}`);// Try to fetch push results right away. We'll follow up on this with retries.for(leti=0;i<pushMembers.length;i++){constmember=pushMembers[i];constticket: ExpoPushTicket=tickets[i];if(isExpoPushTicketSuccess(ticket)){message.pushStatuses.push({userId: member.userId,ticketStatus: ticket.status,ticketId: ticket.id,});}elseif(isExpoPushTicketError(ticket)){message.pushStatuses.push({userId: member.userId,ticketStatus: ticket.status,ticketErrorMessage: ticket.message,ticketErrorType: ticket.details?.error,});}else{logger.error(`Unknown push ticket status`,ticket,member);}}awaitmessage.updatePushReceipts();},asyncaddMember(member: ConversationMember){constConversation: any=mongoose.model(DEFAULT_CONVERSATION_MODEL);if(this.members.length>=1000){thrownewError(`Conversations are limited to 1000 members.`);}for(constmofthis.members){if(m.userId===member.userId){logger.warn(`Cannot add member for user ${member.userId}, already is a member`);return;}}this.members.push(member);constUser=mongoose.model(DEFAULT_USER_MODEL);constuser=awaitUser.findById(member.userId);if(!user){thrownewError(`Could not find user ${member.userId} to add to conversation.`);}constresult=awaitthis.save();(userasany).conversations.push({conversationId: result.id});awaituser.save();if(Conversation.onMemberAdded){awaitConversation.onMemberAdded(this,member);}returnresult;},asyncremoveMember(member: ConversationMember){constConversation: any=mongoose.model(DEFAULT_CONVERSATION_MODEL);this.members.pull({userId: member.userId});constUser=mongoose.model(DEFAULT_USER_MODEL);constuser=awaitUser.findById(member.userId);if(!user){thrownewError(`Could not find user ${member.userId} to remove from conversation.`);}constresult=awaitthis.save();(userasany).conversations.pull({conversationId: result.id});awaituser.save();if(Conversation.onMemberRemoved){awaitConversation.onMemberRemoved(this,member);}returnresult;},// Private method to build the data to send to Expo for a push notification._getExpoPushDataForMember(message: MessageDocument,member: any){constpushToken=member.expoToken;if(!pushToken){logger.debug(`Not sending message to ${member.id}, no expo token.`);returnnull;}if(!Expo.isExpoPushToken(pushToken)){logger.error(`Not sending message to ${member.id}, invalid Expo push token: ${pushToken}`);returnnull;}// TODO: come up with a good way to handle this with reasonable defaults.// if (!member.messageMethods?.push?.enabled) {// logger.debug(`Not sending message to ${member.id}, push is not enabled.`);// return null;// }if(member.messageMethods?.push?.optedOut){logger.debug(`Not sending message to ${member.id}, opted out.`);returnnull;}return{to: pushToken,sound: "default",body: "This is a test notification",data: {withSome: "data"},};},};conversationSchema.statics={createConversationForUser(user: ConversationUser,extraData: any){console.log("Creating conversation for user",user._id);returnthis.create({members: [{userId: user._id}],
...extraData,});},};}
1b2a1696350f0bf9f20315492469e2f6ee38d751
The text was updated successfully, but these errors were encountered:
joshgachnang
changed the title
make these adjustable by the calling app.
Make Message/Conversation model names adjustable by the calling app
Feb 13, 2022
make these adjustable by the calling app.
on the frontend.
mongoose-rest-framework/src/models/messaging.ts
Line 28 in 76bc8c3
1b2a1696350f0bf9f20315492469e2f6ee38d751
The text was updated successfully, but these errors were encountered: