FIRMessagingDataMessageManager.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "Firebase/Messaging/FIRMessagingDataMessageManager.h"
  17. #import "Firebase/Messaging/Protos/GtalkCore.pbobjc.h"
  18. #import "Firebase/Messaging/FIRMessagingClient.h"
  19. #import "Firebase/Messaging/FIRMessagingConnection.h"
  20. #import "Firebase/Messaging/FIRMessagingConstants.h"
  21. #import "Firebase/Messaging/FIRMessagingDefines.h"
  22. #import "Firebase/Messaging/FIRMessagingDelayedMessageQueue.h"
  23. #import "Firebase/Messaging/FIRMessagingLogger.h"
  24. #import "Firebase/Messaging/FIRMessagingReceiver.h"
  25. #import "Firebase/Messaging/FIRMessagingRmqManager.h"
  26. #import "Firebase/Messaging/FIRMessaging_Private.h"
  27. #import "Firebase/Messaging/FIRMessagingSyncMessageManager.h"
  28. #import "Firebase/Messaging/FIRMessagingUtilities.h"
  29. #import "Firebase/Messaging/NSError+FIRMessaging.h"
  30. static const int kMaxAppDataSizeDefault = 4 * 1024; // 4k
  31. static const int kMinDelaySeconds = 1; // 1 second
  32. static const int kMaxDelaySeconds = 60 * 60; // 1 hour
  33. static NSString *const kFromForFIRMessagingMessages = @"mcs.android.com";
  34. static NSString *const kGSFMessageCategory = @"com.google.android.gsf.gtalkservice";
  35. // TODO: Update Gcm to FIRMessaging in the constants below
  36. static NSString *const kFCMMessageCategory = @"com.google.gcm";
  37. static NSString *const kMessageReservedPrefix = @"google.";
  38. static NSString *const kFCMMessageSpecialMessage = @"message_type";
  39. // special messages sent by the server
  40. static NSString *const kFCMMessageTypeDeletedMessages = @"deleted_messages";
  41. static NSString *const kMCSNotificationPrefix = @"gcm.notification.";
  42. static NSString *const kDataMessageNotificationKey = @"notification";
  43. typedef NS_ENUM(int8_t, UpstreamForceReconnect) {
  44. // Never force reconnect on upstream messages
  45. kUpstreamForceReconnectOff = 0,
  46. // Force reconnect for TTL=0 upstream messages
  47. kUpstreamForceReconnectTTL0 = 1,
  48. // Force reconnect for all upstream messages
  49. kUpstreamForceReconnectAll = 2,
  50. };
  51. @interface FIRMessagingDataMessageManager ()
  52. @property(nonatomic, readwrite, weak) FIRMessagingClient *client;
  53. @property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
  54. @property(nonatomic, readwrite, weak) FIRMessagingSyncMessageManager *syncMessageManager;
  55. @property(nonatomic, readwrite, weak) id<FIRMessagingDataMessageManagerDelegate> delegate;
  56. @property(nonatomic, readwrite, strong) FIRMessagingDelayedMessageQueue *delayedMessagesQueue;
  57. @property(nonatomic, readwrite, assign) int ttl;
  58. @property(nonatomic, readwrite, copy) NSString *deviceAuthID;
  59. @property(nonatomic, readwrite, copy) NSString *secretToken;
  60. @property(nonatomic, readwrite, assign) int maxAppDataSize;
  61. @property(nonatomic, readwrite, assign) UpstreamForceReconnect upstreamForceReconnect;
  62. @end
  63. @implementation FIRMessagingDataMessageManager
  64. - (instancetype)initWithDelegate:(id<FIRMessagingDataMessageManagerDelegate>)delegate
  65. client:(FIRMessagingClient *)client
  66. rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager
  67. syncMessageManager:(FIRMessagingSyncMessageManager *)syncMessageManager {
  68. self = [super init];
  69. if (self) {
  70. _delegate = delegate;
  71. _client = client;
  72. _rmq2Manager = rmq2Manager;
  73. _syncMessageManager = syncMessageManager;
  74. _ttl = kFIRMessagingSendTtlDefault;
  75. _maxAppDataSize = kMaxAppDataSizeDefault;
  76. // on by default
  77. _upstreamForceReconnect = kUpstreamForceReconnectAll;
  78. }
  79. return self;
  80. }
  81. - (void)setDeviceAuthID:(NSString *)deviceAuthID secretToken:(NSString *)secretToken {
  82. if (deviceAuthID.length == 0 || secretToken.length == 0) {
  83. FIRMessagingLoggerWarn(kFIRMessagingMessageCodeDataMessageManager013, @"Invalid credentials: deviceAuthID: %@, secrectToken: %@", deviceAuthID, secretToken);
  84. }
  85. self.deviceAuthID = deviceAuthID;
  86. self.secretToken = secretToken;
  87. }
  88. - (void)refreshDelayedMessages {
  89. FIRMessaging_WEAKIFY(self);
  90. self.delayedMessagesQueue =
  91. [[FIRMessagingDelayedMessageQueue alloc] initWithRmqScanner:self.rmq2Manager
  92. sendDelayedMessagesHandler:^(NSArray *messages) {
  93. FIRMessaging_STRONGIFY(self);
  94. [self sendDelayedMessages:messages];
  95. }];
  96. }
  97. - (nullable NSDictionary *)processPacket:(GtalkDataMessageStanza *)dataMessage {
  98. NSString *category = dataMessage.category;
  99. NSString *from = dataMessage.from;
  100. if ([kFCMMessageCategory isEqualToString:category] ||
  101. [kGSFMessageCategory isEqualToString:category]) {
  102. [self handleMCSDataMessage:dataMessage];
  103. return nil;
  104. } else if ([kFromForFIRMessagingMessages isEqualToString:from]) {
  105. [self handleMCSDataMessage:dataMessage];
  106. return nil;
  107. }
  108. return [self parseDataMessage:dataMessage];
  109. }
  110. - (void)handleMCSDataMessage:(GtalkDataMessageStanza *)dataMessage {
  111. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager000,
  112. @"Received message for FIRMessaging from downstream %@", dataMessage);
  113. }
  114. - (NSDictionary *)parseDataMessage:(GtalkDataMessageStanza *)dataMessage {
  115. NSMutableDictionary *message = [NSMutableDictionary dictionary];
  116. NSString *from = [dataMessage from];
  117. if (from.length) {
  118. message[kFIRMessagingFromKey] = from;
  119. }
  120. // raw data
  121. NSData *rawData = [dataMessage rawData];
  122. if (rawData.length) {
  123. message[kFIRMessagingRawDataKey] = rawData;
  124. }
  125. NSString *token = [dataMessage token];
  126. if (token.length) {
  127. message[kFIRMessagingCollapseKey] = token;
  128. }
  129. // Add the persistent_id. This would be removed later before sending the message to the device.
  130. NSString *persistentID = [dataMessage persistentId];
  131. if (persistentID.length) {
  132. message[kFIRMessagingMessageIDKey] = persistentID;
  133. }
  134. // third-party data
  135. for (GtalkAppData *item in dataMessage.appDataArray) {
  136. // do not process the "from" key -- is not useful
  137. if ([kFIRMessagingFromKey isEqualToString:item.key]) {
  138. continue;
  139. }
  140. // Filter the "gcm.notification." keys in the message
  141. if ([item.key hasPrefix:kMCSNotificationPrefix]) {
  142. NSString *key = [item.key substringFromIndex:[kMCSNotificationPrefix length]];
  143. if ([key length]) {
  144. if (!message[kDataMessageNotificationKey]) {
  145. message[kDataMessageNotificationKey] = [NSMutableDictionary dictionary];
  146. }
  147. message[kDataMessageNotificationKey][key] = item.value;
  148. } else {
  149. FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager001,
  150. @"Invalid key in MCS message: %@", key);
  151. }
  152. continue;
  153. }
  154. // Filter the "gcm.duplex" key
  155. if ([item.key isEqualToString:kFIRMessagingMessageSyncViaMCSKey]) {
  156. BOOL value = [item.value boolValue];
  157. message[kFIRMessagingMessageSyncViaMCSKey] = @(value);
  158. continue;
  159. }
  160. // do not allow keys with "reserved" keyword
  161. if ([[item.key lowercaseString] hasPrefix:kMessageReservedPrefix]) {
  162. continue;
  163. }
  164. [message setObject:item.value forKey:item.key];
  165. }
  166. // TODO: Add support for encrypting raw data later
  167. return [NSDictionary dictionaryWithDictionary:message];
  168. }
  169. - (void)didReceiveParsedMessage:(NSDictionary *)message {
  170. if ([message[kFCMMessageSpecialMessage] length]) {
  171. NSString *messageType = message[kFCMMessageSpecialMessage];
  172. if ([kFCMMessageTypeDeletedMessages isEqualToString:messageType]) {
  173. // TODO: Maybe trim down message to remove some unnecessary fields.
  174. // tell the FCM receiver of deleted messages
  175. [self.delegate didDeleteMessagesOnServer];
  176. return;
  177. }
  178. FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager002,
  179. @"Invalid message type received: %@", messageType);
  180. } else if (message[kFIRMessagingMessageSyncViaMCSKey]) {
  181. // Update SYNC_RMQ with the message
  182. BOOL isDuplicate = [self.syncMessageManager didReceiveMCSSyncMessage:message];
  183. if (isDuplicate) {
  184. return;
  185. }
  186. }
  187. NSString *messageId = message[kFIRMessagingMessageIDKey];
  188. NSDictionary *filteredMessage = [self filterInternalFIRMessagingKeysFromMessage:message];
  189. [self.delegate didReceiveMessage:filteredMessage withIdentifier:messageId];
  190. }
  191. - (NSDictionary *)filterInternalFIRMessagingKeysFromMessage:(NSDictionary *)message {
  192. NSMutableDictionary *newMessage = [NSMutableDictionary dictionaryWithDictionary:message];
  193. for (NSString *key in message) {
  194. if ([key hasPrefix:kFIRMessagingMessageInternalReservedKeyword]) {
  195. [newMessage removeObjectForKey:key];
  196. }
  197. }
  198. return [newMessage copy];
  199. }
  200. - (void)sendDataMessageStanza:(NSMutableDictionary *)dataMessage {
  201. NSNumber *ttlNumber = dataMessage[kFIRMessagingSendTTL];
  202. NSString *to = dataMessage[kFIRMessagingSendTo];
  203. NSString *msgId = dataMessage[kFIRMessagingSendMessageID];
  204. NSString *appPackage = [self categoryForUpstreamMessages];
  205. GtalkDataMessageStanza *stanza = [[GtalkDataMessageStanza alloc] init];
  206. // TODO: enforce TTL (right now only ttl=0 is special, means no storage)
  207. int ttl = [ttlNumber intValue];
  208. if (ttl < 0 || ttl > self.ttl) {
  209. ttl = self.ttl;
  210. }
  211. [stanza setTtl:ttl];
  212. [stanza setSent:FIRMessagingCurrentTimestampInSeconds()];
  213. int delay = [self delayForMessage:dataMessage];
  214. if (delay > 0) {
  215. [stanza setMaxDelay:delay];
  216. }
  217. if (msgId) {
  218. [stanza setId_p:msgId];
  219. }
  220. // collapse key as given by the sender
  221. NSString *token = dataMessage[KFIRMessagingSendMessageAppData][kFIRMessagingCollapseKey];
  222. if ([token length]) {
  223. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager003,
  224. @"FIRMessaging using %@ as collapse key", token);
  225. [stanza setToken:token];
  226. }
  227. if (!self.secretToken) {
  228. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager004,
  229. @"Trying to send data message without a secret token. "
  230. @"Authentication failed.");
  231. [self willSendDataMessageFail:stanza
  232. withMessageId:msgId
  233. error:kFIRMessagingErrorCodeMissingDeviceID];
  234. return;
  235. }
  236. if (![to length]) {
  237. [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorMissingTo];
  238. return;
  239. }
  240. [stanza setTo:to];
  241. [stanza setCategory:appPackage];
  242. // required field in the proto this is set by the server
  243. // set it to a sentinel so the runtime doesn't throw an exception
  244. [stanza setFrom:@""];
  245. // MCS itself would set the registration ID
  246. // [stanza setRegId:nil];
  247. int size = [self addData:dataMessage[KFIRMessagingSendMessageAppData] toStanza:stanza];
  248. if (size > kMaxAppDataSizeDefault) {
  249. [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSizeExceeded];
  250. return;
  251. }
  252. BOOL useRmq = (ttl != 0) && (msgId != nil);
  253. if (useRmq) {
  254. if (!self.client.isConnected) {
  255. // do nothing assuming rmq save is enabled
  256. }
  257. NSError *error;
  258. if (![self.rmq2Manager saveRmqMessage:stanza error:&error]) {
  259. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager005, @"%@", error);
  260. [self willSendDataMessageFail:stanza withMessageId:msgId error:kFIRMessagingErrorSave];
  261. return;
  262. }
  263. [self willSendDataMessageSuccess:stanza withMessageId:msgId];
  264. }
  265. // if delay > 0 we don't really care about sending the message right now
  266. // so we piggy-back on any other urgent(delay = 0) message that we are sending
  267. if (delay > 0 && [self delayMessage:stanza]) {
  268. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager006, @"Delaying Message %@",
  269. dataMessage);
  270. return;
  271. }
  272. // send delayed messages
  273. [self sendDelayedMessages:[self.delayedMessagesQueue removeDelayedMessages]];
  274. BOOL sending = [self tryToSendDataMessageStanza:stanza];
  275. if (!sending) {
  276. if (useRmq) {
  277. NSString *event __unused = [NSString stringWithFormat:@"Queued message: %@", [stanza id_p]];
  278. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager007, @"%@", event);
  279. } else {
  280. [self willSendDataMessageFail:stanza
  281. withMessageId:msgId
  282. error:kFIRMessagingErrorCodeNetwork];
  283. return;
  284. }
  285. }
  286. }
  287. - (void)sendDelayedMessages:(NSArray *)delayedMessages {
  288. for (GtalkDataMessageStanza *message in delayedMessages) {
  289. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager008,
  290. @"%@ Sending delayed message %@", @"DMM", message);
  291. [message setActualDelay:(int)(FIRMessagingCurrentTimestampInSeconds() - message.sent)];
  292. [self tryToSendDataMessageStanza:message];
  293. }
  294. }
  295. - (void)didSendDataMessageStanza:(GtalkDataMessageStanza *)message {
  296. NSString *msgId = [message id_p] ?: @"";
  297. [self.delegate didSendDataMessageWithID:msgId];
  298. }
  299. - (void)addParamWithKey:(NSString *)key
  300. value:(NSString *)val
  301. toStanza:(GtalkDataMessageStanza *)stanza {
  302. if (!key || !val) {
  303. return;
  304. }
  305. GtalkAppData *appData = [[GtalkAppData alloc] init];
  306. [appData setKey:key];
  307. [appData setValue:val];
  308. [[stanza appDataArray] addObject:appData];
  309. }
  310. /**
  311. @return The size of the data being added to stanza.
  312. */
  313. - (int)addData:(NSDictionary *)data toStanza:(GtalkDataMessageStanza *)stanza {
  314. int size = 0;
  315. for (NSString *key in data) {
  316. NSObject *val = data[key];
  317. if ([val isKindOfClass:[NSString class]]) {
  318. NSString *strVal = (NSString *)val;
  319. [self addParamWithKey:key value:strVal toStanza:stanza];
  320. size += [key length] + [strVal length];
  321. } else if ([val isKindOfClass:[NSNumber class]]) {
  322. NSString *strVal = [(NSNumber *)val stringValue];
  323. [self addParamWithKey:key value:strVal toStanza:stanza];
  324. size += [key length] + [strVal length];
  325. } else if ([kFIRMessagingRawDataKey isEqualToString:key] &&
  326. [val isKindOfClass:[NSData class]]) {
  327. NSData *rawData = (NSData *)val;
  328. [stanza setRawData:[rawData copy]];
  329. size += [rawData length];
  330. } else {
  331. FIRMessagingLoggerError(kFIRMessagingMessageCodeDataMessageManager009, @"Ignoring key: %@",
  332. key);
  333. }
  334. }
  335. return size;
  336. }
  337. /**
  338. * Notify the messenger that send data message completed with success. This is called for
  339. * TTL=0, after the message has been sent, or when message is saved, to unlock the send()
  340. * method.
  341. */
  342. - (void)willSendDataMessageSuccess:(GtalkDataMessageStanza *)stanza
  343. withMessageId:(NSString *)messageId {
  344. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager010,
  345. @"send message success: %@", messageId);
  346. [self.delegate willSendDataMessageWithID:messageId error:nil];
  347. }
  348. /**
  349. * We send 'send failures' from server as normal FIRMessaging messages, with a 'message_type'
  350. * extra - same as 'message deleted'.
  351. *
  352. * For TTL=0 or errors that can be detected during send ( too many messages, invalid, etc)
  353. * we throw IOExceptions
  354. */
  355. - (void)willSendDataMessageFail:(GtalkDataMessageStanza *)stanza
  356. withMessageId:(NSString *)messageId
  357. error:(FIRMessagingInternalErrorCode)errorCode {
  358. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager011,
  359. @"Send message fail: %@ error: %lu", messageId, (unsigned long)errorCode);
  360. NSError *error = [NSError errorWithFCMErrorCode:errorCode];
  361. if ([self.delegate respondsToSelector:@selector(willSendDataMessageWithID:error:)]) {
  362. [self.delegate willSendDataMessageWithID:messageId error:error];
  363. }
  364. }
  365. - (void)resendMessagesWithConnection:(FIRMessagingConnection *)connection {
  366. NSMutableString *rmqIdsResent = [NSMutableString string];
  367. NSMutableArray *toRemoveRmqIds = [NSMutableArray array];
  368. FIRMessaging_WEAKIFY(self);
  369. FIRMessaging_WEAKIFY(connection);
  370. FIRMessagingRmqMessageHandler messageHandler = ^(int64_t rmqId, int8_t tag, NSData *data) {
  371. FIRMessaging_STRONGIFY(self);
  372. FIRMessaging_STRONGIFY(connection);
  373. GPBMessage *proto =
  374. [FIRMessagingGetClassForTag((FIRMessagingProtoTag)tag) parseFromData:data error:NULL];
  375. if ([proto isKindOfClass:GtalkDataMessageStanza.class]) {
  376. GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
  377. if (![self handleExpirationForDataMessage:stanza]) {
  378. // time expired let's delete from RMQ
  379. [toRemoveRmqIds addObject:stanza.persistentId];
  380. return;
  381. }
  382. [rmqIdsResent appendString:[NSString stringWithFormat:@"%@,", stanza.id_p]];
  383. }
  384. [connection sendProto:proto];
  385. };
  386. [self.rmq2Manager scanWithRmqMessageHandler:messageHandler
  387. dataMessageHandler:nil];
  388. if ([rmqIdsResent length]) {
  389. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeDataMessageManager012, @"Resent: %@",
  390. rmqIdsResent);
  391. }
  392. if ([toRemoveRmqIds count]) {
  393. [self.rmq2Manager removeRmqMessagesWithRmqIds:toRemoveRmqIds];
  394. }
  395. }
  396. /**
  397. * Check the TTL and generate an error if needed.
  398. *
  399. * @return false if the message needs to be deleted
  400. */
  401. - (BOOL)handleExpirationForDataMessage:(GtalkDataMessageStanza *)message {
  402. if (message.ttl == 0) {
  403. return NO;
  404. }
  405. int64_t now = FIRMessagingCurrentTimestampInSeconds();
  406. if (now > message.sent + message.ttl) {
  407. [self willSendDataMessageFail:message
  408. withMessageId:message.id_p
  409. error:kFIRMessagingErrorServiceNotAvailable];
  410. return NO;
  411. }
  412. return YES;
  413. }
  414. #pragma mark - Private
  415. - (int)delayForMessage:(NSMutableDictionary *)message {
  416. int delay = 0; // default
  417. if (message[kFIRMessagingSendDelay]) {
  418. delay = [message[kFIRMessagingSendDelay] intValue];
  419. [message removeObjectForKey:kFIRMessagingSendDelay];
  420. if (delay < kMinDelaySeconds) {
  421. delay = 0;
  422. } else if (delay > kMaxDelaySeconds) {
  423. delay = kMaxDelaySeconds;
  424. }
  425. }
  426. return delay;
  427. }
  428. // return True if successfully delayed else False
  429. - (BOOL)delayMessage:(GtalkDataMessageStanza *)message {
  430. return [self.delayedMessagesQueue queueMessage:message];
  431. }
  432. - (BOOL)tryToSendDataMessageStanza:(GtalkDataMessageStanza *)stanza {
  433. if (self.client.isConnectionActive) {
  434. [self.client sendMessage:stanza];
  435. return YES;
  436. }
  437. // if we only reconnect for TTL = 0 messages check if we ttl = 0 or
  438. // if we reconnect for all messages try to reconnect
  439. if ((self.upstreamForceReconnect == kUpstreamForceReconnectTTL0 && stanza.ttl == 0) ||
  440. self.upstreamForceReconnect == kUpstreamForceReconnectAll) {
  441. BOOL isNetworkAvailable = [[FIRMessaging messaging] isNetworkAvailable];
  442. if (isNetworkAvailable) {
  443. if (stanza.ttl == 0) {
  444. // Add TTL = 0 messages to be sent on next connect. TTL != 0 messages are
  445. // persisted, and will be sent from the RMQ.
  446. [self.client sendOnConnectOrDrop:stanza];
  447. }
  448. [self.client retryConnectionImmediately:YES];
  449. return YES;
  450. }
  451. }
  452. return NO;
  453. }
  454. - (NSString *)categoryForUpstreamMessages {
  455. return FIRMessagingAppIdentifier();
  456. }
  457. @end