Go to the source code of this file.
Data Structures | |
struct | ast_call_feature |
main call feature structure More... | |
Defines | |
#define | FEATURE_APP_ARGS_LEN 256 |
#define | FEATURE_APP_LEN 64 |
#define | FEATURE_EXTEN_LEN 32 |
#define | FEATURE_MAX_LEN 11 |
#define | FEATURE_MOH_LEN 80 |
#define | FEATURE_SNAME_LEN 32 |
Functions | |
int | ast_bridge_call (struct ast_channel *chan, struct ast_channel *peer, struct ast_bridge_config *config) |
Bridge a call, optionally allowing redirection. | |
int | ast_masq_park_call (struct ast_channel *rchan, struct ast_channel *host, int timeout, int *extout) |
Park a call via a masqueraded channel. | |
int | ast_park_call (struct ast_channel *chan, struct ast_channel *host, int timeout, int *extout) |
Park a call and read back parked location. | |
char * | ast_parking_ext (void) |
Determine system parking extension Returns the call parking extension for drivers that provide special call parking help. | |
int | ast_pickup_call (struct ast_channel *chan) |
Pickup a call. | |
char * | ast_pickup_ext (void) |
Determine system call pickup extension. | |
void | ast_register_feature (struct ast_call_feature *feature) |
register new feature into feature_set | |
void | ast_unregister_feature (struct ast_call_feature *feature) |
unregister feature from feature_set |
Definition in file features.h.
#define FEATURE_APP_ARGS_LEN 256 |
Definition at line 29 of file features.h.
#define FEATURE_APP_LEN 64 |
Definition at line 28 of file features.h.
#define FEATURE_EXTEN_LEN 32 |
Definition at line 31 of file features.h.
#define FEATURE_MAX_LEN 11 |
#define FEATURE_MOH_LEN 80 |
Definition at line 32 of file features.h.
#define FEATURE_SNAME_LEN 32 |
Definition at line 30 of file features.h.
int ast_bridge_call | ( | struct ast_channel * | chan, | |
struct ast_channel * | peer, | |||
struct ast_bridge_config * | config | |||
) |
Bridge a call, optionally allowing redirection.
append the event to featurecode. we rely on the string being zero-filled, and not overflowing it.
Definition at line 1403 of file res_features.c.
References ast_channel::appl, ast_answer(), ast_cdr_alloc(), ast_cdr_appenduserfield(), ast_cdr_discard(), AST_CDR_FLAG_LOCKED, ast_cdr_init(), ast_cdr_merge(), ast_cdr_setdestchan(), ast_cdr_setuserfield(), ast_cdr_start(), ast_channel_bridge(), ast_channel_lock, ast_channel_setoption(), ast_channel_unlock, ast_check_hangup(), ast_clear_flag, AST_CONTROL_ATXFERCMD, AST_CONTROL_BUSY, AST_CONTROL_CONGESTION, AST_CONTROL_FLASH, AST_CONTROL_HANGUP, AST_CONTROL_HOLD, AST_CONTROL_OPTION, AST_CONTROL_RINGING, AST_CONTROL_UNHOLD, ast_dtmf_stream(), ast_feature_interpret(), AST_FEATURE_PLAY_WARNING, AST_FLAG_ZOMBIE, AST_FRAME_CONTROL, AST_FRAME_DTMF, AST_FRAME_DTMF_BEGIN, ast_frfree, ast_indicate(), ast_indicate_data(), ast_log(), AST_OPTION_FLAG_REQUEST, ast_strdupa, ast_strlen_zero(), ast_test_flag, ast_channel::cdr, ast_cdr::channel, cmd_atxfer(), config, ast_channel::data, ast_option_header::data, ast_cdr::dstchannel, f, FEATURE_MAX_LEN, FEATURE_RETURN_PASSDIGITS, FEATURE_RETURN_SUCCESS, FEATURE_SENSE_CHAN, FEATURE_SENSE_PEER, ast_option_header::flag, free, LOG_DEBUG, LOG_WARNING, monitor_app, ast_option_header::option, option_debug, pbx_builtin_getvar_helper(), pbx_builtin_setvar_helper(), pbx_exec(), pbx_findapp(), set_config_flags(), and ast_cdr::userfield.
Referenced by app_exec(), ast_bridge_call_thread(), do_atxfer(), park_exec(), and try_calling().
01404 { 01405 /* Copy voice back and forth between the two channels. Give the peer 01406 the ability to transfer calls with '#<extension' syntax. */ 01407 struct ast_frame *f; 01408 struct ast_channel *who; 01409 char chan_featurecode[FEATURE_MAX_LEN + 1]=""; 01410 char peer_featurecode[FEATURE_MAX_LEN + 1]=""; 01411 int res; 01412 int diff; 01413 int hasfeatures=0; 01414 int hadfeatures=0; 01415 struct ast_option_header *aoh; 01416 struct ast_bridge_config backup_config; 01417 struct ast_cdr *bridge_cdr; 01418 01419 memset(&backup_config, 0, sizeof(backup_config)); 01420 01421 config->start_time = ast_tvnow(); 01422 01423 if (chan && peer) { 01424 pbx_builtin_setvar_helper(chan, "BRIDGEPEER", peer->name); 01425 pbx_builtin_setvar_helper(peer, "BRIDGEPEER", chan->name); 01426 } else if (chan) 01427 pbx_builtin_setvar_helper(chan, "BLINDTRANSFER", NULL); 01428 01429 if (monitor_ok) { 01430 const char *monitor_exec; 01431 struct ast_channel *src = NULL; 01432 if (!monitor_app) { 01433 if (!(monitor_app = pbx_findapp("Monitor"))) 01434 monitor_ok=0; 01435 } 01436 if ((monitor_exec = pbx_builtin_getvar_helper(chan, "AUTO_MONITOR"))) 01437 src = chan; 01438 else if ((monitor_exec = pbx_builtin_getvar_helper(peer, "AUTO_MONITOR"))) 01439 src = peer; 01440 if (monitor_app && src) { 01441 char *tmp = ast_strdupa(monitor_exec); 01442 pbx_exec(src, monitor_app, tmp); 01443 } 01444 } 01445 01446 set_config_flags(chan, peer, config); 01447 config->firstpass = 1; 01448 01449 /* Answer if need be */ 01450 if (ast_answer(chan)) 01451 return -1; 01452 peer->appl = "Bridged Call"; 01453 peer->data = chan->name; 01454 01455 /* copy the userfield from the B-leg to A-leg if applicable */ 01456 if (chan->cdr && peer->cdr && !ast_strlen_zero(peer->cdr->userfield)) { 01457 char tmp[256]; 01458 if (!ast_strlen_zero(chan->cdr->userfield)) { 01459 snprintf(tmp, sizeof(tmp), "%s;%s", chan->cdr->userfield, peer->cdr->userfield); 01460 ast_cdr_appenduserfield(chan, tmp); 01461 } else 01462 ast_cdr_setuserfield(chan, peer->cdr->userfield); 01463 /* free the peer's cdr without ast_cdr_free complaining */ 01464 free(peer->cdr); 01465 peer->cdr = NULL; 01466 } 01467 01468 for (;;) { 01469 struct ast_channel *other; /* used later */ 01470 01471 res = ast_channel_bridge(chan, peer, config, &f, &who); 01472 01473 if (config->feature_timer) { 01474 /* Update time limit for next pass */ 01475 diff = ast_tvdiff_ms(ast_tvnow(), config->start_time); 01476 config->feature_timer -= diff; 01477 if (hasfeatures) { 01478 /* Running on backup config, meaning a feature might be being 01479 activated, but that's no excuse to keep things going 01480 indefinitely! */ 01481 if (backup_config.feature_timer && ((backup_config.feature_timer -= diff) <= 0)) { 01482 if (option_debug) 01483 ast_log(LOG_DEBUG, "Timed out, realtime this time!\n"); 01484 config->feature_timer = 0; 01485 who = chan; 01486 if (f) 01487 ast_frfree(f); 01488 f = NULL; 01489 res = 0; 01490 } else if (config->feature_timer <= 0) { 01491 /* Not *really* out of time, just out of time for 01492 digits to come in for features. */ 01493 if (option_debug) 01494 ast_log(LOG_DEBUG, "Timed out for feature!\n"); 01495 if (!ast_strlen_zero(peer_featurecode)) { 01496 ast_dtmf_stream(chan, peer, peer_featurecode, 0); 01497 memset(peer_featurecode, 0, sizeof(peer_featurecode)); 01498 } 01499 if (!ast_strlen_zero(chan_featurecode)) { 01500 ast_dtmf_stream(peer, chan, chan_featurecode, 0); 01501 memset(chan_featurecode, 0, sizeof(chan_featurecode)); 01502 } 01503 if (f) 01504 ast_frfree(f); 01505 hasfeatures = !ast_strlen_zero(chan_featurecode) || !ast_strlen_zero(peer_featurecode); 01506 if (!hasfeatures) { 01507 /* Restore original (possibly time modified) bridge config */ 01508 memcpy(config, &backup_config, sizeof(struct ast_bridge_config)); 01509 memset(&backup_config, 0, sizeof(backup_config)); 01510 } 01511 hadfeatures = hasfeatures; 01512 /* Continue as we were */ 01513 continue; 01514 } else if (!f) { 01515 /* The bridge returned without a frame and there is a feature in progress. 01516 * However, we don't think the feature has quite yet timed out, so just 01517 * go back into the bridge. */ 01518 continue; 01519 } 01520 } else { 01521 if (config->feature_timer <=0) { 01522 /* We ran out of time */ 01523 config->feature_timer = 0; 01524 who = chan; 01525 if (f) 01526 ast_frfree(f); 01527 f = NULL; 01528 res = 0; 01529 } 01530 } 01531 } 01532 if (res < 0) { 01533 if (!ast_test_flag(chan, AST_FLAG_ZOMBIE) && !ast_test_flag(peer, AST_FLAG_ZOMBIE) && !ast_check_hangup(chan) && !ast_check_hangup(peer)) 01534 ast_log(LOG_WARNING, "Bridge failed on channels %s and %s\n", chan->name, peer->name); 01535 return -1; 01536 } 01537 01538 if (!f || (f->frametype == AST_FRAME_CONTROL && 01539 (f->subclass == AST_CONTROL_HANGUP || f->subclass == AST_CONTROL_BUSY || 01540 f->subclass == AST_CONTROL_CONGESTION ) ) ) { 01541 res = -1; 01542 break; 01543 } 01544 /* many things should be sent to the 'other' channel */ 01545 other = (who == chan) ? peer : chan; 01546 if (f->frametype == AST_FRAME_CONTROL) { 01547 switch (f->subclass) { 01548 case AST_CONTROL_RINGING: 01549 case AST_CONTROL_FLASH: 01550 case -1: 01551 ast_indicate(other, f->subclass); 01552 break; 01553 case AST_CONTROL_HOLD: 01554 case AST_CONTROL_UNHOLD: 01555 ast_indicate_data(other, f->subclass, f->data, f->datalen); 01556 break; 01557 case AST_CONTROL_OPTION: 01558 aoh = f->data; 01559 /* Forward option Requests */ 01560 if (aoh && aoh->flag == AST_OPTION_FLAG_REQUEST) { 01561 ast_channel_setoption(other, ntohs(aoh->option), aoh->data, 01562 f->datalen - sizeof(struct ast_option_header), 0); 01563 } 01564 break; 01565 case AST_CONTROL_ATXFERCMD: 01566 cmd_atxfer(chan, peer, config, who, f->data); 01567 break; 01568 } 01569 } else if (f->frametype == AST_FRAME_DTMF_BEGIN) { 01570 /* eat it */ 01571 } else if (f->frametype == AST_FRAME_DTMF) { 01572 char *featurecode; 01573 int sense; 01574 01575 hadfeatures = hasfeatures; 01576 /* This cannot overrun because the longest feature is one shorter than our buffer */ 01577 if (who == chan) { 01578 sense = FEATURE_SENSE_CHAN; 01579 featurecode = chan_featurecode; 01580 } else { 01581 sense = FEATURE_SENSE_PEER; 01582 featurecode = peer_featurecode; 01583 } 01584 /*! append the event to featurecode. we rely on the string being zero-filled, and 01585 * not overflowing it. 01586 * \todo XXX how do we guarantee the latter ? 01587 */ 01588 featurecode[strlen(featurecode)] = f->subclass; 01589 /* Get rid of the frame before we start doing "stuff" with the channels */ 01590 ast_frfree(f); 01591 f = NULL; 01592 config->feature_timer = backup_config.feature_timer; 01593 res = ast_feature_interpret(chan, peer, config, featurecode, sense); 01594 switch(res) { 01595 case FEATURE_RETURN_PASSDIGITS: 01596 ast_dtmf_stream(other, who, featurecode, 0); 01597 /* Fall through */ 01598 case FEATURE_RETURN_SUCCESS: 01599 memset(featurecode, 0, sizeof(chan_featurecode)); 01600 break; 01601 } 01602 if (res >= FEATURE_RETURN_PASSDIGITS) { 01603 res = 0; 01604 } else 01605 break; 01606 hasfeatures = !ast_strlen_zero(chan_featurecode) || !ast_strlen_zero(peer_featurecode); 01607 if (hadfeatures && !hasfeatures) { 01608 /* Restore backup */ 01609 memcpy(config, &backup_config, sizeof(struct ast_bridge_config)); 01610 memset(&backup_config, 0, sizeof(struct ast_bridge_config)); 01611 } else if (hasfeatures) { 01612 if (!hadfeatures) { 01613 /* Backup configuration */ 01614 memcpy(&backup_config, config, sizeof(struct ast_bridge_config)); 01615 /* Setup temporary config options */ 01616 config->play_warning = 0; 01617 ast_clear_flag(&(config->features_caller), AST_FEATURE_PLAY_WARNING); 01618 ast_clear_flag(&(config->features_callee), AST_FEATURE_PLAY_WARNING); 01619 config->warning_freq = 0; 01620 config->warning_sound = NULL; 01621 config->end_sound = NULL; 01622 config->start_sound = NULL; 01623 config->firstpass = 0; 01624 } 01625 config->start_time = ast_tvnow(); 01626 config->feature_timer = featuredigittimeout; 01627 if (option_debug) 01628 ast_log(LOG_DEBUG, "Set time limit to %ld\n", config->feature_timer); 01629 } 01630 } 01631 if (f) 01632 ast_frfree(f); 01633 01634 } 01635 01636 /* arrange the cdrs */ 01637 bridge_cdr = ast_cdr_alloc(); 01638 if (bridge_cdr) { 01639 if (chan->cdr && peer->cdr) { /* both of them? merge */ 01640 ast_channel_lock(chan); /* lock the channel before modifing cdrs */ 01641 ast_cdr_init(bridge_cdr,chan); /* seems more logicaller to use the destination as a base, but, really, it's random */ 01642 ast_cdr_start(bridge_cdr); /* now is the time to start */ 01643 01644 /* absorb the channel cdr */ 01645 ast_cdr_merge(bridge_cdr, chan->cdr); 01646 if (!ast_test_flag(chan->cdr, AST_CDR_FLAG_LOCKED)) 01647 ast_cdr_discard(chan->cdr); /* if locked cdrs are in chan, they are taken over in the merge */ 01648 01649 chan->cdr = NULL; /* remove pointer to freed memory before releasing the lock */ 01650 01651 ast_channel_unlock(chan); 01652 01653 /* absorb the peer cdr */ 01654 ast_channel_lock(peer); 01655 ast_cdr_merge(bridge_cdr, peer->cdr); 01656 if (!ast_test_flag(peer->cdr, AST_CDR_FLAG_LOCKED)) 01657 ast_cdr_discard(peer->cdr); /* if locked cdrs are in peer, they are taken over in the merge */ 01658 01659 peer->cdr = NULL; 01660 ast_channel_unlock(peer); 01661 01662 ast_channel_lock(chan); 01663 chan->cdr = bridge_cdr; /* make this available to the rest of the world via the chan while the call is in progress */ 01664 ast_channel_unlock(chan); 01665 01666 } else if (chan->cdr) { 01667 01668 ast_channel_lock(chan); /* Lock before modifying CDR */ 01669 /* take the cdr from the channel - literally */ 01670 ast_cdr_init(bridge_cdr,chan); 01671 /* absorb this data */ 01672 ast_cdr_merge(bridge_cdr, chan->cdr); 01673 if (!ast_test_flag(chan->cdr, AST_CDR_FLAG_LOCKED)) 01674 ast_cdr_discard(chan->cdr); /* if locked cdrs are in chan, they are taken over in the merge */ 01675 chan->cdr = bridge_cdr; /* make this available to the rest of the world via the chan while the call is in progress */ 01676 ast_channel_unlock(chan); 01677 } else if (peer->cdr) { 01678 ast_channel_lock(peer); /* Lock before modifying CDR */ 01679 /* take the cdr from the peer - literally */ 01680 ast_cdr_init(bridge_cdr,peer); 01681 /* absorb this data */ 01682 ast_cdr_merge(bridge_cdr, peer->cdr); 01683 if (!ast_test_flag(peer->cdr, AST_CDR_FLAG_LOCKED)) 01684 ast_cdr_discard(peer->cdr); /* if locked cdrs are in chan, they are taken over in the merge */ 01685 peer->cdr = NULL; 01686 peer->cdr = bridge_cdr; /* make this available to the rest of the world via the chan while the call is in progress */ 01687 ast_channel_unlock(peer); 01688 } else { 01689 ast_channel_lock(chan); /* Lock before modifying CDR */ 01690 /* make up a new cdr */ 01691 ast_cdr_init(bridge_cdr,chan); /* eh, just pick one of them */ 01692 chan->cdr = bridge_cdr; /* */ 01693 ast_channel_unlock(chan); 01694 } 01695 if (ast_strlen_zero(bridge_cdr->dstchannel)) { 01696 if (strcmp(bridge_cdr->channel, peer->name) != 0) 01697 ast_cdr_setdestchan(bridge_cdr, peer->name); 01698 else 01699 ast_cdr_setdestchan(bridge_cdr, chan->name); 01700 } 01701 } 01702 return res; 01703 }
int ast_masq_park_call | ( | struct ast_channel * | rchan, | |
struct ast_channel * | host, | |||
int | timeout, | |||
int * | extout | |||
) |
Park a call via a masqueraded channel.
rchan | the real channel to be parked | |
host | the channel to have the parking read to Masquerade the channel rchan into a new, empty channel which is then parked with ast_park_call | |
timeout | is a timeout in milliseconds | |
extout | is a parameter to an int that will hold the parked location, or NULL if you want |
Definition at line 455 of file res_features.c.
References ast_channel::amaflags, ast_channel_alloc(), ast_channel_masquerade(), ast_frfree, ast_log(), ast_read(), AST_STATE_DOWN, ast_strdupa, ast_channel::context, ast_channel::exten, f, LOG_WARNING, park_call_full(), ast_channel::priority, ast_channel::readformat, set_c_e_p(), and ast_channel::writeformat.
Referenced by manager_park(), mgcp_ss(), parkandannounce_exec(), and ss_thread().
00456 { 00457 struct ast_channel *chan; 00458 struct ast_frame *f; 00459 char *orig_chan_name = NULL; 00460 00461 /* Make a new, fake channel that we'll use to masquerade in the real one */ 00462 if (!(chan = ast_channel_alloc(0, AST_STATE_DOWN, 0, 0, rchan->accountcode, rchan->exten, rchan->context, rchan->amaflags, "Parked/%s",rchan->name))) { 00463 ast_log(LOG_WARNING, "Unable to create parked channel\n"); 00464 return -1; 00465 } 00466 00467 /* Make formats okay */ 00468 chan->readformat = rchan->readformat; 00469 chan->writeformat = rchan->writeformat; 00470 ast_channel_masquerade(chan, rchan); 00471 00472 /* Setup the extensions and such */ 00473 set_c_e_p(chan, rchan->context, rchan->exten, rchan->priority); 00474 00475 /* Make the masq execute */ 00476 f = ast_read(chan); 00477 if (f) 00478 ast_frfree(f); 00479 00480 orig_chan_name = ast_strdupa(chan->name); 00481 00482 park_call_full(chan, peer, timeout, extout, orig_chan_name); 00483 00484 return 0; 00485 }
int ast_park_call | ( | struct ast_channel * | chan, | |
struct ast_channel * | peer, | |||
int | timeout, | |||
int * | extout | |||
) |
Park a call and read back parked location.
Definition at line 450 of file res_features.c.
References park_call_full().
Referenced by builtin_blindtransfer(), builtin_parkcall(), iax_park_thread(), and sip_park_thread().
00451 { 00452 return park_call_full(chan, peer, timeout, extout, NULL); 00453 }
char* ast_parking_ext | ( | void | ) |
Determine system parking extension Returns the call parking extension for drivers that provide special call parking help.
Definition at line 159 of file res_features.c.
Referenced by builtin_blindtransfer(), dp_lookup(), handle_request_refer(), mgcp_ss(), socket_process(), and ss_thread().
00160 { 00161 return parking_ext; 00162 }
int ast_pickup_call | ( | struct ast_channel * | chan | ) |
Pickup a call.
Definition at line 2253 of file res_features.c.
References ast_channel::_state, ast_answer(), ast_channel_masquerade(), ast_channel_unlock, ast_channel_walk_locked(), AST_CONTROL_ANSWER, ast_log(), ast_queue_control(), AST_STATE_RING, AST_STATE_RINGING, ast_channel::callgroup, LOG_DEBUG, LOG_WARNING, option_debug, ast_channel::pbx, and ast_channel::pickupgroup.
Referenced by cb_events(), handle_request_invite(), mgcp_ss(), and ss_thread().
02254 { 02255 struct ast_channel *cur = NULL; 02256 int res = -1; 02257 02258 while ( (cur = ast_channel_walk_locked(cur)) != NULL) { 02259 if (!cur->pbx && 02260 (cur != chan) && 02261 (chan->pickupgroup & cur->callgroup) && 02262 ((cur->_state == AST_STATE_RINGING) || 02263 (cur->_state == AST_STATE_RING))) { 02264 break; 02265 } 02266 ast_channel_unlock(cur); 02267 } 02268 if (cur) { 02269 if (option_debug) 02270 ast_log(LOG_DEBUG, "Call pickup on chan '%s' by '%s'\n",cur->name, chan->name); 02271 res = ast_answer(chan); 02272 if (res) 02273 ast_log(LOG_WARNING, "Unable to answer '%s'\n", chan->name); 02274 res = ast_queue_control(chan, AST_CONTROL_ANSWER); 02275 if (res) 02276 ast_log(LOG_WARNING, "Unable to queue answer on '%s'\n", chan->name); 02277 res = ast_channel_masquerade(cur, chan); 02278 if (res) 02279 ast_log(LOG_WARNING, "Unable to masquerade '%s' into '%s'\n", chan->name, cur->name); /* Done */ 02280 ast_channel_unlock(cur); 02281 } else { 02282 if (option_debug) 02283 ast_log(LOG_DEBUG, "No call pickup possible...\n"); 02284 } 02285 return res; 02286 }
char* ast_pickup_ext | ( | void | ) |
Determine system call pickup extension.
Definition at line 164 of file res_features.c.
Referenced by cb_events(), get_destination(), handle_request_invite(), handle_showfeatures(), mgcp_ss(), and ss_thread().
00165 { 00166 return pickup_ext; 00167 }
void ast_register_feature | ( | struct ast_call_feature * | feature | ) |
register new feature into feature_set
feature | an ast_call_feature object which contains a keysequence and a callback function which is called when this keysequence is pressed during a call. |
Definition at line 963 of file res_features.c.
References AST_LIST_INSERT_HEAD, AST_LIST_LOCK, AST_LIST_UNLOCK, ast_log(), ast_verbose(), LOG_NOTICE, option_verbose, ast_call_feature::sname, and VERBOSE_PREFIX_2.
00964 { 00965 if (!feature) { 00966 ast_log(LOG_NOTICE,"You didn't pass a feature!\n"); 00967 return; 00968 } 00969 00970 AST_LIST_LOCK(&feature_list); 00971 AST_LIST_INSERT_HEAD(&feature_list,feature,feature_entry); 00972 AST_LIST_UNLOCK(&feature_list); 00973 00974 if (option_verbose >= 2) 00975 ast_verbose(VERBOSE_PREFIX_2 "Registered Feature '%s'\n",feature->sname); 00976 }
void ast_unregister_feature | ( | struct ast_call_feature * | feature | ) |
unregister feature from feature_set
feature | the ast_call_feature object which was registered before |
Definition at line 979 of file res_features.c.
References AST_LIST_LOCK, AST_LIST_REMOVE, AST_LIST_UNLOCK, and free.
00980 { 00981 if (!feature) 00982 return; 00983 00984 AST_LIST_LOCK(&feature_list); 00985 AST_LIST_REMOVE(&feature_list,feature,feature_entry); 00986 AST_LIST_UNLOCK(&feature_list); 00987 free(feature); 00988 }