123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2014-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- #import "MessageSender.h"
- #import "Conversation.h"
- #import "BoxTextMessage.h"
- #import "BoxLocationMessage.h"
- #import "DeliveryReceiptMessage.h"
- #import "TypingIndicatorMessage.h"
- #import "GroupCreateMessage.h"
- #import "GroupLeaveMessage.h"
- #import "GroupRenameMessage.h"
- #import "GroupTextMessage.h"
- #import "GroupLocationMessage.h"
- #import "GroupRequestSyncMessage.h"
- #import "Contact.h"
- #import "ContactStore.h"
- #import "TextMessage.h"
- #import "LocationMessage.h"
- #import "MessageQueue.h"
- #import "SystemMessage.h"
- #import "ThreemaError.h"
- #import "UserSettings.h"
- #import "MyIdentityStore.h"
- #import "DatabaseManager.h"
- #import "EntityManager.h"
- #import "NSString+Hex.h"
- #import "BallotMessageEncoder.h"
- #import "Ballot.h"
- #import "GroupBallotCreateMessage.h"
- #import "GroupBallotVoteMessage.h"
- #import "Utils.h"
- #import "BundleUtil.h"
- #import "ContactSetPhotoMessage.h"
- #import "ContactPhotoSender.h"
- #import "BackgroundTaskManagerProxy.h"
- #import "PinnedHTTPSURLLoader.h"
- #import "QuoteParser.h"
- #ifdef DEBUG
- static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
- #else
- static const DDLogLevel ddLogLevel = DDLogLevelWarning;
- #endif
- @implementation MessageSender
- + (void)sendDeliveryReceiptForMessage:(BaseMessage*)message fromIdentity:(NSString*)identity {
- DDLogVerbose(@"Sending delivery receipt for message ID: %@", message.id);
- DeliveryReceiptMessage *deliveryReceipt = [[DeliveryReceiptMessage alloc] init];
- deliveryReceipt.receiptType = DELIVERYRECEIPT_MSGRECEIVED;
- deliveryReceipt.receiptMessageIds = @[message.id];
- deliveryReceipt.toIdentity = identity;
- [[MessageQueue sharedMessageQueue] enqueue:deliveryReceipt];
- }
- + (void)sendDeliveryReceiptForAbstractMessage:(AbstractMessage*)message fromIdentity:(NSString*)identity {
- NSString *messageId = [NSString stringWithHexData:message.messageId];
- DDLogVerbose(@"Sending delivery receipt for notification extension message ID: %@", messageId);
- DeliveryReceiptMessage *deliveryReceipt = [[DeliveryReceiptMessage alloc] init];
- deliveryReceipt.receiptType = DELIVERYRECEIPT_MSGRECEIVED;
- deliveryReceipt.receiptMessageIds = @[message.messageId];
- deliveryReceipt.toIdentity = identity;
- [[MessageQueue sharedMessageQueue] enqueueWait:deliveryReceipt];
- }
- + (void)sendGroupCreateMessageForGroup:(GroupProxy*)group toMember:(Contact*)toMember {
-
- NSArray *groupMembers = nil;
- if ([group.activeMemberIds containsObject:toMember.identity]) {
- groupMembers = [group.activeMemberIds allObjects];
- } else {
- groupMembers = @[group.conversation.groupMyIdentity];
- }
-
- GroupCreateMessage *createMessage = [[GroupCreateMessage alloc] init];
- createMessage.toIdentity = toMember.identity;
- createMessage.groupId = group.groupId;
- createMessage.groupMembers = groupMembers;
- [[MessageQueue sharedMessageQueue] enqueue:createMessage];
- }
- + (void)sendGroupSharedMessagesForConversation:(Conversation*)groupConversation toMember:(Contact *)newMember {
- // send own open ballots to all members
- for (Ballot *ballot in groupConversation.ballots) {
- if (ballot.isOwn && [ballot isClosed] == NO) {
- [self sendCreateMessageForBallot:ballot toContact:newMember];
-
- if (ballot.isIntermediate) {
- [self sendBallotVoteMessage:ballot toContact:newMember];
- }
- }
- }
- }
- + (void)sendGroupRenameMessageForConversation:(Conversation*)conversation addSystemMessage:(BOOL)addSystemMessage {
-
- if (conversation.groupName.length == 0) {
- return;
- }
-
- /* send rename message to all members */
- for (Contact *member in conversation.members) {
- [self sendGroupRenameMessageForConversation:conversation toMember:member addSystemMessage:addSystemMessage];
- }
-
- if (addSystemMessage) {
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performSyncBlockAndSafe:^{
- Conversation *conversationOwnContext = (Conversation *)[entityManager.entityFetcher getManagedObjectById:conversation.objectID];
-
- /* Insert system message to document this change */
- SystemMessage *systemMessage = [entityManager.entityCreator systemMessageForConversation:conversationOwnContext];
- systemMessage.type = [NSNumber numberWithInt:kSystemMessageRenameGroup];
- systemMessage.arg = [conversationOwnContext.groupName dataUsingEncoding:NSUTF8StringEncoding];
- }];
- }
- }
- + (void)sendGroupRenameMessageForConversation:(Conversation *)groupConversation toMember:(Contact*)member addSystemMessage:(BOOL)addSystemMessage {
-
- if (groupConversation.groupName.length == 0)
- return;
-
- GroupRenameMessage *renameMessage = [[GroupRenameMessage alloc] init];
- renameMessage.toIdentity = member.identity;
- renameMessage.groupId = groupConversation.groupId;
- renameMessage.name = groupConversation.groupName;
- [[MessageQueue sharedMessageQueue] enqueue:renameMessage];
- }
- + (void)sendGroupLeaveMessageForConversation:(Conversation*)groupConversation {
-
- NSString *creator;
-
- if (groupConversation.contact == nil)
- creator = [MyIdentityStore sharedMyIdentityStore].identity;
- else
- creator = groupConversation.contact.identity;
-
- /* send leave message to all members */
- for (Contact *member in groupConversation.members) {
- [self sendGroupLeaveMessageForCreator:creator groupId:groupConversation.groupId toIdentity:member.identity];
- }
- }
- + (void)sendGroupLeaveMessageForCreator:(NSString*)creator groupId:(NSData*)groupId toIdentity:(NSString*)toIdentity {
- GroupLeaveMessage *leaveMessage = [[GroupLeaveMessage alloc] init];
- leaveMessage.toIdentity = toIdentity;
- leaveMessage.groupCreator = creator;
- leaveMessage.groupId = groupId;
- [[MessageQueue sharedMessageQueue] enqueue:leaveMessage];
- }
- + (void)sendGroupRequestSyncMessageForCreatorContact:(Contact*)creatorContact groupId:(NSData*)groupId {
- GroupRequestSyncMessage *requestSyncMessage = [[GroupRequestSyncMessage alloc] init];
- requestSyncMessage.toIdentity = creatorContact.identity;
- requestSyncMessage.groupId = groupId;
- [[MessageQueue sharedMessageQueue] enqueue:requestSyncMessage];
-
- NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
- creatorContact, kKeyContact,
- nil];
- EntityManager *entityManager = [[EntityManager alloc] init];
- Group *group = [entityManager.entityFetcher groupForGroupId:groupId groupCreator:creatorContact.identity];
- if (group == nil) {
- if(![[UserSettings sharedUserSettings].blacklist containsObject:creatorContact.identity]) {
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorUnknownGroup object:nil userInfo:info];
- }
- }
- }
- + (void)sendMessage:(NSString*)message inConversation:(Conversation*)conversation async:(BOOL)async quickReply:(BOOL)quickReply requestId:(NSString *)requestId onCompletion:(void(^)(TextMessage *message, Conversation *conv))onCompletion {
-
- __block Conversation *conversationOwnContext;
- __block TextMessage *newMessage;
- __block NSString *remainingBody;
- NSData *quoteMessageId = [QuoteParser parseQuoteV2FromMessage:message remainingBody:&remainingBody];
-
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performSyncBlockAndSafe:^{
- conversationOwnContext = (Conversation *)[entityManager.entityFetcher getManagedObjectById:conversation.objectID];
- newMessage = [entityManager.entityCreator textMessageForConversation:conversationOwnContext];
-
- newMessage.text = [[UserSettings sharedUserSettings] quoteV2Active] ? remainingBody : message;
-
- if (quoteMessageId != nil) {
- newMessage.quotedMessageId = quoteMessageId;
- }
-
- if (requestId != nil) {
- newMessage.webRequestId = requestId;
- }
- }];
-
- if (conversationOwnContext.groupId != nil) {
- /* send to each group member */
- for (Contact *member in conversationOwnContext.members) {
- DDLogVerbose(@"Sending group message to %@", member.identity);
- GroupTextMessage *msg = [[GroupTextMessage alloc] init];
- msg.messageId = newMessage.id;
- msg.date = newMessage.date;
- msg.text = message;
- msg.groupId = conversationOwnContext.groupId;
-
- if (conversationOwnContext.contact == nil)
- msg.groupCreator = [MyIdentityStore sharedMyIdentityStore].identity;
- else
- msg.groupCreator = conversationOwnContext.contact.identity;
-
- msg.toIdentity = member.identity;
- if (async) {
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- } else {
- [[MessageQueue sharedMessageQueue] enqueueWait:msg];
- }
- if (!quickReply) {
- [ContactPhotoSender sendProfilePicture:msg];
- }
- }
- } else {
- BoxTextMessage *msg = [[BoxTextMessage alloc] init];
- msg.messageId = newMessage.id;
- msg.date = newMessage.date;
- msg.text = message;
- msg.toIdentity = conversationOwnContext.contact.identity;
- if (quickReply) {
- [[MessageQueue sharedMessageQueue] enqueueWaitForQuickReply:msg];
- } else {
- if (async) {
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- } else {
- [[MessageQueue sharedMessageQueue] enqueueWait:msg];
- }
- if (!quickReply) {
- [ContactPhotoSender sendProfilePicture:msg];
- }
- }
- }
-
- onCompletion(newMessage, conversationOwnContext);
- }
- + (void)sendLocation:(CLLocationCoordinate2D)coordinates accuracy:(CLLocationAccuracy)accuracy poiName:(NSString*)poiName poiAddress:(NSString*)poiAddress inConversation:(Conversation*)conversation onCompletion:(void(^)(NSData *messageId))onCompletion {
-
- __block Conversation *conversationOwnContext;
- __block LocationMessage *newMessage;
-
-
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performSyncBlockAndSafe:^{
-
- conversationOwnContext = (Conversation *)[entityManager.entityFetcher getManagedObjectById:conversation.objectID];
-
- newMessage = [entityManager.entityCreator locationMessageForConversation:conversationOwnContext];
- newMessage.latitude = [NSNumber numberWithDouble:coordinates.latitude];
- newMessage.longitude = [NSNumber numberWithDouble:coordinates.longitude];
- newMessage.accuracy = [NSNumber numberWithDouble:accuracy];
- newMessage.poiName = poiName;
- if (poiAddress != nil) {
- newMessage.poiName = [NSString stringWithFormat:@"%@\n%@", poiName, poiAddress];
- } else {
- newMessage.poiName = poiName;
- }
- }];
-
- if (conversationOwnContext.groupId != nil) {
- /* send to each group member */
- for (Contact *member in conversationOwnContext.members) {
- DDLogVerbose(@"Sending group location message to %@", member.identity);
- GroupLocationMessage *msg = [[GroupLocationMessage alloc] init];
- msg.messageId = newMessage.id;
- msg.date = newMessage.date;
- msg.latitude = coordinates.latitude;
- msg.longitude = coordinates.longitude;
- msg.accuracy = accuracy;
- msg.poiName = poiName;
- msg.poiAddress = poiAddress;
- msg.groupId = conversationOwnContext.groupId;
-
- if (conversationOwnContext.contact == nil)
- msg.groupCreator = [MyIdentityStore sharedMyIdentityStore].identity;
- else
- msg.groupCreator = conversationOwnContext.contact.identity;
-
- msg.toIdentity = member.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- [ContactPhotoSender sendProfilePicture:msg];
- }
- } else {
- BoxLocationMessage *msg = [[BoxLocationMessage alloc] init];
- msg.messageId = newMessage.id;
- msg.date = newMessage.date;
- msg.latitude = coordinates.latitude;
- msg.longitude = coordinates.longitude;
- msg.accuracy = accuracy;
- msg.poiName = poiName;
- msg.poiAddress = poiAddress;
- msg.toIdentity = conversationOwnContext.contact.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- [ContactPhotoSender sendProfilePicture:msg];
- }
-
- if (poiName == nil) {
- [Utils reverseGeocodeNearLatitude:coordinates.latitude longitude:coordinates.longitude accuracy:accuracy completion:^(NSString *label) {
- if ([newMessage wasDeleted]) {
- return;
- }
-
- [entityManager performSyncBlockAndSafe:^{
- newMessage.reverseGeocodingResult = label;
- }];
- } onError:^(NSError *error) {
- DDLogWarn(@"Reverse geocoding failed: %@", error);
- if ([newMessage wasDeleted]) {
- return;
- }
-
- [entityManager performSyncBlockAndSafe:^{
- newMessage.reverseGeocodingResult = NSLocalizedString(@"unknown_location", nil);
- }];
- }];
- }
-
- onCompletion(newMessage.id);
- }
- + (void)markMessageAsSent:(NSData*)messageId {
- /* Fetch message from DB */
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performAsyncBlockAndSafe:^{
- BaseMessage *dbmsg = [entityManager.entityFetcher ownMessageWithId: messageId];
- if (dbmsg == nil) {
- /* This can happen normally, e.g. for outgoing delivery receipts that we don't store in the DB */
- return;
- }
-
- if (dbmsg.sent == nil || dbmsg.sent.boolValue == NO) {
- dbmsg.sent = [NSNumber numberWithBool:YES];
- }
- }];
- }
- + (void)sendReadReceiptForMessages:(NSArray*)messages toIdentity:(NSString*)identity async:(BOOL)async quickReply:(BOOL)quickReply {
- if (![UserSettings sharedUserSettings].sendReadReceipts)
- return;
- [self sendReceiptForMessages:messages toIdentity:identity receiptType:DELIVERYRECEIPT_MSGREAD async:async quickReply:quickReply];
- }
- + (void)sendUserAckForMessages:(NSArray*)messages toIdentity:(NSString*)identity async:(BOOL)async quickReply:(BOOL)quickReply {
- [self sendReceiptForMessages:messages toIdentity:identity receiptType:DELIVERYRECEIPT_MSGUSERACK async:async quickReply:quickReply];
- }
- + (void)sendUserDeclineForMessages:(NSArray*)messages toIdentity:(NSString*)identity async:(BOOL)async quickReply:(BOOL)quickReply {
- [self sendReceiptForMessages:messages toIdentity:identity receiptType:DELIVERYRECEIPT_MSGUSERDECLINE async:async quickReply:quickReply];
- }
- + (void)sendReceiptForMessages:(NSArray*)messages toIdentity:(NSString*)identity receiptType:(uint8_t)receiptType async:(BOOL)async quickReply:(BOOL)quickReply {
-
- NSMutableArray *receiptMessageIds = [NSMutableArray arrayWithCapacity:messages.count];
- for (BaseMessage *message in messages) {
- @try {
- if (![identity isEqualToString:message.conversation.contact.identity]) {
- DDLogError(@"Bad from identity encountered while sending read receipt");
- return;
- }
-
- [receiptMessageIds addObject:message.id];
- }
- @catch (NSException *exception) {
- DDLogError(@"Exception while marking message as read: %@", exception);
- }
- }
-
- DDLogVerbose(@"Sending read receipt for message IDs: %@", receiptMessageIds);
-
- DeliveryReceiptMessage *deliveryReceipt = [[DeliveryReceiptMessage alloc] init];
- deliveryReceipt.receiptType = receiptType;
- deliveryReceipt.receiptMessageIds = receiptMessageIds;
- deliveryReceipt.toIdentity = identity;
- if (quickReply) {
- [[MessageQueue sharedMessageQueue] enqueueWaitForQuickReply:deliveryReceipt];
- } else {
- if (async) {
- [[MessageQueue sharedMessageQueue] enqueue:deliveryReceipt];
- } else {
- [[MessageQueue sharedMessageQueue] enqueueWait:deliveryReceipt];
- }
- }
- }
- + (void)sendTypingIndicatorMessage:(BOOL)typing toIdentity:(NSString*)identity {
- if (![UserSettings sharedUserSettings].sendTypingIndicator) {
- return;
- }
-
- DDLogVerbose(@"Sending typing indicator %@ to %@", typing ? @"on" : @"off", identity);
-
- TypingIndicatorMessage *typingIndicatorMessage = [[TypingIndicatorMessage alloc] init];
- typingIndicatorMessage.typing = typing;
- typingIndicatorMessage.toIdentity = identity;
- [[MessageQueue sharedMessageQueue] enqueue:typingIndicatorMessage];
- }
- + (void)markBlobAsDone:(NSData *)blobId {
- NSString *blobIdHex = [NSString stringWithHexData:blobId];
- NSString *blobFirstByteHex = [blobIdHex substringWithRange:NSMakeRange(0, 2)];
- NSURL *blobUrl;
-
- if ([UserSettings sharedUserSettings].enableIPv6) {
- blobUrl = [NSURL URLWithString:[NSString stringWithFormat:[BundleUtil objectForInfoDictionaryKey:@"ThreemaBlobDoneURLv6"], blobFirstByteHex, blobIdHex]];
- } else {
- blobUrl = [NSURL URLWithString:[NSString stringWithFormat:[BundleUtil objectForInfoDictionaryKey:@"ThreemaBlobDoneURL"], blobFirstByteHex, blobIdHex]];
- }
-
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:blobUrl cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:kBlobLoadTimeout];
- request.HTTPMethod = @"POST";
-
- PinnedHTTPSURLLoader *loader = [[PinnedHTTPSURLLoader alloc] init];
- [loader startWithURLRequest:request onCompletion:^(NSData *data) {
- DDLogInfo(@"Blob ID %@ marked as done", blobIdHex);
- } onError:^(NSError *error) {
- DDLogWarn(@"Error marking blob ID %@ as done: %@", blobIdHex, error);
- }];
- }
- + (void)sendCreateMessageForBallot:(Ballot *)ballot toContact:(Contact *)contact {
- EntityManager *entityManager = [[EntityManager alloc] init];
-
- Ballot *ballotOwnContext = (Ballot *)[entityManager.entityFetcher getManagedObjectById:ballot.objectID];
- Conversation *conversation = ballotOwnContext.conversation;
-
- BoxBallotCreateMessage *boxMessage = [BallotMessageEncoder encodeCreateMessageForBallot: ballot];
- boxMessage.messageId = [AbstractMessage randomMessageId];
-
- GroupBallotCreateMessage *msg = [BallotMessageEncoder groupBallotCreateMessageFrom:boxMessage forConversation:conversation];
- msg.toIdentity = contact.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- + (void)sendCreateMessageForBallot:(Ballot *)ballot {
-
- __block Ballot *ballotOwnContext;
- __block Conversation *conversation;
- __block BallotMessage *message;
-
- EntityManager *entityManager = [[EntityManager alloc] init];
- [entityManager performSyncBlockAndSafe:^{
- ballotOwnContext = (Ballot *)[entityManager.entityFetcher getManagedObjectById:ballot.objectID];
-
- conversation = ballotOwnContext.conversation;
- message = [entityManager.entityCreator ballotMessageForConversation:conversation];
- message.ballot = ballotOwnContext;
- message.conversation.lastMessage = message;
- }];
-
- BoxBallotCreateMessage *boxMessage = [BallotMessageEncoder encodeCreateMessageForBallot: ballot];
- boxMessage.messageId = message.id;
-
- if (conversation.groupId != nil) {
- /* send to each group member */
- for (Contact *member in conversation.members) {
- DDLogVerbose(@"Sending ballot create message to %@", member.identity);
-
- GroupBallotCreateMessage *msg = [BallotMessageEncoder groupBallotCreateMessageFrom:boxMessage forConversation:conversation];
-
- msg.toIdentity = member.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- } else {
- boxMessage.toIdentity = conversation.contact.identity;
- [[MessageQueue sharedMessageQueue] enqueue:boxMessage];
- }
- }
- + (void)sendBallotVoteMessage:(Ballot *)ballot toContact:(Contact *) contact {
- EntityManager *entityManager = [[EntityManager alloc] init];
-
- Ballot *ballotOwnContext = (Ballot *)[entityManager.entityFetcher getManagedObjectById:ballot.objectID];
- Conversation *conversation = ballotOwnContext.conversation;
-
- BoxBallotVoteMessage *boxMessage = [BallotMessageEncoder encodeVoteMessageForBallot: ballot];
- boxMessage.messageId = [AbstractMessage randomMessageId];
-
- GroupBallotVoteMessage *msg = [BallotMessageEncoder groupBallotVoteMessageFrom:boxMessage forConversation:conversation];
- msg.toIdentity = contact.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- + (void)sendBallotVoteMessage:(Ballot *)ballot {
- Conversation *conversation = ballot.conversation;
- BoxBallotVoteMessage *boxMessage = [BallotMessageEncoder encodeVoteMessageForBallot: ballot];
-
- if (conversation.groupId != nil) {
- /* send to each group member */
- for (Contact *member in conversation.members) {
- DDLogVerbose(@"Sending ballot vote message to %@", member.identity);
-
- GroupBallotVoteMessage *msg = [BallotMessageEncoder groupBallotVoteMessageFrom:boxMessage forConversation:conversation];
-
- msg.toIdentity = member.identity;
- [[MessageQueue sharedMessageQueue] enqueue:msg];
- }
- } else {
- boxMessage.toIdentity = conversation.contact.identity;
- [[MessageQueue sharedMessageQueue] enqueue:boxMessage];
- }
- }
- @end
|