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
// TODO: Add support for threading messages and replies.
/** * 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,});},};}
d89063813a173e71fbd5d7a97e85c2256ee321e4
The text was updated successfully, but these errors were encountered:
Add support for threading messages and replies.
const res = await axios.post("https://exp.host/--/api/v2/push/getReceipts", {
check with a background job of some sort.
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.
mongoose-rest-framework/src/models/messaging.ts
Line 114 in 76bc8c3
d89063813a173e71fbd5d7a97e85c2256ee321e4
The text was updated successfully, but these errors were encountered: